From 03ae77ee729dbb27b70401ed6ad5264e65d666a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:40:15 +0300 Subject: [PATCH 001/261] build(deps): bump Externals/imgui from `3109131` to `4fa59df` (#1994) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- .../xrRender/PBRConverter/ONNXModelRunner.cpp | 1756 ++++++++++++++ .../xrRender/PBRConverter/ONNXModelRunner.h | 304 +++ .../xrRender/PBRConverter/PBRConversionUI.cpp | 169 ++ .../xrRender/PBRConverter/PBRConversionUI.h | 56 + .../PBRConverter/PBRTextureConverter.cpp | 2014 +++++++++++++++++ .../PBRConverter/PBRTextureConverter.h | 220 ++ 7 files changed, 4520 insertions(+), 1 deletion(-) create mode 100644 src/Layers/xrRender/PBRConverter/ONNXModelRunner.cpp create mode 100644 src/Layers/xrRender/PBRConverter/ONNXModelRunner.h create mode 100644 src/Layers/xrRender/PBRConverter/PBRConversionUI.cpp create mode 100644 src/Layers/xrRender/PBRConverter/PBRConversionUI.h create mode 100644 src/Layers/xrRender/PBRConverter/PBRTextureConverter.cpp create mode 100644 src/Layers/xrRender/PBRConverter/PBRTextureConverter.h diff --git a/Externals/imgui b/Externals/imgui index 3109131a882..4fa59df9fa2 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 3109131a882daec56a530aff540416983c240443 +Subproject commit 4fa59df9fa20e4e2212070c5b1bc6cc52237093f diff --git a/src/Layers/xrRender/PBRConverter/ONNXModelRunner.cpp b/src/Layers/xrRender/PBRConverter/ONNXModelRunner.cpp new file mode 100644 index 00000000000..4527b7fff25 --- /dev/null +++ b/src/Layers/xrRender/PBRConverter/ONNXModelRunner.cpp @@ -0,0 +1,1756 @@ +// xrRender/PBRConverter/ONNXModelRunner.cpp +// ONNX Runtime integration for AI-based PBR conversion + +#include "stdafx.h" +#include "ONNXModelRunner.h" +#include "xrCore/FS.h" + +#include +#include +#include +#include +#include + +#ifdef USE_AI_PBR +// ONNX Runtime C++ API +#include +#include + +namespace xray::render::pbr { + +// ══════════════════════════════════════════════════════════ +// STATIC MEMBERS +// ══════════════════════════════════════════════════════════ + +std::unique_ptr ONNXModelRunner::env_; +bool ONNXModelRunner::env_initialized_ = false; + +// ══════════════════════════════════════════════════════════ +// TENSOR IMPLEMENTATION +// ══════════════════════════════════════════════════════════ + +Tensor Tensor::Create(u32 batch, u32 channels, u32 height, u32 width) { + Tensor tensor; + tensor.shape = { + static_cast(batch), + static_cast(channels), + static_cast(height), + static_cast(width) + }; + const size_t total_size = batch * channels * height * width; + tensor.data.resize(total_size, 0.0f); + return tensor; +} + +Tensor Tensor::FromImageData(const u8* pixels, u32 width, u32 height, u32 channels) { + Msg("~ [Tensor::FromImageData] Creating tensor from image: width=%u, height=%u, channels=%u", + width, height, channels); + Msg("~ Will create tensor shape: [1, %u, %u, %u] (NCHW)", channels, height, width); + + Tensor tensor = Create(1, channels, height, width); + + // Convert u8 [0-255] → float [0.0-1.0] and transpose HWC → CHW + // Assumes pixels are in row-major HWC format with 'width' pixels per row + for (u32 c = 0; c < channels; ++c) { + for (u32 h = 0; h < height; ++h) { + for (u32 w = 0; w < width; ++w) { + const u32 src_idx = (h * width + w) * channels + c; // HWC + const u32 dst_idx = c * height * width + h * width + w; // CHW + tensor.data[dst_idx] = static_cast(pixels[src_idx]) / 255.0f; + } + } + } + + return tensor; +} + +xr_vector Tensor::ToImageData(bool apply_sigmoid) const { + const u32 C = channels(); + const u32 H = height(); + const u32 W = width(); + + xr_vector pixels(C * H * W); + + // DEBUG: Compute tensor statistics + float min_val = FLT_MAX, max_val = -FLT_MAX, sum_val = 0.0f; + for (const float& val : data) { + min_val = std::min(min_val, val); + max_val = std::max(max_val, val); + sum_val += val; + } + float mean_val = sum_val / static_cast(data.size()); + + Msg("~ [Tensor::ToImageData] Shape: [%u,%u,%u,%u], Range: [%.4f, %.4f], Mean: %.4f, Sigmoid: %s", + batch(), C, H, W, min_val, max_val, mean_val, apply_sigmoid ? "YES" : "NO"); + + // Convert CHW → HWC and float → u8 [0-255] + for (u32 c = 0; c < C; ++c) { + for (u32 h = 0; h < H; ++h) { + for (u32 w = 0; w < W; ++w) { + const u32 src_idx = c * H * W + h * W + w; // CHW + const u32 dst_idx = (h * W + w) * C + c; // HWC + + float value = data[src_idx]; + + // Apply sigmoid if requested (matching Python: torch.sigmoid()) + if (apply_sigmoid) { + value = 1.0f / (1.0f + std::exp(-value)); + } + + // Clamp to [0, 1] and convert to [0, 255] (matching Python: .clamp(0, 1)) + value = std::clamp(value, 0.0f, 1.0f); + pixels[dst_idx] = static_cast(value * 255.0f); + } + } + } + + return pixels; +} + +// ══════════════════════════════════════════════════════════ +// ONNX MODEL RUNNER IMPLEMENTATION +// ══════════════════════════════════════════════════════════ + +// Custom logger callback to forward ONNX Runtime/TensorRT logs to X-Ray's Msg() system +static void ORT_API_CALL ONNXLoggerCallback(void* param, OrtLoggingLevel severity, const char* category, + const char* logger_id, const char* code_location, const char* message) { + // Map ONNX Runtime severity levels to X-Ray log prefixes + const char* prefix = ""; + switch (severity) { + case ORT_LOGGING_LEVEL_VERBOSE: + prefix = "~"; // Verbose/debug + break; + case ORT_LOGGING_LEVEL_INFO: + prefix = "*"; // Info + break; + case ORT_LOGGING_LEVEL_WARNING: + prefix = "!"; // Warning + break; + case ORT_LOGGING_LEVEL_ERROR: + case ORT_LOGGING_LEVEL_FATAL: + prefix = "!!!"; // Error + break; + } + + // Forward to X-Ray logging system + // Format: [category] message + if (category && category[0] != '\0' && severity >= ORT_LOGGING_LEVEL_ERROR) { + Msg("%s [%s] %s", prefix, category, message); + } +} + +bool ONNXModelRunner::InitializeEnvironment() { + if (env_initialized_) { + return true; + } + + try { + // Create environment with custom logger callback + // This captures ALL ONNX Runtime and TensorRT logs + OrtLoggingFunction logging_function = ONNXLoggerCallback; + env_ = std::make_unique( + ORT_LOGGING_LEVEL_ERROR, // Capture everything + "OpenXRay_PBR", + logging_function, + nullptr // No user data needed + ); + env_initialized_ = true; + Msg("[ONNXModelRunner] ONNX Runtime environment initialized with custom logger"); + return true; + } + catch (const Ort::Exception& e) { + Msg("! [ONNXModelRunner] Failed to initialize ONNX Runtime: %s", e.what()); + return false; + } +} + +void ONNXModelRunner::ShutdownEnvironment() { + if (env_initialized_) { + env_.reset(); + env_initialized_ = false; + Msg("[ONNXModelRunner] ONNX Runtime environment shut down"); + } +} + +ONNXModelRunner::~ONNXModelRunner() { + session_.reset(); + memory_info_.reset(); +} + +ONNXModelRunner::ONNXModelRunner(ONNXModelRunner&& other) noexcept + : session_(std::move(other.session_)) + , memory_info_(std::move(other.memory_info_)) + , model_path_(std::move(other.model_path_)) + , use_gpu_(other.use_gpu_) + , input_names_(std::move(other.input_names_)) + , output_names_(std::move(other.output_names_)) +{ +} + +ONNXModelRunner& ONNXModelRunner::operator=(ONNXModelRunner&& other) noexcept { + if (this != &other) { + session_ = std::move(other.session_); + memory_info_ = std::move(other.memory_info_); + model_path_ = std::move(other.model_path_); + use_gpu_ = other.use_gpu_; + input_names_ = std::move(other.input_names_); + output_names_ = std::move(other.output_names_); + } + return *this; +} + +bool ONNXModelRunner::LoadModel(const char* model_path, bool use_gpu, const char* trt_cache_path, + const char* trt_profile_min, const char* trt_profile_max, const char* trt_profile_opt) { + if (!env_initialized_) { + Msg("! [ONNXModelRunner] Environment not initialized. Call InitializeEnvironment() first."); + return false; + } + + // model_path is already a fully resolved path from FS.update_path() + if (!FS.exist(model_path, FSType::External)) { + Msg("! [ONNXModelRunner] Model file not found: %s", model_path); + return false; + } + + // Create session options + Ort::SessionOptions session_options; + session_options.SetIntraOpNumThreads(std::thread::hardware_concurrency()); + session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); + session_options.SetLogSeverityLevel(1); // Info and above + session_options.SetExecutionMode(ExecutionMode::ORT_PARALLEL); + + // Try to enable GPU acceleration + use_gpu_ = false; + if (use_gpu) { +#ifdef XR_PLATFORM_APPLE + bool shapes_pinned = false; + if (trt_profile_opt && trt_profile_opt[0]) { + u32 c = 0, h = 0, w = 0; + const char* dims = strchr(trt_profile_opt, ':'); + if (dims && sscanf(dims + 1, "1x%ux%ux%u", &c, &h, &w) == 3) { + session_options.AddFreeDimensionOverrideByName("batch", 1); + session_options.AddFreeDimensionOverrideByName("height", h); + session_options.AddFreeDimensionOverrideByName("width", w); + shapes_pinned = true; + } + + const char* comma = strchr(trt_profile_opt, ','); + const char* feat_dims = comma ? strchr(comma, ':') : nullptr; + u32 fh = 0, fw = 0; + if (feat_dims && sscanf(feat_dims + 1, "1x512x%ux%u", &fh, &fw) == 2) { + session_options.AddFreeDimensionOverrideByName("feat_height", fh); + session_options.AddFreeDimensionOverrideByName("feat_width", fw); + } + } + + try { + std::unordered_map coreml_opts; + coreml_opts["ModelFormat"] = "MLProgram"; + coreml_opts["MLComputeUnits"] = "ALL"; + coreml_opts["RequireStaticInputShapes"] = "1"; + if (trt_cache_path && trt_cache_path[0]) { + coreml_opts["ModelCacheDirectory"] = trt_cache_path; + } + session_options.AppendExecutionProvider("CoreML", coreml_opts); + Msg("[ONNXModelRunner] CoreML acceleration enabled (GPU/ANE, shapes %s)", + shapes_pinned ? "pinned" : "dynamic - CPU fallback for dynamic subgraphs"); + use_gpu_ = true; + } + catch (const Ort::Exception& e) { + Msg("! [ONNXModelRunner] CoreML provider unavailable, falling back to CPU: %s", e.what()); + } +#else + // Use TensorRT V2 API for NVIDIA GPUs (has advanced options like build logging) + Ort::TensorRTProviderOptions trt_options{}; + + // Build options map + std::unordered_map trt_opts; + + // Basic settings + trt_opts["device_id"] = "0"; + trt_opts["trt_max_workspace_size"] = "2147483648"; // 2GB + trt_opts["trt_fp16_enable"] = "1"; // Enable FP16 tensor cores + + if (trt_profile_min && trt_profile_min[0]) trt_opts["trt_profile_min_shapes"] = trt_profile_min; + if (trt_profile_max && trt_profile_max[0]) trt_opts["trt_profile_max_shapes"] = trt_profile_max; + if (trt_profile_opt && trt_profile_opt[0]) trt_opts["trt_profile_opt_shapes"] = trt_profile_opt; + + // Engine caching + trt_opts["trt_engine_cache_enable"] = "1"; + if (trt_cache_path && trt_cache_path[0] != '\0') { + trt_opts["trt_engine_cache_path"] = trt_cache_path; + } + + trt_opts["trt_builder_optimization_level"] = "0"; + + trt_opts["trt_build_heuristics_enable"] = "1"; + + trt_opts["trt_timing_cache_enable"] = "1"; + if (trt_cache_path && trt_cache_path[0] != '\0') { + std::string cache_str(trt_cache_path); + auto last_sep = cache_str.find_last_of("\\/"); + std::string timing_path = (last_sep != std::string::npos) ? cache_str.substr(0, last_sep) : cache_str; + trt_opts["trt_timing_cache_path"] = timing_path; + } + + trt_options.Update(trt_opts); + session_options.AppendExecutionProvider_TensorRT_V2(*trt_options); + Msg("[ONNXModelRunner] TensorRT V2 GPU acceleration enabled (FP16, profile shapes constrained)"); + use_gpu_ = true; +#endif + } + + auto create_session = [&](Ort::SessionOptions& options) { +#ifdef XR_PLATFORM_WINDOWS + // ONNX Runtime expects wide-string paths on Windows + std::string str_path(model_path); + std::wstring wide_path(str_path.begin(), str_path.end()); + session_ = std::make_unique(*env_, wide_path.c_str(), options); +#else + session_ = std::make_unique(*env_, model_path, options); +#endif + }; + + try { + create_session(session_options); + } + catch (const Ort::Exception& e) { + if (!use_gpu_) { + Msg("! [ONNXModelRunner] Session creation failed: %s", e.what()); + return false; + } + + Msg("! [ONNXModelRunner] GPU session creation failed, retrying CPU-only: %s", e.what()); + use_gpu_ = false; + + Ort::SessionOptions cpu_options; + cpu_options.SetIntraOpNumThreads(std::thread::hardware_concurrency()); + cpu_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); + cpu_options.SetLogSeverityLevel(1); + cpu_options.SetExecutionMode(ExecutionMode::ORT_PARALLEL); + + try { + create_session(cpu_options); + } + catch (const Ort::Exception& e2) { + Msg("! [ONNXModelRunner] CPU session creation failed: %s", e2.what()); + return false; + } + } + + // Create memory info + memory_info_ = std::make_unique( + Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault) + ); + + // Cache input/output names + Ort::AllocatorWithDefaultOptions allocator; + + const size_t num_inputs = session_->GetInputCount(); + input_names_.reserve(num_inputs); + for (size_t i = 0; i < num_inputs; ++i) { + auto name_ptr = session_->GetInputNameAllocated(i, allocator); + input_names_.emplace_back(name_ptr.get()); + } + + const size_t num_outputs = session_->GetOutputCount(); + output_names_.reserve(num_outputs); + for (size_t i = 0; i < num_outputs; ++i) { + auto name_ptr = session_->GetOutputNameAllocated(i, allocator); + output_names_.emplace_back(name_ptr.get()); + } + + model_path_ = model_path; + + Msg("[ONNXModelRunner] Loaded model: %s (%zu inputs, %zu outputs)", + model_path, num_inputs, num_outputs); + + return true; +} + +bool ONNXModelRunner::IsLoaded() const { + return session_ != nullptr; +} + +xr_vector ONNXModelRunner::GetInputNames() const { + return input_names_; +} + +xr_vector ONNXModelRunner::GetOutputNames() const { + return output_names_; +} + +Ort::Value ONNXModelRunner::TensorToOrtValue(const Tensor& tensor) { + // Create Ort::Value from Tensor data + return Ort::Value::CreateTensor( + *memory_info_, + const_cast(tensor.data.data()), + tensor.data.size(), + tensor.shape.data(), + tensor.shape.size() + ); +} + +Tensor ONNXModelRunner::OrtValueToTensor(Ort::Value& ort_value) { + Tensor tensor; + + // Get shape + const auto type_info = ort_value.GetTensorTypeAndShapeInfo(); + tensor.shape = type_info.GetShape(); + + // Copy data + const float* data_ptr = ort_value.GetTensorData(); + const size_t data_size = std::accumulate( + tensor.shape.begin(), + tensor.shape.end(), + 1LL, + std::multiplies() + ); + tensor.data.assign(data_ptr, data_ptr + data_size); + + return tensor; +} + +Tensor ONNXModelRunner::Run(const Tensor& input) { + if (!IsLoaded()) { + Msg("! [ONNXModelRunner] Model not loaded"); + return Tensor{}; + } + + try { + // Create input tensor + auto input_tensor = TensorToOrtValue(input); + + // Prepare input/output names + std::vector input_names_cstr = { input_names_[0].c_str() }; + std::vector output_names_cstr = { output_names_[0].c_str() }; + + // Run inference + auto output_tensors = session_->Run( + Ort::RunOptions{nullptr}, + input_names_cstr.data(), + &input_tensor, + 1, + output_names_cstr.data(), + 1 + ); + + // Convert output + return OrtValueToTensor(output_tensors[0]); + } + catch (const Ort::Exception& e) { + Msg("! [ONNXModelRunner] Inference failed: %s", e.what()); + return Tensor{}; + } +} + +xr_vector ONNXModelRunner::Run( + const xr_vector& inputs, + const xr_vector& input_names, + const xr_vector& output_names) +{ + if (!IsLoaded()) { + Msg("! [ONNXModelRunner] Model not loaded"); + return {}; + } + + try { + // Create input tensors + xr_vector input_tensors; + input_tensors.reserve(inputs.size()); + for (const auto& tensor : inputs) { + input_tensors.emplace_back(TensorToOrtValue(tensor)); + } + + // Run inference + auto output_tensors = session_->Run( + Ort::RunOptions{nullptr}, + input_names.data(), + input_tensors.data(), + inputs.size(), + output_names.data(), + output_names.size() + ); + + // Convert outputs + xr_vector results; + results.reserve(output_tensors.size()); + for (auto& ort_tensor : output_tensors) { + results.emplace_back(OrtValueToTensor(ort_tensor)); + } + + return results; + } + catch (const Ort::Exception& e) { + Msg("! [ONNXModelRunner] Inference failed: %s", e.what()); + return {}; + } +} + +// ══════════════════════════════════════════════════════════ +// PREPROCESSING UTILITIES +// ══════════════════════════════════════════════════════════ + +// ImageNet STANDARD normalization (for UNets) +// mean = [0.5, 0.5, 0.5], std = [0.5, 0.5, 0.5] +// Used by: UNetAlbedo, UNetSingleChannel models +void PreprocessingUtils::NormalizeImageNetStandard(Tensor& tensor) { + constexpr float MEAN[] = {0.5f, 0.5f, 0.5f}; + constexpr float STD[] = {0.5f, 0.5f, 0.5f}; + + const u32 H = tensor.height(); + const u32 W = tensor.width(); + + // Only normalize first 3 channels (RGB) + const u32 channels_to_normalize = std::min(tensor.channels(), 3u); + + for (u32 c = 0; c < channels_to_normalize; ++c) { + for (u32 h = 0; h < H; ++h) { + for (u32 w = 0; w < W; ++w) { + const u32 idx = c * H * W + h * W + w; + tensor.data[idx] = (tensor.data[idx] - MEAN[c]) / STD[c]; + } + } + } +} + +// ImageNet DEFAULT normalization (for SegFormer) +// mean = [0.485, 0.456, 0.406], std = [0.229, 0.224, 0.225] +// Used by: SegFormer model (requires standard ImageNet stats) +void PreprocessingUtils::NormalizeImageNetDefault(Tensor& tensor) { + constexpr float MEAN[] = {0.485f, 0.456f, 0.406f}; + constexpr float STD[] = {0.229f, 0.224f, 0.225f}; + + const u32 H = tensor.height(); + const u32 W = tensor.width(); + + // Only normalize first 3 channels (RGB) + const u32 channels_to_normalize = std::min(tensor.channels(), 3u); + + for (u32 c = 0; c < channels_to_normalize; ++c) { + for (u32 h = 0; h < H; ++h) { + for (u32 w = 0; w < W; ++w) { + const u32 idx = c * H * W + h * W + w; + tensor.data[idx] = (tensor.data[idx] - MEAN[c]) / STD[c]; + } + } + } +} + +// Legacy function - defaults to DEFAULT normalization for backwards compatibility +void PreprocessingUtils::NormalizeImageNet(Tensor& tensor) { + NormalizeImageNetDefault(tensor); +} + +Tensor PreprocessingUtils::ComputeMeanCurvature(const Tensor& normal_map) { + const u32 H = normal_map.height(); + const u32 W = normal_map.width(); + + // Step 1: Apply 3x3 median blur to normals (matching Python: K.filters.median_blur) + Tensor blurred = Tensor::Create(1, 3, H, W); + for (u32 c = 0; c < 3; ++c) { + for (u32 h = 1; h < H - 1; ++h) { + for (u32 w = 1; w < W - 1; ++w) { + // Collect 3x3 neighborhood + float neighbors[9]; + int idx = 0; + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + neighbors[idx++] = normal_map.data[c * H * W + (h + dy) * W + (w + dx)]; + } + } + // Median of 9 values + std::nth_element(neighbors, neighbors + 4, neighbors + 9); + blurred.data[c * H * W + h * W + w] = neighbors[4]; + } + } + } + + // Step 2: Compute Sobel gradients on blurred normals + Tensor curvature = Tensor::Create(1, 1, H, W); + + constexpr float sobel_x[9] = { + -1.0f, 0.0f, 1.0f, + -2.0f, 0.0f, 2.0f, + -1.0f, 0.0f, 1.0f + }; + constexpr float sobel_y[9] = { + -1.0f, -2.0f, -1.0f, + 0.0f, 0.0f, 0.0f, + 1.0f, 2.0f, 1.0f + }; + + for (u32 h = 1; h < H - 1; ++h) { + for (u32 w = 1; w < W - 1; ++w) { + // Compute dnx_dx (x-derivative of normal's x component) + float dnx_dx = 0.0f; + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + const u32 idx = 0 * H * W + (h + dy) * W + (w + dx); // Channel 0 (nx) + dnx_dx += blurred.data[idx] * sobel_x[(dy + 1) * 3 + (dx + 1)]; + } + } + + // Compute dny_dy (y-derivative of normal's y component) + float dny_dy = 0.0f; + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + const u32 idx = 1 * H * W + (h + dy) * W + (w + dx); // Channel 1 (ny) + dny_dy += blurred.data[idx] * sobel_y[(dy + 1) * 3 + (dx + 1)]; + } + } + + // Step 3: Mean curvature = 0.5 * (dnx_dx + dny_dy), then absolute value + // (Matching Python: curv = 0.5 * (dnx_dx + dny_dy); curv = curv.abs()) + curvature.data[h * W + w] = std::abs(0.5f * (dnx_dx + dny_dy)); + } + } + + // Step 4: Percentile normalize using 99th percentile (matching Python) + std::vector sorted_values = curvature.data; + std::sort(sorted_values.begin(), sorted_values.end()); + const size_t p99_idx = static_cast(sorted_values.size() * 0.99f); + const float p99 = std::max(sorted_values[p99_idx], 1e-6f); + + for (auto& val : curvature.data) { + val = std::clamp(val / p99, 0.0f, 1.0f); + } + + // Step 5: Remap to [-1, 1] (matching Python: curv = (curv - 0.5) * 2.0) + for (auto& val : curvature.data) { + val = (val - 0.5f) * 2.0f; + } + + return curvature; +} + +Tensor PreprocessingUtils::ComputePoissonCoarse(const Tensor& normal_map) { + const u32 H = normal_map.height(); + const u32 W = normal_map.width(); + + // Step 1: Apply 3x3 median blur to normals (matching Python: K.filters.median_blur) + Tensor blurred = Tensor::Create(1, 3, H, W); + for (u32 c = 0; c < 3; ++c) { + for (u32 h = 1; h < H - 1; ++h) { + for (u32 w = 1; w < W - 1; ++w) { + // Collect 3x3 neighborhood + float neighbors[9]; + int idx = 0; + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + neighbors[idx++] = normal_map.data[c * H * W + (h + dy) * W + (w + dx)]; + } + } + // Median of 9 values + std::nth_element(neighbors, neighbors + 4, neighbors + 9); + blurred.data[c * H * W + h * W + w] = neighbors[4]; + } + } + } + + // Step 2: Compute slopes from normals (matching Python) + Tensor height = Tensor::Create(1, 1, H, W); + + for (u32 h = 0; h < H; ++h) { + for (u32 w = 0; w < W; ++w) { + const u32 idx = h * W + w; + + // Get normal components (already normalized in [0,1], convert to [-1,1]) + const float nx = blurred.data[0 * H * W + idx] * 2.0f - 1.0f; + const float ny = blurred.data[1 * H * W + idx] * 2.0f - 1.0f; + const float nz = std::max(blurred.data[2 * H * W + idx] * 2.0f - 1.0f, 1e-3f); + + // Compute slopes (matching Python: gx = nx / nz, gy = ny / nz) + const float grad_x = std::clamp(nx / nz, -10.0f, 10.0f); + const float grad_y = std::clamp(ny / nz, -10.0f, 10.0f); + + // Simple integration (approximation of FFT Poisson solve) + float accumulated = 0.0f; + if (w > 0) accumulated += height.data[h * W + (w - 1)] + grad_x; + if (h > 0 && w == 0) accumulated += height.data[(h - 1) * W + w] + grad_y; + height.data[idx] = accumulated * 0.5f; + } + } + + // Step 3: Normalize to [0, 1] + float min_h = *std::min_element(height.data.begin(), height.data.end()); + float max_h = *std::max_element(height.data.begin(), height.data.end()); + float range_h = max_h - min_h; + + if (range_h > 1e-6f) { + for (auto& val : height.data) { + val = (val - min_h) / range_h; + } + } + + // Step 4: Percentile clipping (matching Python: p1, p99) + std::vector sorted_values = height.data; + std::sort(sorted_values.begin(), sorted_values.end()); + const size_t p1_idx = static_cast(sorted_values.size() * 0.01f); + const size_t p99_idx = static_cast(sorted_values.size() * 0.99f); + const float p1 = sorted_values[p1_idx]; + const float p99 = sorted_values[p99_idx]; + + for (auto& val : height.data) { + val = std::clamp(val, p1, p99); + val = (val - p1) / (p99 - p1 + 1e-6f); + } + + // Step 5: Gaussian blur (15x15, sigma=5) to smooth the result + // (matching Python: K.filters.gaussian_blur2d with kernel_size=15, sigma=5) + Tensor blurred_height = Tensor::Create(1, 1, H, W); + const int kernel_radius = 7; // 15x15 kernel + const float sigma = 5.0f; + + // Gaussian weights + float kernel[15][15]; + float kernel_sum = 0.0f; + for (int ky = -kernel_radius; ky <= kernel_radius; ++ky) { + for (int kx = -kernel_radius; kx <= kernel_radius; ++kx) { + const float dist_sq = static_cast(kx * kx + ky * ky); + kernel[ky + kernel_radius][kx + kernel_radius] = std::exp(-dist_sq / (2.0f * sigma * sigma)); + kernel_sum += kernel[ky + kernel_radius][kx + kernel_radius]; + } + } + // Normalize kernel + for (int ky = 0; ky < 15; ++ky) { + for (int kx = 0; kx < 15; ++kx) { + kernel[ky][kx] /= kernel_sum; + } + } + + // Apply Gaussian blur + for (u32 h = kernel_radius; h < H - kernel_radius; ++h) { + for (u32 w = kernel_radius; w < W - kernel_radius; ++w) { + float sum = 0.0f; + for (int ky = -kernel_radius; ky <= kernel_radius; ++ky) { + for (int kx = -kernel_radius; kx <= kernel_radius; ++kx) { + sum += height.data[(h + ky) * W + (w + kx)] * kernel[ky + kernel_radius][kx + kernel_radius]; + } + } + blurred_height.data[h * W + w] = sum; + } + } + + // Step 6: Remap to [-1, 1] (matching Python: (blur_torch - 0.5) * 2.0) + for (auto& val : blurred_height.data) { + val = (val - 0.5f) * 2.0f; + } + + return blurred_height; +} + +Tensor PreprocessingUtils::LogitsToMask(const Tensor& logits, u32 num_classes) { + const u32 H = logits.height(); + const u32 W = logits.width(); + + Tensor mask = Tensor::Create(1, num_classes, H, W); + + // Softmax over classes, then threshold + for (u32 h = 0; h < H; ++h) { + for (u32 w = 0; w < W; ++w) { + // Find max logit for softmax stability + float max_logit = -std::numeric_limits::infinity(); + for (u32 c = 0; c < num_classes; ++c) { + const u32 idx = c * H * W + h * W + w; + max_logit = std::max(max_logit, logits.data[idx]); + } + + // Compute softmax + float sum_exp = 0.0f; + for (u32 c = 0; c < num_classes; ++c) { + const u32 idx = c * H * W + h * W + w; + sum_exp += std::exp(logits.data[idx] - max_logit); + } + + // Normalize + for (u32 c = 0; c < num_classes; ++c) { + const u32 idx = c * H * W + h * W + w; + mask.data[idx] = std::exp(logits.data[idx] - max_logit) / sum_exp; + } + } + } + + return mask; +} + +Tensor PreprocessingUtils::Upsample(const Tensor& input, u32 target_height, u32 target_width) { + const u32 C = input.channels(); + const u32 src_H = input.height(); + const u32 src_W = input.width(); + + Tensor output = Tensor::Create(1, C, target_height, target_width); + + // Bilinear interpolation + const float scale_h = static_cast(src_H) / static_cast(target_height); + const float scale_w = static_cast(src_W) / static_cast(target_width); + + for (u32 c = 0; c < C; ++c) { + for (u32 h = 0; h < target_height; ++h) { + for (u32 w = 0; w < target_width; ++w) { + const float src_h = h * scale_h; + const float src_w = w * scale_w; + + const u32 h0 = static_cast(src_h); + const u32 w0 = static_cast(src_w); + const u32 h1 = std::min(h0 + 1, src_H - 1); + const u32 w1 = std::min(w0 + 1, src_W - 1); + + const float dh = src_h - h0; + const float dw = src_w - w0; + + // Get source values + const u32 idx00 = c * src_H * src_W + h0 * src_W + w0; + const u32 idx01 = c * src_H * src_W + h0 * src_W + w1; + const u32 idx10 = c * src_H * src_W + h1 * src_W + w0; + const u32 idx11 = c * src_H * src_W + h1 * src_W + w1; + + const float v00 = input.data[idx00]; + const float v01 = input.data[idx01]; + const float v10 = input.data[idx10]; + const float v11 = input.data[idx11]; + + // Bilinear interpolation + const float v0 = v00 * (1.0f - dw) + v01 * dw; + const float v1 = v10 * (1.0f - dw) + v11 * dw; + const float v = v0 * (1.0f - dh) + v1 * dh; + + const u32 out_idx = c * target_height * target_width + h * target_width + w; + output.data[out_idx] = v; + } + } + } + + return output; +} + +// ══════════════════════════════════════════════════════════ +// PBR PIPELINE IMPLEMENTATION +// ══════════════════════════════════════════════════════════ + +bool PBRPipeline::Initialize(const PBRPipelineConfig& config) { + config_ = config; + + Msg("[PBRPipeline] Initializing AI models from: $game_data$/%s", config.model_dir.c_str()); + + // Initialize ONNX Runtime environment + if (!ONNXModelRunner::InitializeEnvironment()) { + return false; + } + + string_path seg_path, albedo_path, albedo_uncond_path, parallax_path, ao_path, metallic_path, roughness_path; + FS.update_path(seg_path, "$game_data$", (config.model_dir + "\\segformer.onnx").c_str()); + FS.update_path(albedo_path, "$game_data$", (config.model_dir + "\\unet_albedo.onnx").c_str()); + FS.update_path(albedo_uncond_path, "$game_data$", (config.model_dir + "\\unet_albedo_uncond.onnx").c_str()); + FS.update_path(parallax_path, "$game_data$", (config.model_dir + "\\unet_parallax.onnx").c_str()); + FS.update_path(ao_path, "$game_data$", (config.model_dir + "\\unet_ao.onnx").c_str()); + FS.update_path(metallic_path, "$game_data$", (config.model_dir + "\\unet_metallic.onnx").c_str()); + FS.update_path(roughness_path, "$game_data$", (config.model_dir + "\\unet_roughness.onnx").c_str()); + + char* const model_paths[] = { seg_path, albedo_path, albedo_uncond_path, parallax_path, ao_path, metallic_path, roughness_path }; + for (char* p : model_paths) + convert_path_separators(p); + + bool all_exist = true; + all_exist &= FS.exist(seg_path, FSType::External); + all_exist &= FS.exist(albedo_path, FSType::External); + all_exist &= FS.exist(albedo_uncond_path, FSType::External); + all_exist &= FS.exist(parallax_path, FSType::External); + all_exist &= FS.exist(ao_path, FSType::External); + all_exist &= FS.exist(metallic_path, FSType::External); + all_exist &= FS.exist(roughness_path, FSType::External); + + if (!all_exist) { + Msg("! [PBRPipeline] One or more model files not found"); + return false; + } + + segformer_path_ = seg_path; + unet_albedo_path_ = albedo_path; + unet_albedo_uncond_path_ = albedo_uncond_path; + unet_parallax_path_ = parallax_path; + unet_ao_path_ = ao_path; + unet_metallic_path_ = metallic_path; + unet_roughness_path_ = roughness_path; + + string_path cache_path; + FS.update_path(cache_path, "$game_data$", (config.model_dir + "\\trt_cache").c_str()); + convert_path_separators(cache_path); + trt_cache_path_ = cache_path; + Msg("[PBRPipeline] TensorRT cache path: %s", trt_cache_path_.c_str()); + + initialized_ = true; + Msg("[PBRPipeline] All model paths validated (models loaded on-demand)"); + return true; +} + +static std::string TRT_ProfileSingle(const char* input_name, u32 channels, u32 H, u32 W) { + char buf[128]; + xr_sprintf(buf, sizeof(buf), "%s:1x%ux%ux%u", input_name, channels, H, W); + return buf; +} + +static std::string TRT_ProfileWithFeatures(const char* input_name, u32 channels, u32 H, u32 W) { + u32 fH = std::max(H / 32, 1u); + u32 fW = std::max(W / 32, 1u); + char buf[256]; + xr_sprintf(buf, sizeof(buf), "%s:1x%ux%ux%u,seg_features:1x512x%ux%u", + input_name, channels, H, W, fH, fW); + return buf; +} + +static void FormatResCachePath(char* buf, size_t bufSize, const char* trt_cache_path, u32 W, u32 H) { + xr_sprintf(buf, bufSize, "%s\\%ux%u", trt_cache_path, W, H); + convert_path_separators(buf); + std::filesystem::create_directories(buf); +} + +void PBRPipeline::ResetAllCachedModels() { + cached_unet_albedo_uncond_ = ONNXModelRunner(); + cached_segformer_ = ONNXModelRunner(); + cached_unet_albedo_ = ONNXModelRunner(); + cached_unet_parallax_ = ONNXModelRunner(); + cached_unet_ao_ = ONNXModelRunner(); + cached_unet_metallic_ = ONNXModelRunner(); + cached_unet_roughness_ = ONNXModelRunner(); + loaded_stage_ = LoadedStage::None; + cached_H_ = 0; + cached_W_ = 0; +} + +bool PBRPipeline::EnsureStage1ModelsLoaded(u32 H, u32 W) { + if (cached_H_ == H && cached_W_ == W && + (loaded_stage_ == LoadedStage::Stage1 || loaded_stage_ == LoadedStage::All) && + cached_unet_albedo_uncond_.IsLoaded() && cached_segformer_.IsLoaded() && + cached_unet_albedo_.IsLoaded()) { + return true; + } + + Msg("[PBRPipeline] Loading Stage1 models for %ux%u", W, H); + + auto prof_uncond = TRT_ProfileSingle("image", 6, H, W); + auto prof_seg = TRT_ProfileSingle("input", 6, H, W); + auto prof_albedo = TRT_ProfileWithFeatures("image", 6, H, W); + + char res_cache[512]; + FormatResCachePath(res_cache, sizeof(res_cache), trt_cache_path_.c_str(), W, H); + ResetAllCachedModels(); + + bool ok = true; + ok = ok && cached_unet_albedo_uncond_.LoadModel(unet_albedo_uncond_path_.c_str(), config_.use_gpu, res_cache, + prof_uncond.c_str(), prof_uncond.c_str(), prof_uncond.c_str()); + ok = ok && cached_segformer_.LoadModel(segformer_path_.c_str(), config_.use_gpu, res_cache, + prof_seg.c_str(), prof_seg.c_str(), prof_seg.c_str()); + ok = ok && cached_unet_albedo_.LoadModel(unet_albedo_path_.c_str(), config_.use_gpu, res_cache, + prof_albedo.c_str(), prof_albedo.c_str(), prof_albedo.c_str()); + + if (ok) { + cached_H_ = H; + cached_W_ = W; + loaded_stage_ = LoadedStage::Stage1; + } else { + Msg("! [PBRPipeline] Failed to load Stage1 models for %ux%u", W, H); + ResetAllCachedModels(); + } + + return ok; +} + +bool PBRPipeline::EnsureStage2ModelsLoaded(u32 H, u32 W) { + if (cached_H_ == H && cached_W_ == W && + (loaded_stage_ == LoadedStage::Stage2 || loaded_stage_ == LoadedStage::All) && + cached_unet_parallax_.IsLoaded() && cached_unet_ao_.IsLoaded() && + cached_unet_metallic_.IsLoaded() && cached_unet_roughness_.IsLoaded()) { + return true; + } + + Msg("[PBRPipeline] Loading Stage2 models for %ux%u", W, H); + + auto prof_5ch = TRT_ProfileWithFeatures("image", 5, H, W); + auto prof_12ch = TRT_ProfileWithFeatures("image", 12, H, W); + + char res_cache[512]; + FormatResCachePath(res_cache, sizeof(res_cache), trt_cache_path_.c_str(), W, H); + ResetAllCachedModels(); + + bool ok = true; + ok = ok && cached_unet_parallax_.LoadModel(unet_parallax_path_.c_str(), config_.use_gpu, res_cache, + prof_5ch.c_str(), prof_5ch.c_str(), prof_5ch.c_str()); + ok = ok && cached_unet_ao_.LoadModel(unet_ao_path_.c_str(), config_.use_gpu, res_cache, + prof_5ch.c_str(), prof_5ch.c_str(), prof_5ch.c_str()); + ok = ok && cached_unet_metallic_.LoadModel(unet_metallic_path_.c_str(), config_.use_gpu, res_cache, + prof_12ch.c_str(), prof_12ch.c_str(), prof_12ch.c_str()); + ok = ok && cached_unet_roughness_.LoadModel(unet_roughness_path_.c_str(), config_.use_gpu, res_cache, + prof_12ch.c_str(), prof_12ch.c_str(), prof_12ch.c_str()); + + if (ok) { + cached_H_ = H; + cached_W_ = W; + loaded_stage_ = LoadedStage::Stage2; + } else { + Msg("! [PBRPipeline] Failed to load Stage2 models for %ux%u", W, H); + ResetAllCachedModels(); + } + + return ok; +} + +bool PBRPipeline::EnsureAllModelsLoaded(u32 H, u32 W) { + if (cached_H_ == H && cached_W_ == W && loaded_stage_ == LoadedStage::All && + cached_unet_albedo_uncond_.IsLoaded() && cached_segformer_.IsLoaded() && + cached_unet_albedo_.IsLoaded() && cached_unet_parallax_.IsLoaded() && + cached_unet_ao_.IsLoaded() && cached_unet_metallic_.IsLoaded() && + cached_unet_roughness_.IsLoaded()) { + return true; + } + + Msg("[PBRPipeline] Loading all models for %ux%u", W, H); + + auto prof_uncond = TRT_ProfileSingle("image", 6, H, W); + auto prof_seg = TRT_ProfileSingle("input", 6, H, W); + auto prof_albedo = TRT_ProfileWithFeatures("image", 6, H, W); + auto prof_5ch = TRT_ProfileWithFeatures("image", 5, H, W); + auto prof_12ch = TRT_ProfileWithFeatures("image", 12, H, W); + + char res_cache[512]; + FormatResCachePath(res_cache, sizeof(res_cache), trt_cache_path_.c_str(), W, H); + ResetAllCachedModels(); + + bool ok = true; + ok = ok && cached_unet_albedo_uncond_.LoadModel(unet_albedo_uncond_path_.c_str(), config_.use_gpu, res_cache, + prof_uncond.c_str(), prof_uncond.c_str(), prof_uncond.c_str()); + ok = ok && cached_segformer_.LoadModel(segformer_path_.c_str(), config_.use_gpu, res_cache, + prof_seg.c_str(), prof_seg.c_str(), prof_seg.c_str()); + ok = ok && cached_unet_albedo_.LoadModel(unet_albedo_path_.c_str(), config_.use_gpu, res_cache, + prof_albedo.c_str(), prof_albedo.c_str(), prof_albedo.c_str()); + ok = ok && cached_unet_parallax_.LoadModel(unet_parallax_path_.c_str(), config_.use_gpu, res_cache, + prof_5ch.c_str(), prof_5ch.c_str(), prof_5ch.c_str()); + ok = ok && cached_unet_ao_.LoadModel(unet_ao_path_.c_str(), config_.use_gpu, res_cache, + prof_5ch.c_str(), prof_5ch.c_str(), prof_5ch.c_str()); + ok = ok && cached_unet_metallic_.LoadModel(unet_metallic_path_.c_str(), config_.use_gpu, res_cache, + prof_12ch.c_str(), prof_12ch.c_str(), prof_12ch.c_str()); + ok = ok && cached_unet_roughness_.LoadModel(unet_roughness_path_.c_str(), config_.use_gpu, res_cache, + prof_12ch.c_str(), prof_12ch.c_str(), prof_12ch.c_str()); + + if (ok) { + cached_H_ = H; + cached_W_ = W; + loaded_stage_ = LoadedStage::All; + } else { + Msg("! [PBRPipeline] Failed to load models for %ux%u", W, H); + ResetAllCachedModels(); + } + + return ok; +} + +PBRPipeline::Stage1Outputs PBRPipeline::RunStage1(const Tensor& diffuse, const Tensor& normal) { + Stage1Outputs result; + + const u32 H = diffuse.height(); + const u32 W = diffuse.width(); + + if (config_.verbose) { + Msg("[PBRPipeline] Stage1: Processing %ux%u texture (Two-Pass Albedo)", W, H); + } + + if (!cached_unet_albedo_uncond_.IsLoaded() || !cached_segformer_.IsLoaded() || !cached_unet_albedo_.IsLoaded()) { + Msg("! [PBRPipeline] Stage1: Models not loaded for %ux%u", W, H); + return result; + } + + // ═══════════════════════════════════════════════════════ + // STEP 1: Normalize diffuse and normal with STANDARD normalization + // ═══════════════════════════════════════════════════════ + // CRITICAL: UNets expect STANDARD normalization (0.5, 0.5, 0.5) + // SegFormer expects DEFAULT normalization (0.485, 0.456, 0.406) + Tensor diffuse_std = Tensor::Create(1, 3, H, W); + Tensor normal_std = Tensor::Create(1, 3, H, W); + + // Copy diffuse and normal (to avoid modifying originals) + for (u32 c = 0; c < 3; ++c) { + for (u32 i = 0; i < H * W; ++i) { + diffuse_std.data[c * H * W + i] = diffuse.data[c * H * W + i]; + normal_std.data[c * H * W + i] = normal.data[c * H * W + i]; + } + } + + // Normalize with STANDARD stats for UNets + PreprocessingUtils::NormalizeImageNetStandard(diffuse_std); + PreprocessingUtils::NormalizeImageNetStandard(normal_std); + + // ═══════════════════════════════════════════════════════ + // STEP 2: Concatenate STANDARD-normalized tensors → 6ch input for UNets + // ═══════════════════════════════════════════════════════ + Tensor input_6ch_std = Tensor::Create(1, 6, H, W); + + // Copy normalized diffuse → channels 0-2 + for (u32 c = 0; c < 3; ++c) { + for (u32 i = 0; i < H * W; ++i) { + input_6ch_std.data[c * H * W + i] = diffuse_std.data[c * H * W + i]; + } + } + + // Copy normalized normal → channels 3-5 + for (u32 c = 0; c < 3; ++c) { + for (u32 i = 0; i < H * W; ++i) { + input_6ch_std.data[(c + 3) * H * W + i] = normal_std.data[c * H * W + i]; + } + } + + // ═══════════════════════════════════════════════════════ + // STEP 3: FIRST PASS - Run UNet Albedo UNCOND (no features) → "dirty" albedo + // ═══════════════════════════════════════════════════════ + xr_vector uncond_inputs = { input_6ch_std }; + xr_vector uncond_input_names = { "image" }; + xr_vector uncond_output_names = { "albedo" }; + + xr_vector uncond_outputs = cached_unet_albedo_uncond_.Run(uncond_inputs, uncond_input_names, uncond_output_names); + + if (uncond_outputs.empty()) { + Msg("! [PBRPipeline] Stage1: UNet Albedo Uncond failed"); + return result; + } + + Tensor dirty_albedo = std::move(uncond_outputs[0]); + + if (config_.verbose) { + Msg("[PBRPipeline] First pass (uncond): Dirty albedo [%u,%u,%u,%u]", + dirty_albedo.batch(), dirty_albedo.channels(), + dirty_albedo.height(), dirty_albedo.width()); + } + + // ═══════════════════════════════════════════════════════ + // STEP 4: Re-normalize dirty albedo to DEFAULT for SegFormer + // ═══════════════════════════════════════════════════════ + + // Normalize with DEFAULT stats for SegFormer + PreprocessingUtils::NormalizeImageNetDefault(dirty_albedo); + + // ═══════════════════════════════════════════════════════ + // STEP 5: Create 6ch input for SegFormer (DEFAULT-normalized albedo + STANDARD-normalized normal) + // ═══════════════════════════════════════════════════════ + Tensor segformer_input_6ch = Tensor::Create(1, 6, H, W); + + // Copy DEFAULT-normalized dirty albedo → channels 0-2 + for (u32 c = 0; c < 3; ++c) { + for (u32 i = 0; i < H * W; ++i) { + segformer_input_6ch.data[c * H * W + i] = dirty_albedo.data[c * H * W + i]; + } + } + + // Copy STANDARD-normalized normal → channels 3-5 + for (u32 c = 0; c < 3; ++c) { + for (u32 i = 0; i < H * W; ++i) { + segformer_input_6ch.data[(c + 3) * H * W + i] = normal_std.data[c * H * W + i]; + } + } + + // ═══════════════════════════════════════════════════════ + // STEP 6: Run SegFormer → logits + features (load on-demand) + // ═══════════════════════════════════════════════════════ + xr_vector seg_inputs = { segformer_input_6ch }; + xr_vector seg_input_names = { "input" }; + xr_vector seg_output_names = { "logits", "features" }; + + xr_vector seg_outputs = cached_segformer_.Run(seg_inputs, seg_input_names, seg_output_names); + + if (seg_outputs.size() != 2) { + Msg("! [PBRPipeline] Stage1: SegFormer returned %zu outputs, expected 2", seg_outputs.size()); + return result; + } + + result.material_logits = std::move(seg_outputs[0]); + result.seg_features = std::move(seg_outputs[1]); + + if (config_.verbose) { + Msg("[PBRPipeline] SegFormer output: logits [%u,%u,%u,%u], features [%u,%u,%u,%u]", + result.material_logits.batch(), result.material_logits.channels(), + result.material_logits.height(), result.material_logits.width(), + result.seg_features.batch(), result.seg_features.channels(), + result.seg_features.height(), result.seg_features.width()); + } + + // ═══════════════════════════════════════════════════════ + // STEP 7: SECOND PASS - Run UNet Albedo with features → final albedo (load on-demand) + // ═══════════════════════════════════════════════════════ + xr_vector albedo_inputs = { input_6ch_std, result.seg_features }; + xr_vector albedo_input_names = { "image", "seg_features" }; + xr_vector albedo_output_names = { "albedo" }; + + xr_vector albedo_outputs = cached_unet_albedo_.Run(albedo_inputs, albedo_input_names, albedo_output_names); + + if (albedo_outputs.empty()) { + Msg("! [PBRPipeline] Stage1: UNet Albedo failed"); + return result; + } + + result.albedo = std::move(albedo_outputs[0]); + + if (config_.verbose) { + Msg("[PBRPipeline] Second pass (cond): Final albedo [%u,%u,%u,%u]", + result.albedo.batch(), result.albedo.channels(), + result.albedo.height(), result.albedo.width()); + } + + result.success = true; + return result; +} + +struct Stage2Inputs { + Tensor input_5ch; + Tensor input_12ch; +}; + +static Stage2Inputs PrepareStage2Inputs( + const Tensor& albedo, const Tensor& normal, + const Tensor& material_logits, u32 H, u32 W, bool verbose) +{ + Stage2Inputs result; + + Tensor albedo_std = Tensor::Create(1, 3, H, W); + Tensor albedo_default = Tensor::Create(1, 3, H, W); + Tensor normal_std = Tensor::Create(1, 3, H, W); + + for (u32 c = 0; c < 3; ++c) { + for (u32 i = 0; i < H * W; ++i) { + albedo_std.data[c * H * W + i] = albedo.data[c * H * W + i]; + albedo_default.data[c * H * W + i] = albedo.data[c * H * W + i]; + normal_std.data[c * H * W + i] = normal.data[c * H * W + i]; + } + } + + PreprocessingUtils::NormalizeImageNetStandard(albedo_std); + PreprocessingUtils::NormalizeImageNetDefault(albedo_default); + PreprocessingUtils::NormalizeImageNetStandard(normal_std); + + Tensor curvature = PreprocessingUtils::ComputeMeanCurvature(normal_std); + Tensor poisson = PreprocessingUtils::ComputePoissonCoarse(normal_std); + + constexpr u32 NUM_CLASSES = 6; + Tensor material_mask = PreprocessingUtils::LogitsToMask(material_logits, NUM_CLASSES); + material_mask = PreprocessingUtils::Upsample(material_mask, H, W); + + if (verbose) { + Msg("[PBRPipeline] Preprocessing: curvature [%u,%u,%u,%u], poisson [%u,%u,%u,%u], mask [%u,%u,%u,%u]", + curvature.batch(), curvature.channels(), curvature.height(), curvature.width(), + poisson.batch(), poisson.channels(), poisson.height(), poisson.width(), + material_mask.batch(), material_mask.channels(), material_mask.height(), material_mask.width()); + } + + result.input_5ch = Tensor::Create(1, 5, H, W); + for (u32 c = 0; c < 3; ++c) { + for (u32 i = 0; i < H * W; ++i) { + result.input_5ch.data[c * H * W + i] = normal_std.data[c * H * W + i]; + } + } + for (u32 i = 0; i < H * W; ++i) { + result.input_5ch.data[3 * H * W + i] = curvature.data[i]; + result.input_5ch.data[4 * H * W + i] = poisson.data[i]; + } + + result.input_12ch = Tensor::Create(1, 12, H, W); + for (u32 c = 0; c < 3; ++c) { + for (u32 i = 0; i < H * W; ++i) { + result.input_12ch.data[c * H * W + i] = albedo_default.data[c * H * W + i]; + } + } + for (u32 c = 0; c < 3; ++c) { + for (u32 i = 0; i < H * W; ++i) { + result.input_12ch.data[(c + 3) * H * W + i] = normal_std.data[c * H * W + i]; + } + } + for (u32 c = 0; c < NUM_CLASSES; ++c) { + for (u32 i = 0; i < H * W; ++i) { + result.input_12ch.data[(c + 6) * H * W + i] = material_mask.data[c * H * W + i]; + } + } + + return result; +} + +PBRPipeline::Stage2Outputs PBRPipeline::RunStage2( + const Tensor& albedo, + const Tensor& normal, + const Tensor& material_logits, + const Tensor& seg_features) +{ + Stage2Outputs result; + + const u32 H = normal.height(); + const u32 W = normal.width(); + + if (!cached_unet_parallax_.IsLoaded() || !cached_unet_ao_.IsLoaded() || + !cached_unet_metallic_.IsLoaded() || !cached_unet_roughness_.IsLoaded()) { + Msg("! [PBRPipeline] Stage2: Models not loaded for %ux%u", W, H); + return result; + } + + const bool sequential = (loaded_stage_ == LoadedStage::Stage2); + + auto prepared = PrepareStage2Inputs(albedo, normal, material_logits, H, W, config_.verbose); + + xr_vector parallax_outputs, ao_outputs, metallic_outputs, roughness_outputs; + bool parallax_ok = false, ao_ok = false, metallic_ok = false, roughness_ok = false; + + if (sequential) { + { + xr_vector inputs = { prepared.input_5ch, seg_features }; + xr_vector in_names = { "image", "seg_features" }; + xr_vector out_names = { "output" }; + parallax_outputs = cached_unet_parallax_.Run(inputs, in_names, out_names); + parallax_ok = !parallax_outputs.empty(); + } + + { + xr_vector inputs = { prepared.input_5ch, seg_features }; + xr_vector in_names = { "image", "seg_features" }; + xr_vector out_names = { "output" }; + ao_outputs = cached_unet_ao_.Run(inputs, in_names, out_names); + ao_ok = !ao_outputs.empty(); + } + + { + xr_vector inputs = { prepared.input_12ch, seg_features }; + xr_vector in_names = { "image", "seg_features" }; + xr_vector out_names = { "output" }; + metallic_outputs = cached_unet_metallic_.Run(inputs, in_names, out_names); + metallic_ok = !metallic_outputs.empty(); + } + + { + xr_vector inputs = { prepared.input_12ch, seg_features }; + xr_vector in_names = { "image", "seg_features" }; + xr_vector out_names = { "output" }; + roughness_outputs = cached_unet_roughness_.Run(inputs, in_names, out_names); + roughness_ok = !roughness_outputs.empty(); + } + } else { + std::thread ao_thread([&]() { + xr_vector inputs = { prepared.input_5ch, seg_features }; + xr_vector in_names = { "image", "seg_features" }; + xr_vector out_names = { "output" }; + ao_outputs = cached_unet_ao_.Run(inputs, in_names, out_names); + ao_ok = !ao_outputs.empty(); + }); + + std::thread metallic_thread([&]() { + xr_vector inputs = { prepared.input_12ch, seg_features }; + xr_vector in_names = { "image", "seg_features" }; + xr_vector out_names = { "output" }; + metallic_outputs = cached_unet_metallic_.Run(inputs, in_names, out_names); + metallic_ok = !metallic_outputs.empty(); + }); + + std::thread roughness_thread([&]() { + xr_vector inputs = { prepared.input_12ch, seg_features }; + xr_vector in_names = { "image", "seg_features" }; + xr_vector out_names = { "output" }; + roughness_outputs = cached_unet_roughness_.Run(inputs, in_names, out_names); + roughness_ok = !roughness_outputs.empty(); + }); + + { + xr_vector inputs = { prepared.input_5ch, seg_features }; + xr_vector in_names = { "image", "seg_features" }; + xr_vector out_names = { "output" }; + parallax_outputs = cached_unet_parallax_.Run(inputs, in_names, out_names); + parallax_ok = !parallax_outputs.empty(); + } + + ao_thread.join(); + metallic_thread.join(); + roughness_thread.join(); + } + + if (!parallax_ok) { Msg("! [PBRPipeline] Stage2: UNet Parallax failed"); return result; } + result.parallax = std::move(parallax_outputs[0]); + if (!ao_ok) { Msg("! [PBRPipeline] Stage2: UNet AO failed"); return result; } + result.ao = std::move(ao_outputs[0]); + if (!metallic_ok) { Msg("! [PBRPipeline] Stage2: UNet Metallic failed"); return result; } + result.metallic = std::move(metallic_outputs[0]); + if (!roughness_ok) { Msg("! [PBRPipeline] Stage2: UNet Roughness failed"); return result; } + result.roughness = std::move(roughness_outputs[0]); + + if (config_.verbose) { + Msg("[PBRPipeline] Stage2 complete: parallax [%u,%u,%u,%u], AO [%u,%u,%u,%u], metallic [%u,%u,%u,%u], roughness [%u,%u,%u,%u]", + result.parallax.batch(), result.parallax.channels(), result.parallax.height(), result.parallax.width(), + result.ao.batch(), result.ao.channels(), result.ao.height(), result.ao.width(), + result.metallic.batch(), result.metallic.channels(), result.metallic.height(), result.metallic.width(), + result.roughness.batch(), result.roughness.channels(), result.roughness.height(), result.roughness.width()); + } + + result.success = true; + return result; +} + +bool PBRPipeline::NeedsSingleModelProcessing(u32 width, u32 height) const { + return (static_cast(width) * height) > VRAM_SINGLE_MODEL_THRESHOLD; +} + +PBRPipeline::Stage2Outputs PBRPipeline::RunStage2SingleModel( + const Tensor& albedo, + const Tensor& normal, + const Tensor& material_logits, + const Tensor& seg_features) +{ + Stage2Outputs result; + + const u32 H = normal.height(); + const u32 W = normal.width(); + + Msg("[PBRPipeline] Stage2 single-model mode for %ux%u", W, H); + + auto prepared = PrepareStage2Inputs(albedo, normal, material_logits, H, W, config_.verbose); + + char res_cache[512]; + FormatResCachePath(res_cache, sizeof(res_cache), trt_cache_path_.c_str(), W, H); + + auto prof_5ch = TRT_ProfileWithFeatures("image", 5, H, W); + auto prof_12ch = TRT_ProfileWithFeatures("image", 12, H, W); + + ResetAllCachedModels(); + + auto run_single_model = [&](const char* name, const xr_string& model_path, + const std::string& profile, const Tensor& input, + const Tensor& features) -> Tensor + { + Msg("[PBRPipeline] Stage2 single-model: loading %s @ %ux%u", name, W, H); + + ONNXModelRunner runner; + if (!runner.LoadModel(model_path.c_str(), config_.use_gpu, res_cache, + profile.c_str(), profile.c_str(), profile.c_str())) { + Msg("! [PBRPipeline] Stage2 single-model: failed to load %s", name); + return {}; + } + + xr_vector inputs = { input, features }; + xr_vector in_names = { "image", "seg_features" }; + xr_vector out_names = { "output" }; + auto outputs = runner.Run(inputs, in_names, out_names); + + if (outputs.empty()) { + Msg("! [PBRPipeline] Stage2 single-model: inference failed for %s", name); + return {}; + } + + Msg("[PBRPipeline] Stage2 single-model: %s complete", name); + return std::move(outputs[0]); + }; + + result.parallax = run_single_model("unet_parallax", unet_parallax_path_, prof_5ch, prepared.input_5ch, seg_features); + if (result.parallax.data.empty()) { Msg("! [PBRPipeline] Stage2: UNet Parallax failed (single-model)"); return result; } + + result.ao = run_single_model("unet_ao", unet_ao_path_, prof_5ch, prepared.input_5ch, seg_features); + if (result.ao.data.empty()) { Msg("! [PBRPipeline] Stage2: UNet AO failed (single-model)"); return result; } + + result.metallic = run_single_model("unet_metallic", unet_metallic_path_, prof_12ch, prepared.input_12ch, seg_features); + if (result.metallic.data.empty()) { Msg("! [PBRPipeline] Stage2: UNet Metallic failed (single-model)"); return result; } + + result.roughness = run_single_model("unet_roughness", unet_roughness_path_, prof_12ch, prepared.input_12ch, seg_features); + if (result.roughness.data.empty()) { Msg("! [PBRPipeline] Stage2: UNet Roughness failed (single-model)"); return result; } + + if (config_.verbose) { + Msg("[PBRPipeline] Stage2 single-model complete: parallax [%u,%u,%u,%u], AO [%u,%u,%u,%u], metallic [%u,%u,%u,%u], roughness [%u,%u,%u,%u]", + result.parallax.batch(), result.parallax.channels(), result.parallax.height(), result.parallax.width(), + result.ao.batch(), result.ao.channels(), result.ao.height(), result.ao.width(), + result.metallic.batch(), result.metallic.channels(), result.metallic.height(), result.metallic.width(), + result.roughness.batch(), result.roughness.channels(), result.roughness.height(), result.roughness.width()); + } + + result.success = true; + return result; +} + +struct PreparedInputTensors { + Tensor diffuse; + Tensor normal; + Tensor diffuse_rgba; +}; + +static PreparedInputTensors PrepareInputTensors(const u8* diffuse_data, const u8* normal_data, u32 width, u32 height) { + PreparedInputTensors result; + result.diffuse_rgba = Tensor::FromImageData(diffuse_data, width, height, 4); + Tensor normal_rgba = Tensor::FromImageData(normal_data, width, height, 4); + + const u32 H = result.diffuse_rgba.height(); + const u32 W = result.diffuse_rgba.width(); + + result.diffuse = Tensor::Create(1, 3, H, W); + result.normal = Tensor::Create(1, 3, H, W); + + for (u32 c = 0; c < 3; ++c) { + for (u32 i = 0; i < H * W; ++i) { + result.diffuse.data[c * H * W + i] = result.diffuse_rgba.data[c * H * W + i]; + } + } + + for (u32 i = 0; i < H * W; ++i) { + result.normal.data[0 * H * W + i] = normal_rgba.data[3 * H * W + i]; + result.normal.data[1 * H * W + i] = normal_rgba.data[2 * H * W + i]; + result.normal.data[2 * H * W + i] = normal_rgba.data[1 * H * W + i]; + } + + return result; +} + +PBRPipelineOutputs PBRPipeline::Process(const u8* diffuse, const u8* normal, u32 width, u32 height) { + PBRPipelineOutputs outputs; + + if (!initialized_) { + Msg("! [PBRPipeline] Pipeline not initialized"); + return outputs; + } + + if (width < MIN_AI_RESOLUTION || height < MIN_AI_RESOLUTION) { + Msg("[PBRPipeline] %ux%u below minimum %u, skipping AI conversion", width, height, MIN_AI_RESOLUTION); + return outputs; + } + + auto tensors = PrepareInputTensors(diffuse, normal, width, height); + + const u32 H = tensors.diffuse.height(); + const u32 W = tensors.diffuse.width(); + + if (!EnsureAllModelsLoaded(H, W)) { + Msg("! [PBRPipeline] Process: Failed to load models for %ux%u", W, H); + return outputs; + } + + auto t_stage1_start = std::chrono::high_resolution_clock::now(); + auto stage1 = RunStage1(tensors.diffuse, tensors.normal); + auto t_stage1_end = std::chrono::high_resolution_clock::now(); + if (!stage1.success) { + return outputs; + } + + auto t_stage2_start = std::chrono::high_resolution_clock::now(); + auto stage2 = RunStage2(stage1.albedo, tensors.normal, stage1.material_logits, stage1.seg_features); + auto t_stage2_end = std::chrono::high_resolution_clock::now(); + if (!stage2.success) { + return outputs; + } + + auto stage1_ms = std::chrono::duration_cast(t_stage1_end - t_stage1_start).count(); + auto stage2_ms = std::chrono::duration_cast(t_stage2_end - t_stage2_start).count(); + Msg("[PBRPipeline] %ux%u inference: Stage1 %lldms, Stage2 %lldms, total %lldms", + W, H, stage1_ms, stage2_ms, stage1_ms + stage2_ms); + + outputs.albedo = std::move(stage1.albedo); + outputs.metallic = std::move(stage2.metallic); + outputs.roughness = std::move(stage2.roughness); + outputs.ao = std::move(stage2.ao); + outputs.parallax = std::move(stage2.parallax); + outputs.normal = std::move(tensors.normal); + outputs.success = true; + + return outputs; +} + +void PBRPipeline::WarmupTRTEngines(const xr_vector>& dimensions) { + if (!initialized_) return; + if (dimensions.empty()) return; +#ifdef XR_PLATFORM_APPLE + Msg("[PBRPipeline] TRT warmup skipped (CoreML/CPU provider)"); + return; +#endif + + struct ModelInfo { + const char* name; + const xr_string* path; + int profile_type; + }; + + const ModelInfo models[] = { + { "unet_albedo_uncond", &unet_albedo_uncond_path_, 0 }, + { "segformer", &segformer_path_, 1 }, + { "unet_albedo", &unet_albedo_path_, 2 }, + { "unet_parallax", &unet_parallax_path_, 3 }, + { "unet_ao", &unet_ao_path_, 3 }, + { "unet_metallic", &unet_metallic_path_, 4 }, + { "unet_roughness", &unet_roughness_path_, 4 }, + }; + + Msg("[PBRPipeline] Warming up TRT engines for %zu resolutions, 7 models each...", dimensions.size()); + + for (const auto& [W, H] : dimensions) { + if (W < MIN_AI_RESOLUTION || H < MIN_AI_RESOLUTION) { + Msg("[PBRPipeline] Warmup: %ux%u below minimum %u, skipping", W, H, MIN_AI_RESOLUTION); + continue; + } + + char res_cache[512]; + FormatResCachePath(res_cache, sizeof(res_cache), trt_cache_path_.c_str(), W, H); + + auto prof_uncond = TRT_ProfileSingle("image", 6, H, W); + auto prof_seg = TRT_ProfileSingle("input", 6, H, W); + auto prof_albedo = TRT_ProfileWithFeatures("image", 6, H, W); + auto prof_5ch = TRT_ProfileWithFeatures("image", 5, H, W); + auto prof_12ch = TRT_ProfileWithFeatures("image", 12, H, W); + + u32 cached_engine_count = 0; + try { + for (const auto& entry : std::filesystem::directory_iterator(res_cache)) { + if (entry.path().extension() == ".engine") + ++cached_engine_count; + } + } catch (const std::filesystem::filesystem_error&) { + } + + if (cached_engine_count >= 7) { + Msg("[PBRPipeline] Warmup: %ux%u already cached (%u .engine files), skipping", W, H, cached_engine_count); + continue; + } + + Msg("[PBRPipeline] Warmup: %ux%u has %u/7 cached engines, building missing...", W, H, cached_engine_count); + + for (const auto& model : models) { + const char* prof = nullptr; + switch (model.profile_type) { + case 0: prof = prof_uncond.c_str(); break; + case 1: prof = prof_seg.c_str(); break; + case 2: prof = prof_albedo.c_str(); break; + case 3: prof = prof_5ch.c_str(); break; + case 4: prof = prof_12ch.c_str(); break; + } + + Msg("[PBRPipeline] Warmup: %s @ %ux%u", model.name, W, H); + + ONNXModelRunner runner; + if (!runner.LoadModel(model.path->c_str(), config_.use_gpu, res_cache, + prof, prof, prof)) { + Msg("! [PBRPipeline] Warmup failed: %s @ %ux%u", model.name, W, H); + } + } + + Msg("[PBRPipeline] Warmup complete for %ux%u", W, H); + } + + ResetAllCachedModels(); + + Msg("[PBRPipeline] TRT engine warmup complete for all resolutions"); +} + +bool PBRPipeline::NeedsSplitProcessing(u32 width, u32 height) const { + return (static_cast(width) * height) > VRAM_SPLIT_THRESHOLD; +} + +PBRPipeline::Stage1Result PBRPipeline::ProcessStage1(const u8* diffuse, const u8* normal, u32 width, u32 height) { + Stage1Result result; + + if (!initialized_) { + Msg("! [PBRPipeline] Pipeline not initialized"); + return result; + } + + if (width < MIN_AI_RESOLUTION || height < MIN_AI_RESOLUTION) { + Msg("[PBRPipeline] %ux%u below minimum %u, skipping AI conversion", width, height, MIN_AI_RESOLUTION); + return result; + } + + auto tensors = PrepareInputTensors(diffuse, normal, width, height); + + const u32 H = tensors.diffuse.height(); + const u32 W = tensors.diffuse.width(); + + if (!EnsureStage1ModelsLoaded(H, W)) { + Msg("! [PBRPipeline] ProcessStage1: Failed to load Stage1 models for %ux%u", W, H); + return result; + } + + auto stage1 = RunStage1(tensors.diffuse, tensors.normal); + if (!stage1.success) + return result; + + result.albedo = std::move(stage1.albedo); + result.normal = std::move(tensors.normal); + result.material_logits = std::move(stage1.material_logits); + result.seg_features = std::move(stage1.seg_features); + result.diffuse_rgba_original = std::move(tensors.diffuse_rgba); + result.success = true; + return result; +} + +PBRPipelineOutputs PBRPipeline::ProcessStage2(Stage1Result& stage1) { + PBRPipelineOutputs outputs; + + if (!stage1.success) { + Msg("! [PBRPipeline] ProcessStage2: Stage1 result is invalid"); + return outputs; + } + + const u32 H = stage1.normal.height(); + const u32 W = stage1.normal.width(); + + Stage2Outputs stage2; + + if (NeedsSingleModelProcessing(W, H)) { + stage2 = RunStage2SingleModel(stage1.albedo, stage1.normal, stage1.material_logits, stage1.seg_features); + } else { + if (!EnsureStage2ModelsLoaded(H, W)) { + Msg("! [PBRPipeline] ProcessStage2: Failed to load Stage2 models for %ux%u", W, H); + return outputs; + } + stage2 = RunStage2(stage1.albedo, stage1.normal, stage1.material_logits, stage1.seg_features); + } + + if (!stage2.success) + return outputs; + + outputs.albedo = std::move(stage1.albedo); + outputs.metallic = std::move(stage2.metallic); + outputs.roughness = std::move(stage2.roughness); + outputs.ao = std::move(stage2.ao); + outputs.parallax = std::move(stage2.parallax); + outputs.normal = std::move(stage1.normal); + outputs.success = true; + return outputs; +} + +// ══════════════════════════════════════════════════════════ +// HELPER FUNCTIONS +// ══════════════════════════════════════════════════════════ + +bool AreAIModelsAvailable(const char* model_dir) { + // Check if all required models exist + const char* required_models[] = { + "segformer.onnx", + "unet_albedo.onnx", + "unet_albedo_uncond.onnx", + "unet_parallax.onnx", + "unet_ao.onnx", + "unet_metallic.onnx", + "unet_roughness.onnx" + }; + + for (const auto& model : required_models) { + // Build relative path + xr_string relative_path = model_dir; + relative_path.append("/"); + relative_path.append(model); + + // Resolve using VFS + string_path full_path; + FS.update_path(full_path, "$game_data$", relative_path.c_str()); + + if (!FS.exist(full_path)) { + return false; + } + } + + return true; +} +} + +#else // !USE_AI_PBR + +// Stub implementations when ONNX Runtime is not available +namespace xray::render::pbr { + +bool AreAIModelsAvailable(const char* model_dir) { + return false; // AI models not available when ONNX Runtime is disabled +} + +} // namespace xray::render::pbr + +#endif // USE_AI_PBR diff --git a/src/Layers/xrRender/PBRConverter/ONNXModelRunner.h b/src/Layers/xrRender/PBRConverter/ONNXModelRunner.h new file mode 100644 index 00000000000..9a3425b4b25 --- /dev/null +++ b/src/Layers/xrRender/PBRConverter/ONNXModelRunner.h @@ -0,0 +1,304 @@ +// xrRender/PBRConverter/ONNXModelRunner.h +// ONNX Runtime wrapper for AI-based PBR texture generation +// +// PURPOSE: +// Provides C++ interface to run ONNX models exported from the ai-pbr-models Python project. +// Handles model loading, inference, and tensor management. +// +// ARCHITECTURE: +// - ONNXModelRunner: Generic ONNX model executor (single model) +// - PBRPipeline: Orchestrates 2-stage AI pipeline (SegFormer → UNets) +// +// MODELS USED: +// Stage 1: SegFormer (material classification) + UNetAlbedo (basecolor generation) +// Stage 2: 5x UNetSingleChannel (metallic, roughness, AO, parallax, normal) + +#pragma once + +#include "xrCore/xrCore.h" +#include +#include + +// ONNX Runtime headers (required for std::unique_ptr) +#ifdef USE_AI_PBR +#include +#else +// Forward declarations when ONNX Runtime not available +namespace Ort { + class Env; + class Session; + class MemoryInfo; + struct Value; +} +#endif + +namespace xray::render::pbr { + +// ══════════════════════════════════════════════════════════ +// TENSOR DATA (Input/Output for ONNX models) +// ══════════════════════════════════════════════════════════ + +struct Tensor { + std::vector data; // Flattened tensor data (NCHW format) + std::vector shape; // [batch, channels, height, width] + + u32 batch() const { return shape.size() > 0 ? static_cast(shape[0]) : 1; } + u32 channels() const { return shape.size() > 1 ? static_cast(shape[1]) : 0; } + u32 height() const { return shape.size() > 2 ? static_cast(shape[2]) : 0; } + u32 width() const { return shape.size() > 3 ? static_cast(shape[3]) : 0; } + u32 size() const { return static_cast(data.size()); } + + // Create tensor with given shape (allocated but uninitialized) + static Tensor Create(u32 batch, u32 channels, u32 height, u32 width); + + // Create tensor from image data (RGB or single-channel) + static Tensor FromImageData(const u8* pixels, u32 width, u32 height, u32 channels); + + // Convert tensor to u8 image data (CHW → HWC format, float [0-1] → u8 [0-255]) + // apply_sigmoid: Apply sigmoid activation before conversion (matches Python torch.sigmoid()) + xr_vector ToImageData(bool apply_sigmoid = false) const; +}; + +// ══════════════════════════════════════════════════════════ +// ONNX MODEL RUNNER (Single Model Executor) +// ══════════════════════════════════════════════════════════ + +class ONNXModelRunner { +public: + ONNXModelRunner() = default; + ~ONNXModelRunner(); + + // Non-copyable (owns ONNX session resources) + ONNXModelRunner(const ONNXModelRunner&) = delete; + ONNXModelRunner& operator=(const ONNXModelRunner&) = delete; + + // Move-only + ONNXModelRunner(ONNXModelRunner&& other) noexcept; + ONNXModelRunner& operator=(ONNXModelRunner&& other) noexcept; + + // ═══════════════════════════════════════════════════════ + // INITIALIZATION + // ═══════════════════════════════════════════════════════ + + // Initialize ONNX Runtime environment (call once per application) + static bool InitializeEnvironment(); + static void ShutdownEnvironment(); + + // Load ONNX model from file + // model_path: Absolute path to .onnx file (e.g., "$game_data$/ai_models/segformer.onnx") + // use_gpu: Try to use DirectML (Windows) or CUDA (Linux) if available + // trt_cache_path: Path to TensorRT engine cache directory (optional, uses current dir if empty) + bool LoadModel(const char* model_path, bool use_gpu = true, const char* trt_cache_path = "", + const char* trt_profile_min = nullptr, const char* trt_profile_max = nullptr, const char* trt_profile_opt = nullptr); + + // Check if model is loaded and ready + bool IsLoaded() const; + + // Get model information + xr_string GetModelPath() const { return model_path_; } + xr_vector GetInputNames() const; + xr_vector GetOutputNames() const; + + // ═══════════════════════════════════════════════════════ + // INFERENCE + // ═══════════════════════════════════════════════════════ + + // Run inference with single input → single output + Tensor Run(const Tensor& input); + + // Run inference with multiple inputs → multiple outputs + // input_names/output_names must match model's expected names + xr_vector Run( + const xr_vector& inputs, + const xr_vector& input_names, + const xr_vector& output_names + ); + +private: + static std::unique_ptr env_; + static bool env_initialized_; + + std::unique_ptr session_; + std::unique_ptr memory_info_; + + xr_string model_path_; + bool use_gpu_ = false; + + // Cached model metadata + xr_vector input_names_; + xr_vector output_names_; + + // Helper: Convert Tensor → Ort::Value + Ort::Value TensorToOrtValue(const Tensor& tensor); + + // Helper: Convert Ort::Value → Tensor + Tensor OrtValueToTensor(Ort::Value& ort_value); +}; + +// ══════════════════════════════════════════════════════════ +// PREPROCESSING UTILITIES +// ══════════════════════════════════════════════════════════ + +class PreprocessingUtils { +public: + // ImageNet STANDARD normalization (for UNets) + // mean = [0.5, 0.5, 0.5], std = [0.5, 0.5, 0.5] + // Used by: UNetAlbedo, UNetSingleChannel models + static void NormalizeImageNetStandard(Tensor& tensor); + + // ImageNet DEFAULT normalization (for SegFormer) + // mean = [0.485, 0.456, 0.406], std = [0.229, 0.224, 0.225] + // Used by: SegFormer model (requires standard ImageNet stats) + static void NormalizeImageNetDefault(Tensor& tensor); + + // Legacy ImageNet normalization (defaults to DEFAULT for backwards compatibility) + static void NormalizeImageNet(Tensor& tensor); + + // Compute mean curvature map from normal map (3-channel → 1-channel) + // Uses Sobel gradients on normal channels + static Tensor ComputeMeanCurvature(const Tensor& normal_map); + + // Compute coarse height map from normal map via Poisson reconstruction + // Uses FFT-based integration (requires normal_x, normal_y, normal_z) + static Tensor ComputePoissonCoarse(const Tensor& normal_map); + + // Convert material logits → one-hot mask (for metallic/roughness conditioning) + // logits: [1, num_classes, H, W] → mask: [1, num_classes, H, W] (softmax + threshold) + static Tensor LogitsToMask(const Tensor& logits, u32 num_classes); + + // Upsample tensor to target size (bilinear interpolation) + static Tensor Upsample(const Tensor& input, u32 target_height, u32 target_width); +}; + +// ══════════════════════════════════════════════════════════ +// AI PBR PIPELINE (2-Stage Inference) +// ══════════════════════════════════════════════════════════ + +struct PBRPipelineConfig { + xr_string model_dir = "ai_models"; // Directory containing ONNX models (relative to $game_data$) + bool use_gpu = true; + bool verbose = false; // Print inference timing +}; + +struct PBRPipelineOutputs { + Tensor albedo; // [1, 3, H, W] - RGB basecolor + Tensor metallic; // [1, 1, H, W] - Single-channel metallic + Tensor roughness; // [1, 1, H, W] - Single-channel roughness + Tensor ao; // [1, 1, H, W] - Single-channel ambient occlusion + Tensor parallax; // [1, 1, H, W] - Single-channel height/parallax + Tensor normal; // [1, 3, H, W] - RGB normal (XYZ) + + bool success = false; +}; + +class PBRPipeline { +public: + PBRPipeline() = default; + ~PBRPipeline() = default; + + // ═══════════════════════════════════════════════════════ + // INITIALIZATION + // ═══════════════════════════════════════════════════════ + + // Load all models (must be called before Process) + // Returns false if any model fails to load + bool Initialize(const PBRPipelineConfig& config); + + // Check if pipeline is ready + bool IsInitialized() const { return initialized_; } + + // ═══════════════════════════════════════════════════════ + // INFERENCE + // ═══════════════════════════════════════════════════════ + + PBRPipelineOutputs Process(const u8* diffuse, const u8* normal, u32 width, u32 height); + + struct Stage1Result { + Tensor albedo; + Tensor normal; + Tensor material_logits; + Tensor seg_features; + Tensor diffuse_rgba_original; + bool success = false; + }; + + Stage1Result ProcessStage1(const u8* diffuse, const u8* normal, u32 width, u32 height); + PBRPipelineOutputs ProcessStage2(Stage1Result& stage1); + bool NeedsSplitProcessing(u32 width, u32 height) const; + void WarmupTRTEngines(const xr_vector>& dimensions); + + xr_string GetModelDir() const { return config_.model_dir; } + +private: + PBRPipelineConfig config_; + bool initialized_ = false; + + xr_string segformer_path_; + xr_string unet_albedo_path_; + xr_string unet_albedo_uncond_path_; + xr_string unet_parallax_path_; + xr_string unet_ao_path_; + xr_string unet_metallic_path_; + xr_string unet_roughness_path_; + xr_string trt_cache_path_; + + ONNXModelRunner cached_unet_albedo_uncond_; + ONNXModelRunner cached_segformer_; + ONNXModelRunner cached_unet_albedo_; + ONNXModelRunner cached_unet_parallax_; + ONNXModelRunner cached_unet_ao_; + ONNXModelRunner cached_unet_metallic_; + ONNXModelRunner cached_unet_roughness_; + u32 cached_H_ = 0; + u32 cached_W_ = 0; + + enum class LoadedStage { None, Stage1, Stage2, All }; + LoadedStage loaded_stage_ = LoadedStage::None; + + static constexpr u32 MIN_AI_RESOLUTION = 16; + static constexpr u32 VRAM_SPLIT_THRESHOLD = 512 * 1024; + static constexpr u32 VRAM_SINGLE_MODEL_THRESHOLD = 2048 * 1024; + + struct Stage1Outputs { + Tensor albedo; + Tensor material_logits; + Tensor seg_features; + bool success = false; + }; + + struct Stage2Outputs { + Tensor metallic; + Tensor roughness; + Tensor ao; + Tensor parallax; + bool success = false; + }; + + void ResetAllCachedModels(); + bool EnsureStage1ModelsLoaded(u32 H, u32 W); + bool EnsureStage2ModelsLoaded(u32 H, u32 W); + bool EnsureAllModelsLoaded(u32 H, u32 W); + + Stage1Outputs RunStage1(const Tensor& diffuse, const Tensor& normal); + Stage2Outputs RunStage2( + const Tensor& albedo, + const Tensor& normal, + const Tensor& material_logits, + const Tensor& seg_features + ); + Stage2Outputs RunStage2SingleModel( + const Tensor& albedo, + const Tensor& normal, + const Tensor& material_logits, + const Tensor& seg_features + ); + bool NeedsSingleModelProcessing(u32 width, u32 height) const; +}; + +// ══════════════════════════════════════════════════════════ +// HELPER: Check if AI models are available +// ══════════════════════════════════════════════════════════ + +bool AreAIModelsAvailable(const char* model_dir = "ai_models"); + +} // namespace xray::render::pbr diff --git a/src/Layers/xrRender/PBRConverter/PBRConversionUI.cpp b/src/Layers/xrRender/PBRConverter/PBRConversionUI.cpp new file mode 100644 index 00000000000..4e3893f3ed9 --- /dev/null +++ b/src/Layers/xrRender/PBRConverter/PBRConversionUI.cpp @@ -0,0 +1,169 @@ +#include "stdafx.h" +#include "PBRConversionUI.h" +#include "xrEngine/device.h" +#include +#include + +namespace xray::render::pbr { + +static constexpr float UI_LINGER_SECONDS = 6.0f; + +static double NowSeconds() +{ + using namespace std::chrono; + return duration(steady_clock::now().time_since_epoch()).count(); +} + +ConversionProgress& ConversionProgress::Get() +{ + static ConversionProgress instance; + return instance; +} + +void ConversionProgress::BeginJob() +{ + m_total.store(0, std::memory_order_relaxed); + m_converted.store(0, std::memory_order_relaxed); + m_skipped.store(0, std::memory_order_relaxed); + m_failed.store(0, std::memory_order_relaxed); + { + ScopeLock lock{ &m_string_lock }; + m_phase = "Starting..."; + m_current_item.clear(); + m_job_start_time = NowSeconds(); + m_job_end_time = -1.0; + } + m_job_active.store(true, std::memory_order_release); +} + +void ConversionProgress::EndJob() +{ + if (!m_job_active.load(std::memory_order_acquire)) + return; + { + ScopeLock lock{ &m_string_lock }; + m_phase = "Done"; + m_current_item.clear(); + m_job_end_time = NowSeconds(); + } + m_job_active.store(false, std::memory_order_release); +} + +void ConversionProgress::BeginPhase(pcstr phase_name, u32 total) +{ + m_total.store(total, std::memory_order_relaxed); + m_converted.store(0, std::memory_order_relaxed); + m_skipped.store(0, std::memory_order_relaxed); + m_failed.store(0, std::memory_order_relaxed); + + ScopeLock lock{ &m_string_lock }; + m_phase = phase_name ? phase_name : ""; + m_current_item.clear(); +} + +void ConversionProgress::SetCurrentItem(pcstr name) +{ + ScopeLock lock{ &m_string_lock }; + m_current_item = name ? name : ""; +} + +ConversionProgress::Snapshot ConversionProgress::GetSnapshot() +{ + Snapshot snap; + snap.job_active = m_job_active.load(std::memory_order_acquire); + snap.total = m_total.load(std::memory_order_relaxed); + snap.converted = m_converted.load(std::memory_order_relaxed); + snap.skipped = m_skipped.load(std::memory_order_relaxed); + snap.failed = m_failed.load(std::memory_order_relaxed); + + ScopeLock lock{ &m_string_lock }; + snap.phase = m_phase; + snap.current_item = m_current_item; + + const double now = NowSeconds(); + if (snap.job_active) + snap.job_elapsed_seconds = static_cast(now - m_job_start_time); + else if (m_job_end_time >= 0.0) + { + snap.job_elapsed_seconds = static_cast(m_job_end_time - m_job_start_time); + snap.seconds_since_job_end = static_cast(now - m_job_end_time); + } + + return snap; +} + +void RenderConversionProgressUI() +{ + auto snap = ConversionProgress::Get().GetSnapshot(); + + const bool lingering = !snap.job_active && + snap.seconds_since_job_end >= 0.f && + snap.seconds_since_job_end < UI_LINGER_SECONDS; + + if (!snap.job_active && !lingering) + return; + + ImGui::SetCurrentContext(Device.GetImGuiContext()); + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos( + ImVec2(viewport->WorkPos.x + viewport->WorkSize.x - 10.f, viewport->WorkPos.y + 10.f), + ImGuiCond_FirstUseEver, ImVec2(1.f, 0.f)); + ImGui::SetNextWindowSize(ImVec2(420.f, 0.f), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowBgAlpha(0.85f); + + const bool ideActive = Device.editor().IsActiveState(); + + ImGuiWindowFlags flags = ImGuiWindowFlags_None; + if (!ideActive) + flags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoFocusOnAppearing; + + if (!ImGui::Begin("PBR Texture Conversion", nullptr, flags)) + { + ImGui::End(); + return; + } + + ImGui::TextUnformatted(snap.phase.c_str()); + + const u32 done = snap.converted + snap.skipped + snap.failed; + + if (snap.total > 0) + { + const float fraction = static_cast(done) / static_cast(snap.total); + char overlay[64]; + xr_sprintf(overlay, sizeof(overlay), "%u / %u", done, snap.total); + ImGui::ProgressBar(fraction, ImVec2(-FLT_MIN, 0.f), overlay); + } + else if (snap.job_active) + { + ImGui::ProgressBar(-1.f * static_cast(ImGui::GetTime()), ImVec2(-FLT_MIN, 0.f), "..."); + } + + if (!snap.current_item.empty()) + ImGui::Text("Current: %s", snap.current_item.c_str()); + + ImGui::Text("Converted: %u Skipped: %u", snap.converted, snap.skipped); + if (snap.failed > 0) + { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.f, 0.4f, 0.4f, 1.f), " Failed: %u", snap.failed); + } + + const u32 elapsed = static_cast(snap.job_elapsed_seconds); + if (snap.job_active && snap.total > 0 && snap.converted > 0 && done > 0) + { + const float remaining = snap.job_elapsed_seconds / done * (snap.total - done); + ImGui::TextDisabled("Elapsed: %um %02us ETA: ~%um %02us", + elapsed / 60, elapsed % 60, + static_cast(remaining) / 60, static_cast(remaining) % 60); + } + else + { + ImGui::TextDisabled("Elapsed: %um %02us", elapsed / 60, elapsed % 60); + } + + ImGui::End(); +} + +} diff --git a/src/Layers/xrRender/PBRConverter/PBRConversionUI.h b/src/Layers/xrRender/PBRConverter/PBRConversionUI.h new file mode 100644 index 00000000000..008fadb74ae --- /dev/null +++ b/src/Layers/xrRender/PBRConverter/PBRConversionUI.h @@ -0,0 +1,56 @@ +#pragma once + +#include "xrCore/xrCore.h" +#include "xrCore/Threading/Lock.hpp" +#include + +namespace xray::render::pbr { + +class ConversionProgress +{ +public: + static ConversionProgress& Get(); + + void BeginJob(); + void EndJob(); + + void BeginPhase(pcstr phase_name, u32 total); + + void SetCurrentItem(pcstr name); + + void OnConverted() { m_converted.fetch_add(1, std::memory_order_relaxed); } + void OnSkipped() { m_skipped.fetch_add(1, std::memory_order_relaxed); } + void OnFailed() { m_failed.fetch_add(1, std::memory_order_relaxed); } + + struct Snapshot + { + bool job_active = false; + float seconds_since_job_end = -1.f; + float job_elapsed_seconds = 0.f; + xr_string phase; + xr_string current_item; + u32 total = 0; + u32 converted = 0; + u32 skipped = 0; + u32 failed = 0; + }; + Snapshot GetSnapshot(); + +private: + std::atomic m_job_active{ false }; + std::atomic m_total{ 0 }; + std::atomic m_converted{ 0 }; + std::atomic m_skipped{ 0 }; + std::atomic m_failed{ 0 }; + + double m_job_start_time = 0.0; + double m_job_end_time = -1.0; + + Lock m_string_lock; + xr_string m_phase; + xr_string m_current_item; +}; + +void RenderConversionProgressUI(); + +} diff --git a/src/Layers/xrRender/PBRConverter/PBRTextureConverter.cpp b/src/Layers/xrRender/PBRConverter/PBRTextureConverter.cpp new file mode 100644 index 00000000000..358bc149ea9 --- /dev/null +++ b/src/Layers/xrRender/PBRConverter/PBRTextureConverter.cpp @@ -0,0 +1,2014 @@ +// xrRender/PBR/PBRTextureConverter.cpp +// Phase 2.5: PBR Texture Conversion Implementation + +#include "stdafx.h" +#include "PBRTextureConverter.h" +#include "PBRConversionUI.h" +#include "ONNXModelRunner.h" // AI-based conversion +#include "xrCore/FS.h" +#include "xrCore/Threading/ParallelFor.hpp" +#include "Layers/xrRender/ResourceManager/DDSLoader.h" +#include "Layers/xrRender/ETextureParams.h" // For .thm file support +#include +#include +#include +#include +#include +#include +#include + +using namespace xray::render::fg; +namespace xray::render::pbr { + +// Runtime PBR flag +extern ENGINE_API int ps_r4_use_pbr; + +// ══════════════════════════════════════════════════════════ +// HELPER: Software BC1/BC3 Decompression (DXT1/DXT5 → RGBA8) +// ══════════════════════════════════════════════════════════ + +// Decompress BC3 alpha block → 16 alpha values +static void DecompressBC3AlphaBlock(const u8* blockData, u8* outAlpha) { + // Read alpha endpoints + u8 alpha0 = blockData[0]; + u8 alpha1 = blockData[1]; + + // Build alpha palette (8 values) + u8 alphaPalette[8]; + alphaPalette[0] = alpha0; + alphaPalette[1] = alpha1; + + if (alpha0 > alpha1) { + // 8-alpha mode: 6 interpolated values + alphaPalette[2] = (6 * alpha0 + 1 * alpha1) / 7; + alphaPalette[3] = (5 * alpha0 + 2 * alpha1) / 7; + alphaPalette[4] = (4 * alpha0 + 3 * alpha1) / 7; + alphaPalette[5] = (3 * alpha0 + 4 * alpha1) / 7; + alphaPalette[6] = (2 * alpha0 + 5 * alpha1) / 7; + alphaPalette[7] = (1 * alpha0 + 6 * alpha1) / 7; + } else { + // 6-alpha mode: 4 interpolated values + transparent + opaque + alphaPalette[2] = (4 * alpha0 + 1 * alpha1) / 5; + alphaPalette[3] = (3 * alpha0 + 2 * alpha1) / 5; + alphaPalette[4] = (2 * alpha0 + 3 * alpha1) / 5; + alphaPalette[5] = (1 * alpha0 + 4 * alpha1) / 5; + alphaPalette[6] = 0; // Transparent + alphaPalette[7] = 255; // Opaque + } + + // Read 48 bits of indices (16 pixels × 3 bits each) + u64 indices = 0; + indices |= static_cast(blockData[2]); + indices |= static_cast(blockData[3]) << 8; + indices |= static_cast(blockData[4]) << 16; + indices |= static_cast(blockData[5]) << 24; + indices |= static_cast(blockData[6]) << 32; + indices |= static_cast(blockData[7]) << 40; + + // Write 16 alpha values + for (u32 i = 0; i < 16; ++i) { + u32 index = static_cast((indices >> (i * 3)) & 0x7); + outAlpha[i] = alphaPalette[index]; + } +} + +// Decompress a single BC1 (DXT1) block → 16 RGBA pixels +static void DecompressBC1Block(const u8* blockData, u8* outPixels, bool hasAlpha) { + // Read color endpoints (565 format) + u16 color0 = blockData[0] | (blockData[1] << 8); + u16 color1 = blockData[2] | (blockData[3] << 8); + + // Extract RGB (565 → 888) + u8 r0 = ((color0 >> 11) & 0x1F) * 255 / 31; + u8 g0 = ((color0 >> 5) & 0x3F) * 255 / 63; + u8 b0 = (color0 & 0x1F) * 255 / 31; + + u8 r1 = ((color1 >> 11) & 0x1F) * 255 / 31; + u8 g1 = ((color1 >> 5) & 0x3F) * 255 / 63; + u8 b1 = (color1 & 0x1F) * 255 / 31; + + // Build color palette + u8 palette[4][4]; + palette[0][0] = r0; palette[0][1] = g0; palette[0][2] = b0; palette[0][3] = 255; + palette[1][0] = r1; palette[1][1] = g1; palette[1][2] = b1; palette[1][3] = 255; + + if (color0 > color1 || !hasAlpha) { + // 4-color mode + palette[2][0] = (2 * r0 + r1) / 3; palette[2][1] = (2 * g0 + g1) / 3; palette[2][2] = (2 * b0 + b1) / 3; palette[2][3] = 255; + palette[3][0] = (r0 + 2 * r1) / 3; palette[3][1] = (g0 + 2 * g1) / 3; palette[3][2] = (b0 + 2 * b1) / 3; palette[3][3] = 255; + } else { + // 3-color mode + transparent + palette[2][0] = (r0 + r1) / 2; palette[2][1] = (g0 + g1) / 2; palette[2][2] = (b0 + b1) / 2; palette[2][3] = 255; + palette[3][0] = 0; palette[3][1] = 0; palette[3][2] = 0; palette[3][3] = 0; // Transparent + } + + // Read indices (2 bits per pixel, 32 bits total) + u32 indices = blockData[4] | (blockData[5] << 8) | (blockData[6] << 16) | (blockData[7] << 24); + + // Write 4x4 pixels + for (u32 i = 0; i < 16; ++i) { + u32 index = (indices >> (i * 2)) & 0x3; + outPixels[i * 4 + 0] = palette[index][0]; + outPixels[i * 4 + 1] = palette[index][1]; + outPixels[i * 4 + 2] = palette[index][2]; + outPixels[i * 4 + 3] = palette[index][3]; + } +} + +// Decompress DXT1/DXT5 texture → RGBA8 +static xr_vector DecompressDDS(const resources::DDSData& ddsData, u32& outWidth, u32& outHeight) { + xr_vector result; + + if (!ddsData.isValid || ddsData.mipLevels.empty()) { + return result; + } + + const auto& mip0 = ddsData.mipLevels[0]; + outWidth = mip0.width; + outHeight = mip0.height; + + const u32 pixelCount = outWidth * outHeight; + result.resize(pixelCount * 4); // RGBA8 + + // Check format (cast enum to int for comparison) + bool isDXT1 = (static_cast(ddsData.desc.format) == 56); // BC1/DXT1 + bool isDXT5 = (static_cast(ddsData.desc.format) == 60); // BC3/DXT5 + bool isRGBA8 = (static_cast(ddsData.desc.format) == 19); // R8G8B8A8_UNORM + bool isR8 = (static_cast(ddsData.desc.format) == 3); // R8_UNORM (single channel) + + // Handle uncompressed RGBA8 (e.g., generated bumpmaps) + if (isRGBA8) { + if (mip0.data && mip0.size >= pixelCount * 4) { + std::memcpy(result.data(), mip0.data, pixelCount * 4); + } + return result; + } + + // Handle R8_UNORM (single channel) - expand to RGBA + if (isR8) { + if (mip0.data && mip0.size >= pixelCount) { + for (u32 i = 0; i < pixelCount; ++i) { + u8 r = mip0.data[i]; + result[i * 4 + 0] = r; // R + result[i * 4 + 1] = r; // G (copy R for grayscale view) + result[i * 4 + 2] = r; // B (copy R for grayscale view) + result[i * 4 + 3] = 255; // A + } + } + return result; + } + + if (!isDXT1 && !isDXT5) { + Msg("! [PBRTextureConverter] Unsupported format: %d (only DXT1/DXT5/RGBA8/R8 supported)", static_cast(ddsData.desc.format)); + return result; + } + + const u32 blockSize = isDXT1 ? 8 : 16; // DXT1=8 bytes, DXT5=16 bytes + const u32 blocksX = (outWidth + 3) / 4; + const u32 blocksY = (outHeight + 3) / 4; + + const u8* srcData = mip0.data; + + for (u32 by = 0; by < blocksY; ++by) { + for (u32 bx = 0; bx < blocksX; ++bx) { + const u8* blockData = srcData + (by * blocksX + bx) * blockSize; + u8 blockPixels[64]; // 4x4 RGBA + + if (isDXT5) { + // DXT5: 8 bytes alpha + 8 bytes color + u8 alphaValues[16]; + DecompressBC3AlphaBlock(blockData, alphaValues); + DecompressBC1Block(blockData + 8, blockPixels, false); + + // Apply alpha values to RGB pixels + for (u32 i = 0; i < 16; ++i) { + blockPixels[i * 4 + 3] = alphaValues[i]; + } + } else { + // DXT1: 8 bytes color only + DecompressBC1Block(blockData, blockPixels, true); + } + + // Copy 4x4 block to output + for (u32 py = 0; py < 4 && (by * 4 + py) < outHeight; ++py) { + for (u32 px = 0; px < 4 && (bx * 4 + px) < outWidth; ++px) { + u32 srcIdx = (py * 4 + px) * 4; + u32 dstIdx = ((by * 4 + py) * outWidth + (bx * 4 + px)) * 4; + result[dstIdx + 0] = blockPixels[srcIdx + 0]; + result[dstIdx + 1] = blockPixels[srcIdx + 1]; + result[dstIdx + 2] = blockPixels[srcIdx + 2]; + result[dstIdx + 3] = blockPixels[srcIdx + 3]; + } + } + } + } + + return result; +} + +static bool GenerateNormalMapFromDiffuse( + const xr_vector& diffuseRGBA, + u32 width, u32 height, + xr_vector& outNormalRGBA, + float amplitude = 12.0f) +{ + if (diffuseRGBA.empty() || width == 0 || height == 0) { + return false; + } + + const u32 pixelCount = width * height; + xr_vector luminance(pixelCount); + for (u32 i = 0; i < pixelCount; ++i) { + luminance[i] = (0.2126f * diffuseRGBA[i * 4 + 0] + + 0.7152f * diffuseRGBA[i * 4 + 1] + + 0.0722f * diffuseRGBA[i * 4 + 2]) / 255.0f; + } + + auto sample = [&](u32 x, u32 y) { + return luminance[(y % height) * width + (x % width)]; + }; + + outNormalRGBA.resize(pixelCount * 4); + + const float scale = amplitude * 0.25f; + for (u32 y = 0; y < height; ++y) { + for (u32 x = 0; x < width; ++x) { + const u32 xl = x + width - 1; + const u32 xr = x + 1; + const u32 yt = y + height - 1; + const u32 yb = y + 1; + + const float dx = + (sample(xr, yt) + 2.0f * sample(xr, y) + sample(xr, yb)) - + (sample(xl, yt) + 2.0f * sample(xl, y) + sample(xl, yb)); + const float dy = + (sample(xl, yb) + 2.0f * sample(x, yb) + sample(xr, yb)) - + (sample(xl, yt) + 2.0f * sample(x, yt) + sample(xr, yt)); + + const float nx = -dx * scale; + const float ny = -dy * scale; + const float invLen = 1.0f / std::sqrt(nx * nx + ny * ny + 1.0f); + + const u8 bx = static_cast((nx * invLen * 0.5f + 0.5f) * 255.0f); + const u8 by = static_cast((ny * invLen * 0.5f + 0.5f) * 255.0f); + const u8 bz = static_cast((invLen * 0.5f + 0.5f) * 255.0f); + + const u32 i = (y * width + x) * 4; + outNormalRGBA[i + 0] = 128; + outNormalRGBA[i + 1] = bz; + outNormalRGBA[i + 2] = by; + outNormalRGBA[i + 3] = bx; + } + } + + return true; +} + +// Helper to write generated normal map as DDS (RGBA8 uncompressed for simplicity) +static bool WriteNormalMapDDS( + const char* root_alias, + const xr_string& relative_path, + const xr_vector& normalRGBA, + u32 width, u32 height) +{ + using namespace resources; + + DDS_HEADER header = {}; + header.dwSize = 124; + header.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_PITCH | DDSD_MIPMAPCOUNT; + header.dwWidth = width; + header.dwHeight = height; + header.dwPitchOrLinearSize = width * 4; + header.dwDepth = 0; + header.dwMipMapCount = 1; + + // RGBA8 pixel format + header.ddspf.dwSize = 32; + header.ddspf.dwFlags = DDPF_RGB | DDPF_ALPHAPIXELS; + header.ddspf.dwRGBBitCount = 32; + header.ddspf.dwRBitMask = 0x000000FF; // R in byte 0 + header.ddspf.dwGBitMask = 0x0000FF00; // G in byte 1 + header.ddspf.dwBBitMask = 0x00FF0000; // B in byte 2 + header.ddspf.dwABitMask = 0xFF000000; // A in byte 3 + + header.dwCaps = DDSCAPS_TEXTURE; + + IWriter* writer = FS.w_open(root_alias, relative_path.c_str()); + if (!writer) { + Msg("! [PBRTextureConverter] Failed to open file for writing: %s/%s", root_alias, relative_path.c_str()); + return false; + } + + const u32 magic = DDS_MAGIC; + writer->w(&magic, sizeof(magic)); + writer->w(&header, sizeof(header)); + writer->w(normalRGBA.data(), normalRGBA.size()); + + FS.w_close(writer); + Msg("* [PBRTextureConverter] Generated normal map: %s/%s (%ux%u)", root_alias, relative_path.c_str(), width, height); + return true; +} + +#ifdef USE_AI_PBR +// Global AI pipeline (initialized once, reused for all conversions) +static xr_unique_ptr g_ai_pipeline; +static bool g_ai_available = false; + +// Mutex to serialize AI inference (ONNX Runtime sessions are NOT thread-safe) +static Lock g_ai_pipeline_mutex; +#endif + +// ══════════════════════════════════════════════════════════ +// HELPER: Extract base name from texture path +// ══════════════════════════════════════════════════════════ +// "wood\oak_d.dds" → "wood\oak" +// "metal_rusty_s.dds" → "metal_rusty" + +static xr_string ExtractBaseName(const xr_string& path) { + xr_string result = path; + + // Remove extension + const auto dot = result.find_last_of('.'); + if (dot != xr_string::npos) { + result = result.substr(0, dot); + } + + // Remove suffix (_d, _s, _g, _bump, _bump#) + const auto suffixes = { "_d", "_s", "_g", "_bump", "_bump#" }; + for (const auto& suffix : suffixes) { + const size_t suffix_len = xr_strlen(suffix); + if (result.size() >= suffix_len) { + const xr_string ending = result.substr(result.size() - suffix_len); + if (ending == suffix) { + result = result.substr(0, result.size() - suffix_len); + break; + } + } + } + + return result; +} + +// ══════════════════════════════════════════════════════════ +// HELPER: Check if file exists +// ══════════════════════════════════════════════════════════ + +static bool FileExists(const char* root_alias, const char* relative_path) { + string_path full_path; + FS.update_path(full_path, root_alias, relative_path); + return FS.exist(full_path); +} + +// ══════════════════════════════════════════════════════════ +// HELPER: Get file metadata +// ══════════════════════════════════════════════════════════ + +static FileLocation MakeFileLocation( + const xr_string& root_alias, + const FS_File& file) +{ + FileLocation loc; + loc.root_alias = root_alias; + loc.relative_path = file.name.c_str(); + loc.file_size = file.size >= 0 ? static_cast(file.size) : 0; + loc.modified_time_seconds = file.time_write >= 0 ? static_cast(file.time_write) : 0; + return loc; +} + +// ══════════════════════════════════════════════════════════ +// BUILD TEXTURE INVENTORY (Phase 2.5.2) +// ══════════════════════════════════════════════════════════ +// Scans filesystem for diffuse textures (_d.dds) and finds +// matching specular/gloss/normal textures + +// ══════════════════════════════════════════════════════════ +// FOLDER BLACKLIST - Skip folders with special texture semantics +// ══════════════════════════════════════════════════════════ +// These folders contain textures where RGBA channels have special meanings +// (e.g., terrain masks use RGBA for 4-layer blending) and should NOT be +// processed by the PBR converter which may corrupt alpha channels. + +static const char* const FOLDER_BLACKLIST[] = { + "editor", + "fx", + "glow", + "grad", + "internal", + "lights", + "pfx", + "shaders", + "sky", // Sky textures + "ui", // UI textures + "intro", + "terrain", + "wm", +}; + +static const char* const METALLIC_WHITELIST[] = { + "metall", "metal", "iron", "steel", "chrome", "alumin", + "copper", "brass", "bronze", "gold", "silver", "tin", "zinc", + "pipe", "rebar", "wire", "chain", "bolt", "nut", "screw", + "rail", "grate", "mesh", +}; + +static const char* const NON_METALLIC_KEYWORDS[] = { + "wood", "bark", "bork", + "dirt", "ground", "grnd", "soil", "mud", "sand", + "grass", "leaf", "leaves", "vine", "moss", "bush", "tree", + "brick", "briks", "concrete", "crete", "stone", "rock", "gravel", + "plaster", "stucco", "cement", "asphalt", "road", + "cloth", "fabric", "carpet", "curtain", "leather", + "paper", "cardboard", + "skin", "flesh", "body", + "food", + "oblaka", "sky", "cloud", + "water", + "detail", "build_details", + "floor", "roof", "tile", "wall", +}; + +static bool IsNonMetallic(const xr_string& name) { + xr_string lower = name; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + for (const char* keyword : METALLIC_WHITELIST) { + if (lower.find(keyword) != xr_string::npos) + return false; + } + for (const char* keyword : NON_METALLIC_KEYWORDS) { + if (lower.find(keyword) != xr_string::npos) + return true; + } + return false; +} + +static bool IsInBlacklistedFolder(const xr_string& path) { + for (const char* folder : FOLDER_BLACKLIST) { + // Check if path starts with "folder\" or "folder/" + xr_string prefix1 = xr_string(folder) + "\\"; + xr_string prefix2 = xr_string(folder) + "/"; + if (path.find(prefix1) == 0 || path.find(prefix2) == 0) { + return true; + } + // Also check if folder appears anywhere in path (subfolder) + xr_string infix1 = xr_string("\\") + folder + "\\"; + xr_string infix2 = xr_string("/") + folder + "/"; + if (path.find(infix1) != xr_string::npos || path.find(infix2) != xr_string::npos) { + return true; + } + } + return false; +} + +static bool ReadDDSDimensions(const char* root_alias, const char* relative_path, u32& outWidth, u32& outHeight) { + string_path full_path; + FS.update_path(full_path, root_alias, relative_path); + if (!FS.exist(full_path)) + return false; + + IReader* reader = FS.r_open(root_alias, relative_path); + if (!reader) return false; + + if (reader->length() < 20) { + FS.r_close(reader); + return false; + } + + u32 magic = reader->r_u32(); + if (magic != 0x20534444) { + FS.r_close(reader); + return false; + } + + reader->r_u32(); + reader->r_u32(); + outHeight = reader->r_u32(); + outWidth = reader->r_u32(); + + FS.r_close(reader); + return (outWidth > 0 && outHeight > 0); +} + +TextureInventory BuildTextureInventory(const TextureScanConfig& config) { + TextureInventory inventory; + + // Map: base_name → asset + xr_map asset_map; + + for (const auto& root : config.texture_roots) { + // ═══════════════════════════════════════════════════════ + // SCAN FOR ALL DDS TEXTURES + // ═══════════════════════════════════════════════════════ + FS_FileSet diffuse_files; + + // Recursive or non-recursive based on config + // NOTE: FS_RootOnly means "don't recurse" - so we add it when recursive=false! + const u32 flags = FS_ListFiles | (config.recursive ? 0 : FS_RootOnly); + FS.file_list(diffuse_files, root.c_str(), flags, "*.dds"); + + for (const auto& file : diffuse_files) { + const xr_string file_name = file.name.c_str(); + + // Skip special shader test textures (start with $) + if (!file_name.empty() && file_name[0] == '$') { + continue; // Skip shader test textures like $alphadxt1, $shadertest, etc. + } + + // Skip blacklisted folders (terrain, levels, etc.) + if (IsInBlacklistedFolder(file_name)) { + continue; // Skip textures in folders with special RGBA semantics + } + + // Skip files that are clearly NOT base textures + // We want to find base diffuse/albedo textures, which can be: + // - Vanilla: wood.dds (no suffix) + // - Modded: wood_d.dds (diffuse suffix) + // Skip: bump maps, normal maps, metallic, roughness, etc. + if (file_name.find("_bump") != xr_string::npos || + file_name.find("_bump#") != xr_string::npos || + file_name.find("_normal") != xr_string::npos || + file_name.find("_metallic") != xr_string::npos || + file_name.find("_roughness") != xr_string::npos || + file_name.find("_ao") != xr_string::npos || + file_name.find("_parallax") != xr_string::npos || + file_name.find("_pbr") != xr_string::npos || // Skip consolidated PBR textures + file_name.find("_s.dds") != xr_string::npos || // Skip specular maps + file_name.find("_g.dds") != xr_string::npos) { // Skip gloss maps + continue; // Skip these utility textures + } + + // This is a potential base texture (either vanilla or modded) + // Examples: wood.dds, wood_d.dds, metal_rusty.dds, etc. + + // Extract base name + const xr_string base_name = ExtractBaseName(file_name); + if (base_name.empty()) { + continue; + } + + // Create or get asset + auto& asset = asset_map[base_name]; + asset.base_name = base_name; + asset.diffuse = MakeFileLocation(root, file); + asset.total_size_bytes = asset.diffuse.file_size; + asset.latest_modified_time = asset.diffuse.modified_time_seconds; + + ReadDDSDimensions(root.c_str(), file_name.c_str(), asset.diffuse_width, asset.diffuse_height); + + // ═══════════════════════════════════════════════════════ + // LOOK FOR MATCHING TEXTURES + // ═══════════════════════════════════════════════════════ + + // Specular (_s.dds) - for heuristic fallback + xr_string spec_path = base_name + "_s.dds"; + if (FileExists(root.c_str(), spec_path.c_str())) { + asset.has_specular = true; + asset.specular.root_alias = root; + asset.specular.relative_path = spec_path; + } + + // Gloss (_g.dds) - for heuristic fallback + xr_string gloss_path = base_name + "_g.dds"; + if (FileExists(root.c_str(), gloss_path.c_str())) { + asset.has_gloss = true; + asset.gloss.root_alias = root; + asset.gloss.relative_path = gloss_path; + } + + // Normal (_bump.dds or _bump#.dds) - for AI conversion + xr_string normal_path = base_name + "_bump.dds"; + if (FileExists(root.c_str(), normal_path.c_str())) { + asset.has_normal = true; + asset.normal.root_alias = root; + asset.normal.relative_path = normal_path; + } else { + // Try alternate naming + normal_path = base_name + "_bump#.dds"; + if (FileExists(root.c_str(), normal_path.c_str())) { + asset.has_normal = true; + asset.normal.root_alias = root; + asset.normal.relative_path = normal_path; + } + } + } + } + + // Convert map to vector + inventory.assets.reserve(asset_map.size()); + for (auto& [base_name, asset] : asset_map) { + inventory.assets.emplace_back(std::move(asset)); + + // Update statistics + inventory.total_textures++; + if (asset.has_specular) inventory.textures_with_specular++; + if (asset.has_gloss) inventory.textures_with_gloss++; + inventory.total_size_bytes += asset.total_size_bytes; + } + + Msg("[PBRTextureConverter] Scanned %u textures (%u with specular, %u with gloss)", + inventory.total_textures, + inventory.textures_with_specular, + inventory.textures_with_gloss); + + return inventory; +} + +// ══════════════════════════════════════════════════════════ +// COMPUTE INVENTORY DIGEST (Change Detection) +// ══════════════════════════════════════════════════════════ +// Similar to OGF digest: Hash all file sizes and timestamps + +xr_string ComputeTextureInventoryDigest(const TextureInventory& inventory) { + // Build string with all metadata + xr_string digest_input; + digest_input.reserve(inventory.assets.size() * 128); + + for (const auto& asset : inventory.assets) { + digest_input.append(asset.base_name); + digest_input.append(":"); + digest_input.append(std::to_string(asset.total_size_bytes)); + digest_input.append(":"); + digest_input.append(std::to_string(asset.latest_modified_time)); + digest_input.append("|"); + } + + // Compute SHA256 hash + // TODO: Use X-Ray's crypto API (xrCore/crypto/crypto.h) + // For now, use simple hash + const u32 simple_hash = crc32(digest_input.c_str(), digest_input.size()); + + char hex_buffer[16]; + xr_sprintf(hex_buffer, sizeof(hex_buffer), "%08x", simple_hash); + return xr_string(hex_buffer); +} + +// Forward declaration for WriteTHMFileConsolidated (defined in consolidation section) +static bool WriteTHMFileConsolidated(const char* root_alias, const xr_string& base_name, const xr_string& pbr_name); + +// ══════════════════════════════════════════════════════════ +// HELPER: Check if .thm needs PBR update +// ══════════════════════════════════════════════════════════ +// Returns true if .thm doesn't exist or lacks PBR texture references + +static bool THMNeedsPBRUpdate( + const char* root_alias, + const xr_string& base_name, + const xr_string& pbr_name) // Check for consolidated pbr_name +{ + xr_string thm_path = base_name + ".thm"; + + string_path full_path; + FS.update_path(full_path, root_alias, thm_path.c_str()); + + if (!FS.exist(full_path)) { + return true; // .thm doesn't exist + } + + // Load existing .thm and check PBR fields + IReader* reader = FS.r_open(root_alias, thm_path.c_str()); + if (!reader) { + return true; // Can't read, needs update + } + + STextureParams params; + params.Clear(); + params.Load(*reader); + FS.r_close(reader); + + // Check if consolidated pbr_name matches expected value + if (params.pbr_name != pbr_name.c_str()) { + return true; // pbr_name doesn't match + } + + return false; // .thm is up to date with consolidated PBR +} + +// ══════════════════════════════════════════════════════════ +// VERIFY PBR OUTPUTS (Check if conversion needed) +// ══════════════════════════════════════════════════════════ + +bool VerifyPBROutputs( + const TextureInventory& inventory, + const PBRConversionParams& params) +{ + u32 missing_textures = 0; + u32 missing_thm = 0; + + for (const auto& asset : inventory.assets) { + // Check if consolidated _pbr.dds exists (preferred) + xr_string pbr_path = asset.base_name + "_pbr.dds"; + xr_string pbr_name = asset.base_name + "_pbr"; + + if (FileExists(params.output_root.c_str(), pbr_path.c_str())) { + // Consolidated PBR exists - check if .thm has pbr_name field + if (THMNeedsPBRUpdate(params.output_root.c_str(), asset.base_name, pbr_name)) { + missing_thm++; + } + } else { + // No consolidated _pbr.dds - need conversion + missing_textures++; + } + } + + if (missing_textures > 0) { + Msg("[PBRTextureConverter] %u/%u textures need conversion", + missing_textures, inventory.total_textures); + return false; // Need full conversion + } + + if (missing_thm > 0) { + Msg("[PBRTextureConverter] %u/%u .thm files need PBR fields updated", + missing_thm, inventory.total_textures); + return false; // Need .thm update pass + } + + return true; // All outputs verified +} + +// ══════════════════════════════════════════════════════════ +// HELPER: Convert Spec/Gloss → Metallic/Roughness (Phase 2.5.2) +// ══════════════════════════════════════════════════════════ +// PBR Conversion Algorithm: +// - Metallic: Extracted from specular reflectance (high specular = metallic) +// - Roughness: Inverted gloss (roughness = 1.0 - gloss) +// - Fallbacks: metallic=0.0 (non-metallic), roughness=0.5 (mid-range) + +struct ConvertedPBRTextures { + xr_vector albedoData; // RGBA (4 channels) - preserves original alpha! + xr_vector metallicData; // Single-channel (R8) + xr_vector roughnessData; // Single-channel (R8) + xr_vector aoData; // Single-channel (R8) + xr_vector parallaxData; // Single-channel (R8) - height map + u32 width = 0; + u32 height = 0; + bool success = false; +}; + +static void ZeroMetallicIfBlacklisted(ConvertedPBRTextures& converted, const xr_string& name) { + if (!converted.success || converted.metallicData.empty()) + return; + if (IsNonMetallic(name)) { + std::memset(converted.metallicData.data(), 0, converted.metallicData.size()); + Msg("~ [PBRTextureConverter] Forced metallic=0 for non-metallic material: %s", name.c_str()); + } +} + +#ifdef USE_AI_PBR + +// ══════════════════════════════════════════════════════════ +// HELPER: Initialize AI Pipeline (Once) +// ══════════════════════════════════════════════════════════ + +static bool InitializeAIPipeline() { + if (g_ai_pipeline) { + return true; // Already initialized + } + + // Initialize pipeline + PBRPipelineConfig config; + config.use_gpu = true; + config.verbose = true; // Set to true for debugging + + g_ai_pipeline.reset(xr_new()); + if (!g_ai_pipeline->Initialize(config)) { + Msg("! [PBRTextureConverter] Failed to initialize AI pipeline"); + g_ai_pipeline.reset(); + g_ai_available = false; + return false; + } + + g_ai_available = true; + Msg("[PBRTextureConverter] AI pipeline initialized successfully"); + return true; +} + +static xr_vector NearestNeighborUpscaleRGBA( + const xr_vector& src, u32 srcW, u32 srcH, u32 dstW, u32 dstH) +{ + xr_vector dst(dstW * dstH * 4); + for (u32 y = 0; y < dstH; ++y) { + for (u32 x = 0; x < dstW; ++x) { + u32 srcIdx = (y * srcH / dstH * srcW + x * srcW / dstW) * 4; + u32 dstIdx = (y * dstW + x) * 4; + dst[dstIdx + 0] = src[srcIdx + 0]; + dst[dstIdx + 1] = src[srcIdx + 1]; + dst[dstIdx + 2] = src[srcIdx + 2]; + dst[dstIdx + 3] = src[srcIdx + 3]; + } + } + return dst; +} + +struct DecompressedPair { + xr_vector diffuseRGBA; + xr_vector normalRGBA; + u32 width = 0; + u32 height = 0; + bool success = false; +}; + +static DecompressedPair DecompressAndPrepare( + const resources::DDSData* diffuseData, + const resources::DDSData* normalData) +{ + DecompressedPair result; + + if (!diffuseData || !diffuseData->isValid || diffuseData->mipLevels.empty()) + return result; + if (!normalData || !normalData->isValid || normalData->mipLevels.empty()) + return result; + + u32 diffuseWidth, diffuseHeight, normalWidth, normalHeight; + result.diffuseRGBA = DecompressDDS(*diffuseData, diffuseWidth, diffuseHeight); + result.normalRGBA = DecompressDDS(*normalData, normalWidth, normalHeight); + + if (result.diffuseRGBA.empty() || result.normalRGBA.empty()) { + Msg("! [PBRTextureConverter] Failed to decompress textures"); + return result; + } + + Msg("~ [PBRTextureConverter] Decompressed dimensions: diffuse %ux%u, normal %ux%u", + diffuseWidth, diffuseHeight, normalWidth, normalHeight); + + constexpr u32 MIN_AI_TEXTURE_SIZE = 32; + if (diffuseWidth < MIN_AI_TEXTURE_SIZE || diffuseHeight < MIN_AI_TEXTURE_SIZE) { + u32 newWidth = std::max(diffuseWidth, MIN_AI_TEXTURE_SIZE); + u32 newHeight = std::max(diffuseHeight, MIN_AI_TEXTURE_SIZE); + Msg("~ [PBRTextureConverter] Upscaling small texture %ux%u → %ux%u for AI processing", + diffuseWidth, diffuseHeight, newWidth, newHeight); + + result.diffuseRGBA = NearestNeighborUpscaleRGBA(result.diffuseRGBA, diffuseWidth, diffuseHeight, newWidth, newHeight); + result.normalRGBA = NearestNeighborUpscaleRGBA(result.normalRGBA, normalWidth, normalHeight, newWidth, newHeight); + + diffuseWidth = newWidth; + diffuseHeight = newHeight; + normalWidth = newWidth; + normalHeight = newHeight; + } + + if (normalWidth != diffuseWidth || normalHeight != diffuseHeight) { + Msg("~ [PBRTextureConverter] Upscaling normal map: %ux%u → %ux%u", + normalWidth, normalHeight, diffuseWidth, diffuseHeight); + result.normalRGBA = NearestNeighborUpscaleRGBA(result.normalRGBA, normalWidth, normalHeight, diffuseWidth, diffuseHeight); + } + + result.width = diffuseWidth; + result.height = diffuseHeight; + result.success = true; + return result; +} + +static ConvertedPBRTextures PackOutputs(PBRPipelineOutputs& outputs, const xr_vector& originalDiffuseRGBA, u32 width, u32 height) { + ConvertedPBRTextures result; + result.width = width; + result.height = height; + + xr_vector aiAlbedoRGB = outputs.albedo.ToImageData(false); + result.metallicData = outputs.metallic.ToImageData(true); + result.roughnessData = outputs.roughness.ToImageData(true); + result.aoData = outputs.ao.ToImageData(true); + result.parallaxData = outputs.parallax.ToImageData(true); + + const u32 pixelCount = width * height; + result.albedoData.resize(pixelCount * 4); + + for (u32 i = 0; i < pixelCount; ++i) { + result.albedoData[i * 4 + 0] = aiAlbedoRGB[i * 3 + 0]; + result.albedoData[i * 4 + 1] = aiAlbedoRGB[i * 3 + 1]; + result.albedoData[i * 4 + 2] = aiAlbedoRGB[i * 3 + 2]; + result.albedoData[i * 4 + 3] = originalDiffuseRGBA[i * 4 + 3]; + } + + result.success = true; + return result; +} + +struct Stage1Intermediate { + PBRPipeline::Stage1Result stage1; + xr_vector originalDiffuseRGBA; + u32 width = 0; + u32 height = 0; + xr_string base_name; + xr_string pbr_path; + xr_string pbr_name; + xr_string albedo_path; +}; + +#endif // USE_AI_PBR + +// ══════════════════════════════════════════════════════════ +// HELPER: Write Single-Channel DDS (Phase 2.5.2) +// ══════════════════════════════════════════════════════════ +// Writes R8 format DDS file (single channel, 8-bit) + +static xr_vector GenerateMipLevel(const u8* srcData, u32 srcWidth, u32 srcHeight, u32 channels) { + // Box filter downsample by 2x2 + const u32 dstWidth = std::max(srcWidth / 2, 1u); + const u32 dstHeight = std::max(srcHeight / 2, 1u); + xr_vector dstData(dstWidth * dstHeight * channels); + + for (u32 y = 0; y < dstHeight; ++y) { + for (u32 x = 0; x < dstWidth; ++x) { + // Sample 2x2 pixels from source + for (u32 c = 0; c < channels; ++c) { + u32 sum = 0; + u32 count = 0; + + for (u32 sy = 0; sy < 2 && (y * 2 + sy) < srcHeight; ++sy) { + for (u32 sx = 0; sx < 2 && (x * 2 + sx) < srcWidth; ++sx) { + sum += srcData[((y * 2 + sy) * srcWidth + (x * 2 + sx)) * channels + c]; + count++; + } + } + + dstData[(y * dstWidth + x) * channels + c] = static_cast(sum / count); + } + } + } + + return dstData; +} + +static bool WriteRGBADDS( + const char* root_alias, + const xr_string& relative_path, + const u8* rgbaData, + u32 width, + u32 height, + bool generateMipmaps) +{ + // Build DDS header for RGBA8 (32-bit with alpha, like texconv outputs) + using namespace resources; + + // Calculate mipmap count + u32 mipCount = 1; + if (generateMipmaps) { + u32 w = width, h = height; + while (w > 1 || h > 1) { + w = std::max(w / 2, 1u); + h = std::max(h / 2, 1u); + mipCount++; + } + } + + DDS_HEADER header = {}; + header.dwSize = 124; + header.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_PITCH; + if (generateMipmaps) { + header.dwFlags |= DDSD_MIPMAPCOUNT; + } + header.dwHeight = height; + header.dwWidth = width; + header.dwPitchOrLinearSize = width * 4; // 4 bytes per pixel (RGBA) + header.dwDepth = 0; + header.dwMipMapCount = mipCount; + + // Pixel format: RGBA8 (32-bit with alpha) + header.ddspf.dwSize = 32; + header.ddspf.dwFlags = DDPF_RGB | DDPF_ALPHAPIXELS; + header.ddspf.dwRGBBitCount = 32; + header.ddspf.dwRBitMask = 0x000000FF; // Red (low byte) + header.ddspf.dwGBitMask = 0x0000FF00; // Green + header.ddspf.dwBBitMask = 0x00FF0000; // Blue + header.ddspf.dwABitMask = 0xFF000000; // Alpha (high byte) + + header.dwCaps = DDSCAPS_TEXTURE; + if (generateMipmaps) { + header.dwCaps |= DDSCAPS_MIPMAP | DDSCAPS_COMPLEX; + } + + // Write file + IWriter* writer = FS.w_open(root_alias, relative_path.c_str()); + if (!writer) { + Msg("! [PBRTextureConverter] Failed to open file for writing: %s/%s", root_alias, relative_path.c_str()); + return false; + } + + const u32 magic = DDS_MAGIC; + writer->w(&magic, sizeof(magic)); + writer->w(&header, sizeof(header)); + + // Write mip 0 - data is already RGBA with preserved alpha! + const u32 pixelCount = width * height; + writer->w(rgbaData, pixelCount * 4); + + // Generate and write mipmaps + if (generateMipmaps) { + xr_vector currentMip(rgbaData, rgbaData + pixelCount * 4); + u32 mipWidth = width; + u32 mipHeight = height; + + for (u32 mipLevel = 1; mipLevel < mipCount; ++mipLevel) { + currentMip = GenerateMipLevel(currentMip.data(), mipWidth, mipHeight, 4); + mipWidth = std::max(mipWidth / 2, 1u); + mipHeight = std::max(mipHeight / 2, 1u); + + writer->w(currentMip.data(), currentMip.size()); + } + } + + FS.w_close(writer); + return true; +} + +// ══════════════════════════════════════════════════════════ +// HELPER: Write Packed PBR DDS (Metallic/Roughness/AO/Parallax) +// ══════════════════════════════════════════════════════════ +// Packs 4 single-channel textures into one RGBA8 texture: +// R = Metallic +// G = Roughness +// B = AO +// A = Parallax (height map, or 255 if not provided) + +static bool WritePackedPBRDDS( + const char* root_alias, + const xr_string& relative_path, + const u8* metallicData, + const u8* roughnessData, + const u8* aoData, + const u8* parallaxData, // Can be nullptr + u32 width, + u32 height, + bool generateMipmaps) +{ + // Pack into RGBA8 + const u32 pixelCount = width * height; + xr_vector packedData(pixelCount * 4); + + for (u32 i = 0; i < pixelCount; ++i) { + packedData[i * 4 + 0] = metallicData ? metallicData[i] : 0; // R = Metallic + packedData[i * 4 + 1] = roughnessData ? roughnessData[i] : 128; // G = Roughness (default 0.5) + packedData[i * 4 + 2] = aoData ? aoData[i] : 255; // B = AO (default 1.0) + packedData[i * 4 + 3] = parallaxData ? parallaxData[i] : 128; // A = Parallax (default 0.5) + } + + return WriteRGBADDS(root_alias, relative_path, packedData.data(), width, height, generateMipmaps); +} + +static bool WriteSingleChannelDDS( + const char* root_alias, + const xr_string& relative_path, + const u8* pixelData, + u32 width, + u32 height, + bool generateMipmaps) +{ + // Build DDS header + using namespace resources; + + // Calculate mipmap count + u32 mipCount = 1; + if (generateMipmaps) { + u32 w = width, h = height; + while (w > 1 || h > 1) { + w = std::max(w / 2, 1u); + h = std::max(h / 2, 1u); + mipCount++; + } + } + + DDS_HEADER header = {}; + header.dwSize = 124; + header.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT; + if (generateMipmaps) { + header.dwFlags |= DDSD_MIPMAPCOUNT; + } + header.dwHeight = height; + header.dwWidth = width; + header.dwPitchOrLinearSize = width; // 1 byte per pixel + header.dwDepth = 0; + header.dwMipMapCount = mipCount; + + // Pixel format: R8 (single channel, 8-bit) + header.ddspf.dwSize = 32; + header.ddspf.dwFlags = DDPF_LUMINANCE; + header.ddspf.dwRGBBitCount = 8; + header.ddspf.dwRBitMask = 0xFF; + header.ddspf.dwGBitMask = 0; + header.ddspf.dwBBitMask = 0; + header.ddspf.dwABitMask = 0; + + header.dwCaps = DDSCAPS_TEXTURE; + if (generateMipmaps) { + header.dwCaps |= DDSCAPS_MIPMAP | DDSCAPS_COMPLEX; + } + + // Write file using two-parameter VFS pattern + IWriter* writer = FS.w_open(root_alias, relative_path.c_str()); + if (!writer) { + Msg("! [PBRTextureConverter] Failed to open file for writing: %s/%s", root_alias, relative_path.c_str()); + return false; + } + + // Write magic number + const u32 magic = DDS_MAGIC; + writer->w(&magic, sizeof(magic)); + + // Write header + writer->w(&header, sizeof(header)); + + // Write mip 0 + const u32 dataSize = width * height; + writer->w(pixelData, dataSize); + + // Generate and write mipmaps + if (generateMipmaps) { + xr_vector currentMip(pixelData, pixelData + dataSize); + u32 mipWidth = width; + u32 mipHeight = height; + + for (u32 mipLevel = 1; mipLevel < mipCount; ++mipLevel) { + currentMip = GenerateMipLevel(currentMip.data(), mipWidth, mipHeight, 1); + mipWidth = std::max(mipWidth / 2, 1u); + mipHeight = std::max(mipHeight / 2, 1u); + + writer->w(currentMip.data(), currentMip.size()); + } + } + + FS.w_close(writer); + return true; +} + +// ══════════════════════════════════════════════════════════ +// SANITY TEST: Write decompressed DDS to verify decompressor +// ══════════════════════════════════════════════════════════ +static bool WriteUncompressedDDS( + const char* root_alias, + const xr_string& relative_path, + const u8* rgbaData, + u32 width, + u32 height) +{ + using namespace resources; + + DDS_HEADER header = {}; + header.dwSize = 124; + header.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT; + header.dwWidth = width; + header.dwHeight = height; + header.dwPitchOrLinearSize = width * 4; // RGBA8 + header.dwDepth = 0; + header.dwMipMapCount = 1; + + // RGBA8 pixel format (uncompressed) + header.ddspf.dwSize = 32; + header.ddspf.dwFlags = DDPF_RGB | DDPF_ALPHAPIXELS; + header.ddspf.dwRGBBitCount = 32; + header.ddspf.dwRBitMask = 0x000000FF; + header.ddspf.dwGBitMask = 0x0000FF00; + header.ddspf.dwBBitMask = 0x00FF0000; + header.ddspf.dwABitMask = 0xFF000000; + + header.dwCaps = DDSCAPS_TEXTURE; + + // Write file + IWriter* writer = FS.w_open(root_alias, relative_path.c_str()); + if (!writer) { + Msg("! [PBRTextureConverter] Failed to open file for writing: %s/%s", root_alias, relative_path.c_str()); + return false; + } + + const u32 magic = DDS_MAGIC; + writer->w(&magic, sizeof(magic)); + writer->w(&header, sizeof(header)); + writer->w(rgbaData, width * height * 4); // RGBA8 + + FS.w_close(writer); + Msg("* [PBRTextureConverter] Wrote uncompressed DDS: %s/%s (%ux%u)", root_alias, relative_path.c_str(), width, height); + return true; +} + +static void SanityTestDecompressor(const LegacyTextureAsset& asset) +{ + Msg("~ [PBRTextureConverter] SANITY TEST: Decompressing %s...", asset.diffuse.relative_path.c_str()); + + // Load DDS + auto StripDDSExtension = [](const xr_string& path) -> xr_string { + if (path.size() >= 4 && path.substr(path.size() - 4) == ".dds") { + return path.substr(0, path.size() - 4); + } + return path; + }; + + resources::DDSData diffuseData; + xr_string diffuse_vfs_path = StripDDSExtension(asset.diffuse.relative_path); + if (!resources::DDSLoader::LoadFromFile(diffuse_vfs_path.c_str(), diffuseData)) { + Msg("! [PBRTextureConverter] SANITY TEST: Failed to load diffuse"); + return; + } + + // Decompress + u32 width, height; + xr_vector rgba = DecompressDDS(diffuseData, width, height); + + if (rgba.empty()) { + Msg("! [PBRTextureConverter] SANITY TEST: Decompression failed"); + return; + } + + Msg("* [PBRTextureConverter] SANITY TEST: Decompressed to %ux%u RGBA8 (%u bytes)", width, height, rgba.size()); + + // Write to res/gamedata/test_decompress.dds + WriteUncompressedDDS("$game_data$", "test_decompress_diffuse.dds", rgba.data(), width, height); + + // Also test normal map if available + if (asset.has_normal) { + resources::DDSData normalData; + xr_string normal_vfs_path = StripDDSExtension(asset.normal.relative_path); + if (resources::DDSLoader::LoadFromFile(normal_vfs_path.c_str(), normalData)) { + xr_vector normalRGBA = DecompressDDS(normalData, width, height); + if (!normalRGBA.empty()) { + WriteUncompressedDDS("$game_data$", "test_decompress_normal.dds", normalRGBA.data(), width, height); + } + } + } + + Msg("* [PBRTextureConverter] SANITY TEST: Complete - check gamedata/test_decompress_*.dds"); +} + +// ══════════════════════════════════════════════════════════ +// HELPER: Write/Update .thm file with PBR texture references +// ══════════════════════════════════════════════════════════ +// Creates or updates a .thm file to include PBR texture names +// This allows TextureDescrManager to find PBR textures via normal lookup + +static bool WriteTHMFile( + const char* root_alias, + const xr_string& base_name, + const xr_string& metallic_name, + const xr_string& roughness_name, + const xr_string& ao_name, + const xr_string& parallax_name) +{ + xr_string thm_path = base_name + ".thm"; + + // Try to load existing .thm file + STextureParams params; + params.Clear(); + + string_path full_path; + FS.update_path(full_path, root_alias, thm_path.c_str()); + + Msg("* [PBRTextureConverter] WriteTHMFile: Writing to %s (resolved: %s)", thm_path.c_str(), full_path); + + if (FS.exist(full_path)) { + // Load existing .thm to preserve other settings + IReader* reader = FS.r_open(root_alias, thm_path.c_str()); + if (reader) { + // Skip THM_CHUNK_VERSION and THM_CHUNK_DATA (thumbnail) + if (reader->find_chunk(THM_CHUNK_VERSION)) { + // Version chunk exists, skip it + } + if (reader->find_chunk(THM_CHUNK_DATA)) { + // Thumbnail data exists, skip it + } + // Load texture params + params.Load(*reader); + FS.r_close(reader); + Msg("* [PBRTextureConverter] Loaded existing .thm, updating PBR fields"); + } + } else { + Msg("* [PBRTextureConverter] Creating new .thm file"); + // Set default type for new .thm files + params.type = STextureParams::ttImage; + } + + // Update PBR texture names + params.metallic_name = metallic_name.c_str(); + params.roughness_name = roughness_name.c_str(); + params.ao_name = ao_name.c_str(); + params.parallax_name = parallax_name.c_str(); + + // Write updated .thm file + IWriter* writer = FS.w_open(root_alias, thm_path.c_str()); + if (!writer) { + Msg("! [PBRTextureConverter] Failed to open .thm for writing: %s/%s (full: %s)", + root_alias, thm_path.c_str(), full_path); + return false; + } + + // Write version chunk + writer->open_chunk(THM_CHUNK_VERSION); + writer->w_u16(1); // Version 1 + writer->close_chunk(); + + // Write THM_CHUNK_TYPE - required by LoadTHM before params.Load() + writer->open_chunk(THM_CHUNK_TYPE); + writer->w_u32(0); // THM type (0 = texture) + writer->close_chunk(); + + // Write texture params (includes PBR names) + params.Save(*writer); + + FS.w_close(writer); + Msg("* [PBRTextureConverter] Successfully wrote .thm: %s", full_path); + return true; +} + +template +class BoundedQueue { +public: + explicit BoundedQueue(size_t max_size) : max_size_(max_size) {} + + void push(T item) { + std::unique_lock lock(mutex_); + not_full_.wait(lock, [this] { return queue_.size() < max_size_ || done_; }); + if (done_) return; + queue_.push(std::move(item)); + not_empty_.notify_one(); + } + + bool pop(T& item) { + std::unique_lock lock(mutex_); + not_empty_.wait(lock, [this] { return !queue_.empty() || done_; }); + if (queue_.empty()) return false; + item = std::move(queue_.front()); + queue_.pop(); + not_full_.notify_one(); + return true; + } + + void signal_done() { + std::unique_lock lock(mutex_); + done_ = true; + not_empty_.notify_all(); + not_full_.notify_all(); + } + +private: + std::queue queue_; + std::mutex mutex_; + std::condition_variable not_empty_; + std::condition_variable not_full_; + size_t max_size_; + bool done_ = false; +}; + +struct PreparedTexture { + xr_string base_name; + xr_string pbr_path; + xr_string pbr_name; + xr_string albedo_path; + resources::DDSData diffuseData; + resources::DDSData normalData; +}; + +struct ConvertedOutput { + xr_string base_name; + xr_string pbr_path; + xr_string pbr_name; + xr_string albedo_path; + ConvertedPBRTextures converted; + bool generate_mipmaps; + xr_string output_root; +}; + +bool ConvertTexturesToPBR( + const TextureInventory& inventory, + const PBRConversionParams& params, + PBRConversionStats& out_stats, + ProgressCallback progress_callback) +{ + if (inventory.assets.empty()) { + return true; + } + +#ifdef USE_AI_PBR + InitializeAIPipeline(); +#endif + + const u32 total_textures = static_cast(inventory.assets.size()); + + std::atomic converted_count{0}; + std::atomic skipped_count{0}; + std::atomic failed_count{0}; +#ifdef USE_AI_PBR + std::atomic ai_conversions{0}; +#endif + + const auto start_time = std::chrono::high_resolution_clock::now(); + + xr_vector sorted_assets(inventory.assets.begin(), inventory.assets.end()); + std::sort(sorted_assets.begin(), sorted_assets.end(), + [](const LegacyTextureAsset& a, const LegacyTextureAsset& b) { + if (a.diffuse_width != b.diffuse_width) + return a.diffuse_width < b.diffuse_width; + if (a.diffuse_height != b.diffuse_height) + return a.diffuse_height < b.diffuse_height; + return a.diffuse.file_size < b.diffuse.file_size; + }); + + Msg("[PBRTextureConverter] Converting %u textures (pipelined, sorted by dimensions)...", total_textures); + + auto& ui_progress = ConversionProgress::Get(); + ui_progress.BeginPhase("Converting textures", total_textures); + +#ifndef USE_AI_PBR + Msg("! [PBRTextureConverter] USE_AI_PBR not defined, cannot convert any textures"); + out_stats.textures_scanned = total_textures; + out_stats.textures_skipped = total_textures; + return true; +#else + if (!g_ai_available) { + Msg("! [PBRTextureConverter] AI pipeline not available, cannot convert any textures"); + out_stats.textures_scanned = total_textures; + out_stats.textures_skipped = total_textures; + return true; + } + + { + xr_vector> unique_dims; + for (const auto& asset : sorted_assets) { + if (asset.diffuse_width == 0 || asset.diffuse_height == 0) + continue; + std::pair dim = { asset.diffuse_width, asset.diffuse_height }; + if (unique_dims.empty() || unique_dims.back() != dim) + unique_dims.push_back(dim); + } + + if (!unique_dims.empty()) { + ScopeLock lock{ &g_ai_pipeline_mutex }; + g_ai_pipeline->WarmupTRTEngines(unique_dims); + } + } + + BoundedQueue prepared_queue(2); + BoundedQueue write_queue(2); + + auto StripDDSExtension = [](const xr_string& path) -> xr_string { + if (path.size() >= 4 && path.substr(path.size() - 4) == ".dds") { + return path.substr(0, path.size() - 4); + } + return path; + }; + + std::thread producer([&]() { + for (size_t idx = 0; idx < sorted_assets.size(); ++idx) { + LegacyTextureAsset asset = sorted_assets[idx]; + + if (idx % 10 == 0 || idx == 0) { + Msg("~ [PBRTextureConverter] Preparing texture %u/%u: %s", + static_cast(idx + 1), total_textures, asset.base_name.c_str()); + } + + xr_string pbr_path = asset.base_name + "_pbr.dds"; + xr_string pbr_name = asset.base_name + "_pbr"; + + bool pbrExists = FileExists(params.output_root.c_str(), pbr_path.c_str()); + bool thmUpToDate = !THMNeedsPBRUpdate(params.output_root.c_str(), asset.base_name, pbr_name); + + if (pbrExists && thmUpToDate) { + skipped_count++; + ui_progress.OnSkipped(); + continue; + } + + if (pbrExists && !thmUpToDate) { + WriteTHMFileConsolidated(params.output_root.c_str(), asset.base_name, pbr_name); + skipped_count++; + ui_progress.OnSkipped(); + continue; + } + + resources::DDSData diffuseData; + xr_string diffuse_vfs_path = StripDDSExtension(asset.diffuse.relative_path); + if (!resources::DDSLoader::LoadFromFile(diffuse_vfs_path.c_str(), diffuseData)) { + Msg("! [PBRTextureConverter] Failed to load diffuse: %s", asset.diffuse.relative_path.c_str()); + failed_count++; + ui_progress.OnFailed(); + continue; + } + + if (!asset.has_normal) { + Msg("~ [PBRTextureConverter] No normal map for %s, generating from diffuse...", asset.base_name.c_str()); + u32 diffuseWidth, diffuseHeight; + xr_vector diffuseRGBA = DecompressDDS(diffuseData, diffuseWidth, diffuseHeight); + + if (!diffuseRGBA.empty()) { + xr_vector generatedNormalRGBA; + if (GenerateNormalMapFromDiffuse(diffuseRGBA, diffuseWidth, diffuseHeight, generatedNormalRGBA, 4.0f)) { + xr_string bump_path = asset.base_name + "_bump.dds"; + if (WriteNormalMapDDS(params.output_root.c_str(), bump_path, generatedNormalRGBA, diffuseWidth, diffuseHeight)) { + asset.has_normal = true; + asset.normal.root_alias = params.output_root; + asset.normal.relative_path = bump_path; + Msg("* [PBRTextureConverter] Generated normal map saved: %s", bump_path.c_str()); + } + } + } + } + + if (!asset.has_normal) { + Msg("~ [PBRTextureConverter] Skipping %s (no normal map available)", asset.base_name.c_str()); + skipped_count++; + ui_progress.OnSkipped(); + continue; + } + + resources::DDSData normalData; + xr_string normal_vfs_path = StripDDSExtension(asset.normal.relative_path); + if (!resources::DDSLoader::LoadFromFile(normal_vfs_path.c_str(), normalData)) { + Msg("! [PBRTextureConverter] Failed to load normal: %s", asset.normal.relative_path.c_str()); + failed_count++; + ui_progress.OnFailed(); + continue; + } + + Msg("* [PBRTextureConverter] PBR textures missing, running conversion: %s", asset.base_name.c_str()); + + PreparedTexture prepared; + prepared.base_name = asset.base_name; + prepared.pbr_path = std::move(pbr_path); + prepared.pbr_name = std::move(pbr_name); + prepared.albedo_path = asset.base_name + ".dds"; + prepared.diffuseData = std::move(diffuseData); + prepared.normalData = std::move(normalData); + prepared_queue.push(std::move(prepared)); + } + prepared_queue.signal_done(); + }); + + std::thread writer([&]() { + ConvertedOutput output; + while (write_queue.pop(output)) { + auto t_write_start = std::chrono::high_resolution_clock::now(); + + if (!output.converted.albedoData.empty()) { + if (!WriteRGBADDS(output.output_root.c_str(), output.albedo_path, + output.converted.albedoData.data(), + output.converted.width, output.converted.height, output.generate_mipmaps)) { + Msg("! [PBRTextureConverter] Failed to write albedo: %s", output.albedo_path.c_str()); + failed_count++; + ui_progress.OnFailed(); + continue; + } + } + + if (!WritePackedPBRDDS(output.output_root.c_str(), output.pbr_path, + output.converted.metallicData.data(), + output.converted.roughnessData.data(), + output.converted.aoData.empty() ? nullptr : output.converted.aoData.data(), + output.converted.parallaxData.empty() ? nullptr : output.converted.parallaxData.data(), + output.converted.width, output.converted.height, output.generate_mipmaps)) { + Msg("! [PBRTextureConverter] Failed to write packed PBR: %s", output.pbr_path.c_str()); + failed_count++; + ui_progress.OnFailed(); + continue; + } + + WriteTHMFileConsolidated(output.output_root.c_str(), output.base_name, output.pbr_name); + + auto t_write_end = std::chrono::high_resolution_clock::now(); + auto write_ms = std::chrono::duration_cast(t_write_end - t_write_start).count(); + Msg("* [PBRTextureConverter] Consolidated PBR: %s (%ux%u) [write %lldms]", + output.pbr_path.c_str(), output.converted.width, output.converted.height, write_ms); + + converted_count++; + ui_progress.OnConverted(); + + if (progress_callback) { + const float progress = static_cast(converted_count + skipped_count + failed_count) + / static_cast(total_textures); + xr_string status = "Converting "; + status.append(output.base_name); + status.append("..."); + progress_callback(progress, status.c_str()); + } + } + }); + + { + auto EmitResult = [&](Stage1Intermediate& inter, ConvertedPBRTextures&& converted, long long infer_ms) { + if (!converted.success) { + Msg("! [PBRTextureConverter] Conversion failed: %s [%lldms]", inter.base_name.c_str(), infer_ms); + failed_count++; + ui_progress.OnFailed(); + return; + } + + Msg("~ [PBRTextureConverter] AI inference done: %s (%ux%u) [%lldms]", + inter.base_name.c_str(), converted.width, converted.height, infer_ms); + + ZeroMetallicIfBlacklisted(converted, inter.base_name); + ai_conversions++; + + ConvertedOutput output; + output.base_name = std::move(inter.base_name); + output.pbr_path = std::move(inter.pbr_path); + output.pbr_name = std::move(inter.pbr_name); + output.albedo_path = std::move(inter.albedo_path); + output.converted = std::move(converted); + output.generate_mipmaps = params.generate_mipmaps; + output.output_root = params.output_root; + write_queue.push(std::move(output)); + }; + + auto FlushLargeBatch = [&](xr_vector& batch) { + if (batch.empty()) return; + + Msg("[PBRTextureConverter] Stage2 pass for %zu large textures (%ux%u)", + batch.size(), batch[0].width, batch[0].height); + + for (auto& inter : batch) { + auto t_start = std::chrono::high_resolution_clock::now(); + PBRPipelineOutputs outputs = g_ai_pipeline->ProcessStage2(inter.stage1); + auto t_end = std::chrono::high_resolution_clock::now(); + auto ms = std::chrono::duration_cast(t_end - t_start).count(); + + ConvertedPBRTextures converted; + if (outputs.success) + converted = PackOutputs(outputs, inter.originalDiffuseRGBA, inter.width, inter.height); + + EmitResult(inter, std::move(converted), ms); + } + batch.clear(); + }; + + xr_vector large_batch; + PreparedTexture prepared; + + while (prepared_queue.pop(prepared)) { + auto t_infer_start = std::chrono::high_resolution_clock::now(); + + ui_progress.SetCurrentItem(prepared.base_name.c_str()); + + DecompressedPair decompressed = DecompressAndPrepare(&prepared.diffuseData, &prepared.normalData); + if (!decompressed.success) { + Msg("! [PBRTextureConverter] Decompress failed: %s", prepared.base_name.c_str()); + failed_count++; + ui_progress.OnFailed(); + continue; + } + + Msg("~ [PBRTextureConverter] Calling Process with dimensions: width=%u, height=%u", + decompressed.width, decompressed.height); + + const bool large = g_ai_pipeline->NeedsSplitProcessing(decompressed.width, decompressed.height); + + if (!large_batch.empty() && + (large_batch[0].width != decompressed.width || large_batch[0].height != decompressed.height)) { + FlushLargeBatch(large_batch); + } + + if (!large) { + PBRPipelineOutputs outputs = g_ai_pipeline->Process( + decompressed.diffuseRGBA.data(), + decompressed.normalRGBA.data(), + decompressed.width, + decompressed.height + ); + + auto t_infer_end = std::chrono::high_resolution_clock::now(); + auto infer_ms = std::chrono::duration_cast(t_infer_end - t_infer_start).count(); + + ConvertedPBRTextures converted; + if (outputs.success) + converted = PackOutputs(outputs, decompressed.diffuseRGBA, decompressed.width, decompressed.height); + + Stage1Intermediate dummy; + dummy.base_name = std::move(prepared.base_name); + dummy.pbr_path = std::move(prepared.pbr_path); + dummy.pbr_name = std::move(prepared.pbr_name); + dummy.albedo_path = std::move(prepared.albedo_path); + EmitResult(dummy, std::move(converted), infer_ms); + } else { + PBRPipeline::Stage1Result stage1 = g_ai_pipeline->ProcessStage1( + decompressed.diffuseRGBA.data(), + decompressed.normalRGBA.data(), + decompressed.width, + decompressed.height + ); + + if (!stage1.success) { + auto t_infer_end = std::chrono::high_resolution_clock::now(); + auto infer_ms = std::chrono::duration_cast(t_infer_end - t_infer_start).count(); + Msg("! [PBRTextureConverter] Stage1 failed: %s [%lldms]", prepared.base_name.c_str(), infer_ms); + failed_count++; + ui_progress.OnFailed(); + continue; + } + + Stage1Intermediate inter; + inter.stage1 = std::move(stage1); + inter.originalDiffuseRGBA = std::move(decompressed.diffuseRGBA); + inter.width = decompressed.width; + inter.height = decompressed.height; + inter.base_name = std::move(prepared.base_name); + inter.pbr_path = std::move(prepared.pbr_path); + inter.pbr_name = std::move(prepared.pbr_name); + inter.albedo_path = std::move(prepared.albedo_path); + large_batch.push_back(std::move(inter)); + } + } + + FlushLargeBatch(large_batch); + + write_queue.signal_done(); + } + + producer.join(); + writer.join(); +#endif + + const auto end_time = std::chrono::high_resolution_clock::now(); + const auto duration = std::chrono::duration_cast(end_time - start_time); + + out_stats.textures_scanned = total_textures; + out_stats.textures_converted = converted_count.load(); + out_stats.textures_skipped = skipped_count.load(); + out_stats.textures_failed = failed_count.load(); + out_stats.conversion_time_seconds = duration.count() / 1000.0f; + + Msg("[PBRTextureConverter] Conversion complete: %u converted, %u skipped, %u failed (%.2fs)", + out_stats.textures_converted, + out_stats.textures_skipped, + out_stats.textures_failed, + out_stats.conversion_time_seconds); + + return out_stats.textures_failed == 0; +} + +// ══════════════════════════════════════════════════════════ +// CONSOLIDATE PBR TEXTURES (Pack separate files into _pbr.dds) +// ══════════════════════════════════════════════════════════ +// Scans for existing _metallic.dds, _roughness.dds, _ao.dds, _parallax.dds +// Packs them into a single _pbr.dds (R=metallic, G=roughness, B=ao, A=parallax) +// Updates .thm to use pbr_name field, deletes old separate files + +// Helper to load single-channel DDS into byte array +static xr_vector LoadSingleChannelDDS(const char* root_alias, const xr_string& path, u32& outWidth, u32& outHeight) +{ + xr_vector result; + outWidth = outHeight = 0; + + // Strip .dds extension for DDSLoader + xr_string vfs_path = path; + if (vfs_path.size() >= 4 && vfs_path.substr(vfs_path.size() - 4) == ".dds") { + vfs_path = vfs_path.substr(0, vfs_path.size() - 4); + } + + resources::DDSData ddsData; + if (!resources::DDSLoader::LoadFromFile(vfs_path.c_str(), ddsData)) { + return result; + } + + // Decompress to RGBA + xr_vector rgba = DecompressDDS(ddsData, outWidth, outHeight); + if (rgba.empty()) { + return result; + } + + // Extract single channel (R) - our single-channel DDS uses R8 or RGBA with data in R + const u32 pixelCount = outWidth * outHeight; + result.resize(pixelCount); + + // Check if it's actually single channel or RGBA + if (rgba.size() == pixelCount) { + // Already single channel + result = std::move(rgba); + } else if (rgba.size() == pixelCount * 4) { + // RGBA - extract R channel + for (u32 i = 0; i < pixelCount; ++i) { + result[i] = rgba[i * 4]; // R channel + } + } + + return result; +} + +// Helper to update .thm with consolidated pbr_name +static bool WriteTHMFileConsolidated( + const char* root_alias, + const xr_string& base_name, + const xr_string& pbr_name) +{ + xr_string thm_path = base_name + ".thm"; + + STextureParams params; + params.Clear(); + + string_path full_path; + FS.update_path(full_path, root_alias, thm_path.c_str()); + + if (FS.exist(full_path)) { + IReader* reader = FS.r_open(root_alias, thm_path.c_str()); + if (reader) { + if (reader->find_chunk(THM_CHUNK_VERSION)) {} + if (reader->find_chunk(THM_CHUNK_DATA)) {} + params.Load(*reader); + FS.r_close(reader); + } + } else { + params.type = STextureParams::ttImage; + } + + // Clear old separate PBR fields, set consolidated pbr_name + params.metallic_name = ""; + params.roughness_name = ""; + params.ao_name = ""; + params.parallax_name = ""; + params.pbr_name = pbr_name.c_str(); + + IWriter* writer = FS.w_open(root_alias, thm_path.c_str()); + if (!writer) { + return false; + } + + writer->open_chunk(THM_CHUNK_VERSION); + writer->w_u16(1); + writer->close_chunk(); + + writer->open_chunk(THM_CHUNK_TYPE); + writer->w_u32(0); + writer->close_chunk(); + + params.Save(*writer); + FS.w_close(writer); + + return true; +} + +// Helper to delete a file +static bool DeleteFile(const char* root_alias, const xr_string& path) +{ + string_path full_path; + FS.update_path(full_path, root_alias, path.c_str()); + + if (!FS.exist(full_path)) { + return true; // Already gone + } + + // Use standard file deletion + return (std::remove(full_path) == 0); +} + +bool ConsolidatePBRTextures( + const xr_string& root_alias, + ConsolidationStats& out_stats, + ProgressCallback progress_callback) +{ + Msg("[PBRTextureConverter] Starting PBR texture consolidation..."); + + // Scan for _metallic.dds files (indicates PBR textures exist) + FS_FileSet metallic_files; + FS.file_list(metallic_files, root_alias.c_str(), FS_ListFiles, "*_metallic.dds"); + + out_stats.textures_found = static_cast(metallic_files.size()); + Msg("[PBRTextureConverter] Found %u texture sets to consolidate", out_stats.textures_found); + + if (metallic_files.empty()) { + return true; + } + + auto& ui_progress = ConversionProgress::Get(); + ui_progress.BeginPhase("Consolidating PBR textures", out_stats.textures_found); + + u32 processed = 0; + for (const auto& file : metallic_files) { + // Extract base name from metallic path + xr_string metallic_path = file.name.c_str(); + xr_string base_name = metallic_path.substr(0, metallic_path.size() - 13); // Remove "_metallic.dds" + + ui_progress.SetCurrentItem(base_name.c_str()); + + // Build paths for all PBR textures + xr_string roughness_path = base_name + "_roughness.dds"; + xr_string ao_path = base_name + "_ao.dds"; + xr_string parallax_path = base_name + "_parallax.dds"; + xr_string pbr_path = base_name + "_pbr.dds"; + + // Check if already consolidated + string_path pbr_full_path; + FS.update_path(pbr_full_path, root_alias.c_str(), pbr_path.c_str()); + if (FS.exist(pbr_full_path)) { + processed++; + ui_progress.OnSkipped(); + continue; // Already done + } + + // Load existing separate textures + u32 width = 0, height = 0; + u32 tempW, tempH; + + xr_vector metallicData = LoadSingleChannelDDS(root_alias.c_str(), metallic_path, width, height); + if (metallicData.empty()) { + Msg("! [PBRTextureConverter] Failed to load metallic: %s", metallic_path.c_str()); + out_stats.textures_failed++; + ui_progress.OnFailed(); + continue; + } + + xr_vector roughnessData = LoadSingleChannelDDS(root_alias.c_str(), roughness_path, tempW, tempH); + if (roughnessData.empty() || tempW != width || tempH != height) { + Msg("! [PBRTextureConverter] Failed to load roughness or size mismatch: %s", roughness_path.c_str()); + out_stats.textures_failed++; + ui_progress.OnFailed(); + continue; + } + + xr_vector aoData = LoadSingleChannelDDS(root_alias.c_str(), ao_path, tempW, tempH); + if (aoData.empty() || tempW != width || tempH != height) { + Msg("! [PBRTextureConverter] Failed to load AO or size mismatch: %s", ao_path.c_str()); + out_stats.textures_failed++; + ui_progress.OnFailed(); + continue; + } + + // Parallax is optional + xr_vector parallaxData = LoadSingleChannelDDS(root_alias.c_str(), parallax_path, tempW, tempH); + if (!parallaxData.empty() && (tempW != width || tempH != height)) { + parallaxData.clear(); // Size mismatch, skip parallax + } + + // Pack and write consolidated _pbr.dds + if (!WritePackedPBRDDS(root_alias.c_str(), pbr_path, + metallicData.data(), + roughnessData.data(), + aoData.data(), + parallaxData.empty() ? nullptr : parallaxData.data(), + width, height, true)) { + Msg("! [PBRTextureConverter] Failed to write packed PBR: %s", pbr_path.c_str()); + out_stats.textures_failed++; + ui_progress.OnFailed(); + continue; + } + + Msg("* [PBRTextureConverter] Consolidated: %s (%ux%u)", pbr_path.c_str(), width, height); + + // Update .thm to use pbr_name + xr_string pbr_name = base_name + "_pbr"; + WriteTHMFileConsolidated(root_alias.c_str(), base_name, pbr_name); + + // Delete old separate files + if (DeleteFile(root_alias.c_str(), metallic_path)) out_stats.files_deleted++; + if (DeleteFile(root_alias.c_str(), roughness_path)) out_stats.files_deleted++; + if (DeleteFile(root_alias.c_str(), ao_path)) out_stats.files_deleted++; + if (!parallaxData.empty() && DeleteFile(root_alias.c_str(), parallax_path)) out_stats.files_deleted++; + + out_stats.textures_consolidated++; + processed++; + ui_progress.OnConverted(); + + // Progress callback + if (progress_callback) { + float progress = static_cast(processed) / static_cast(out_stats.textures_found); + xr_string status = "Consolidating " + base_name + "..."; + progress_callback(progress, status.c_str()); + } + } + + Msg("[PBRTextureConverter] Consolidation complete: %u consolidated, %u failed, %u files deleted", + out_stats.textures_consolidated, out_stats.textures_failed, out_stats.files_deleted); + + return out_stats.textures_failed == 0; +} + +bool ConvertSingleTextureToPBR( + const char* root_alias, + const char* relative_path, + const PBRConversionParams& params) +{ + if (!FileExists(root_alias, relative_path)) + { + Msg("! [PBRTextureConverter] Texture not found: %s/%s", root_alias, relative_path); + return false; + } + + xr_string rel(relative_path); + xr_string base_name = rel; + if (const auto dot = base_name.find_last_of('.'); dot != xr_string::npos) + base_name = base_name.substr(0, dot); + + xr_string pbr_path = base_name + "_pbr.dds"; + if (FileExists(root_alias, pbr_path.c_str())) + { + Msg("~ [PBRTextureConverter] %s already exists, skipping", pbr_path.c_str()); + return true; + } + + LegacyTextureAsset asset; + asset.base_name = base_name; + asset.diffuse.root_alias = root_alias; + asset.diffuse.relative_path = relative_path; + + TextureInventory inventory; + inventory.assets.push_back(std::move(asset)); + inventory.total_textures = 1; + + PBRConversionParams localParams = params; + localParams.output_root = root_alias; + + PBRConversionStats stats; + if (!ConvertTexturesToPBR(inventory, localParams, stats, nullptr)) + { + Msg("! [PBRTextureConverter] Conversion failed: %s", relative_path); + return false; + } + + ConsolidationStats consolidationStats; + ConsolidatePBRTextures(root_alias, consolidationStats, nullptr); + + return stats.textures_failed == 0; +} + +} // namespace xray::render::pbr diff --git a/src/Layers/xrRender/PBRConverter/PBRTextureConverter.h b/src/Layers/xrRender/PBRConverter/PBRTextureConverter.h new file mode 100644 index 00000000000..b6901830a7f --- /dev/null +++ b/src/Layers/xrRender/PBRConverter/PBRTextureConverter.h @@ -0,0 +1,220 @@ +// xrRender/PBR/PBRTextureConverter.h +// Phase 2.5: PBR Texture Conversion System +// +// PURPOSE: +// Automatically converts legacy X-Ray textures (diffuse/specular/gloss) +// to PBR workflow (basecolor/metallic/roughness) at startup when r4_use_pbr=1 +// +// DESIGN: +// - Scans $game_textures$ for diffuse textures (_d.dds suffix) +// - For each diffuse, looks for matching specular (_s.dds) and gloss (_g.dds) +// - Converts in parallel using xr_parallel_for (like OGF→OZZX conversion) +// - Generates NEW textures alongside originals: +// wood_d.dds → wood_d.dds (basecolor, converted in-place) +// wood_s.dds → wood_metallic.dds (NEW) +// wood_s.dds+gloss → wood_roughness.dds (NEW) +// - Digest-based caching: Only converts if source textures changed +// +// NAMING CONVENTION: +// - Keeps vanilla names intact (wood_d.dds stays wood_d.dds) +// - Adds new PBR-specific textures with descriptive suffixes +// - Shaders detect which mode via r4_use_pbr flag + +#pragma once + +#include "xrCore/xrCore.h" +#include "xrCore/_std_extensions.h" +#include "xrCore/Threading/ParallelFor.hpp" + +namespace xray::render::pbr { + +// ══════════════════════════════════════════════════════════ +// FILE LOCATION (From OGF conversion pattern) +// ══════════════════════════════════════════════════════════ + +struct FileLocation { + xr_string root_alias; // e.g., "$game_textures$" + xr_string relative_path; // e.g., "wood\wood_d.dds" + std::int64_t file_size = 0; + std::int64_t modified_time_seconds = 0; +}; + +// ══════════════════════════════════════════════════════════ +// LEGACY TEXTURE ASSET (Input) +// ══════════════════════════════════════════════════════════ +// Represents a set of legacy textures that need PBR conversion: +// - Diffuse (_d.dds) → Required +// - Specular (_s.dds) → Optional (fallback: derive from diffuse) +// - Gloss (_g.dds) → Optional (fallback: constant roughness) +// - Normal (_bump.dds) → Passthrough (no conversion needed) + +struct LegacyTextureAsset { + xr_string base_name; // "wood\wood" (without suffix) + + FileLocation diffuse; // wood_d.dds (required) + FileLocation specular; // wood_s.dds (optional) + FileLocation gloss; // wood_g.dds (optional) + FileLocation normal; // wood_bump.dds (optional, passthrough) + + bool has_specular = false; + bool has_gloss = false; + bool has_normal = false; + + u32 diffuse_width = 0; + u32 diffuse_height = 0; + + // For digest calculation (detect changes) + std::int64_t total_size_bytes = 0; + std::int64_t latest_modified_time = 0; +}; + +// ══════════════════════════════════════════════════════════ +// PBR TEXTURE OUTPUTS (Generated) +// ══════════════════════════════════════════════════════════ + +struct PBRTextureOutputs { + xr_string basecolor_path; // wood_d.dds (converted in-place) + xr_string metallic_path; // wood_metallic.dds (NEW) + xr_string roughness_path; // wood_roughness.dds (NEW) + xr_string ao_path; // wood_ao.dds (NEW, optional) + xr_string normal_path; // wood_bump.dds (passthrough) + + bool needs_conversion = true; // False if outputs already exist and are up-to-date +}; + +// ══════════════════════════════════════════════════════════ +// TEXTURE INVENTORY (All assets to convert) +// ══════════════════════════════════════════════════════════ + +struct TextureInventory { + xr_vector assets; + + u32 total_textures = 0; + u32 textures_with_specular = 0; + u32 textures_with_gloss = 0; + std::int64_t total_size_bytes = 0; +}; + +// ══════════════════════════════════════════════════════════ +// SCAN CONFIGURATION +// ══════════════════════════════════════════════════════════ + +struct TextureScanConfig { + xr_vector texture_roots = { "$game_textures$" }; + bool include_levels = true; // Also scan $level$ for per-level textures + bool recursive = true; +}; + +// ══════════════════════════════════════════════════════════ +// CONVERSION PARAMETERS +// ══════════════════════════════════════════════════════════ + +struct PBRConversionParams { + xr_string output_root = "$game_textures$"; // Where to write converted textures + + // Conversion quality settings + bool generate_mipmaps = true; + bool compress_output = true; // Use DXT compression + + // Fallback values when source textures missing + float default_metallic = 0.0f; // Non-metallic by default + float default_roughness = 0.5f; // Mid-range roughness + float default_ao = 1.0f; // No occlusion by default + + // Performance + u32 max_parallel_conversions = 0; // 0 = use all cores +}; + +// ══════════════════════════════════════════════════════════ +// CONVERSION STATISTICS +// ══════════════════════════════════════════════════════════ + +struct PBRConversionStats { + u32 textures_scanned = 0; + u32 textures_converted = 0; + u32 textures_skipped = 0; // Already up-to-date + u32 textures_failed = 0; + + std::int64_t bytes_read = 0; + std::int64_t bytes_written = 0; + + float conversion_time_seconds = 0.0f; +}; + +// ══════════════════════════════════════════════════════════ +// PROGRESS CALLBACK +// ══════════════════════════════════════════════════════════ + +using ProgressCallback = std::function; + +// ══════════════════════════════════════════════════════════ +// PUBLIC API +// ══════════════════════════════════════════════════════════ + +// Scan filesystem for legacy textures needing conversion +TextureInventory BuildTextureInventory(const TextureScanConfig& config); + +// Compute digest (hash) of inventory for change detection +// Returns hex string like "a3f5c9..." (similar to OGF digest) +xr_string ComputeTextureInventoryDigest(const TextureInventory& inventory); + +// Check if PBR outputs exist and are up-to-date +bool VerifyPBROutputs( + const TextureInventory& inventory, + const PBRConversionParams& params +); + +// Convert all textures in inventory (parallel processing) +bool ConvertTexturesToPBR( + const TextureInventory& inventory, + const PBRConversionParams& params, + PBRConversionStats& out_stats, + ProgressCallback progress_callback = nullptr +); + +// ══════════════════════════════════════════════════════════ +// PBR TEXTURE CONSOLIDATION +// ══════════════════════════════════════════════════════════ +// Packs existing separate PBR textures (_metallic, _roughness, _ao, _parallax) +// into a single _pbr.dds texture (R=M, G=R, B=AO, A=Parallax) + +struct ConsolidationStats { + u32 textures_found = 0; + u32 textures_consolidated = 0; + u32 textures_failed = 0; + u32 files_deleted = 0; +}; + +// Convert a single texture to PBR at the given VFS location +// Produces _pbr.dds (packed R=M, G=R, B=AO, A=Parallax) alongside the original +// Returns true if conversion succeeded or output already exists +bool ConvertSingleTextureToPBR( + const char* root_alias, + const char* relative_path, + const PBRConversionParams& params +); + +// Consolidate existing separate PBR textures into packed _pbr.dds +// Also updates .thm files and deletes old separate files +bool ConsolidatePBRTextures( + const xr_string& root_alias, + ConsolidationStats& out_stats, + ProgressCallback progress_callback = nullptr +); + +// ══════════════════════════════════════════════════════════ +// HELPER: Build default inventory (common case) +// ══════════════════════════════════════════════════════════ + +inline TextureInventory BuildDefaultTextureInventory() { + TextureScanConfig config; + config.texture_roots = { "$game_textures$" }; + config.include_levels = true; + config.recursive = true; + return BuildTextureInventory(config); +} + +} // namespace xray::render::pbr From c6bf9ce5e3339d0035620c5d4a2bc7f17211b170 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:40:15 +0300 Subject: [PATCH 002/261] build(deps): bump Externals/imgui from `3109131` to `4fa59df` (#1994) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 3109131a882..4fa59df9fa2 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 3109131a882daec56a530aff540416983c240443 +Subproject commit 4fa59df9fa20e4e2212070c5b1bc6cc52237093f From de0b71715ffaf418274c35d86cad5c6ee38922dc Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 18 Nov 2025 23:30:45 +0300 Subject: [PATCH 003/261] Fix crash on engine start during script export (closes #1995) --- src/xrGame/MainMenu.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrGame/MainMenu.h b/src/xrGame/MainMenu.h index 2aadfcabf7e..8c10d5f0d0c 100644 --- a/src/xrGame/MainMenu.h +++ b/src/xrGame/MainMenu.h @@ -202,7 +202,7 @@ class CMainMenu : public IMainMenu, CEventNotifierCallback::CID m_script_reset_event_cid; private: - DECLARE_SCRIPT_REGISTER_FUNCTION(CDialogHolder); + DECLARE_SCRIPT_REGISTER_FUNCTION(CDialogHolder, CUIDialogWnd, CUIWindow); }; extern CMainMenu* MainMenu(); From da5fa3b5a8bcc1076c700f872cc751eefa907f37 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 18 Nov 2025 23:30:45 +0300 Subject: [PATCH 004/261] Fix crash on engine start during script export (closes #1995) --- src/xrGame/MainMenu.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrGame/MainMenu.h b/src/xrGame/MainMenu.h index 2aadfcabf7e..8c10d5f0d0c 100644 --- a/src/xrGame/MainMenu.h +++ b/src/xrGame/MainMenu.h @@ -202,7 +202,7 @@ class CMainMenu : public IMainMenu, CEventNotifierCallback::CID m_script_reset_event_cid; private: - DECLARE_SCRIPT_REGISTER_FUNCTION(CDialogHolder); + DECLARE_SCRIPT_REGISTER_FUNCTION(CDialogHolder, CUIDialogWnd, CUIWindow); }; extern CMainMenu* MainMenu(); From 25a82a11461e184c06b9aee14b8a5068ac813acf Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 20 Nov 2025 15:13:52 +0300 Subject: [PATCH 005/261] Fix another crash on engine start during script export --- src/xrGame/StalkerOutfit.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrGame/StalkerOutfit.h b/src/xrGame/StalkerOutfit.h index 7a759f0ddc8..9abfdc2df0f 100644 --- a/src/xrGame/StalkerOutfit.h +++ b/src/xrGame/StalkerOutfit.h @@ -17,5 +17,5 @@ class CStalkerOutfit : public CCustomOutfit virtual ~CStalkerOutfit(void); private: - DECLARE_SCRIPT_REGISTER_FUNCTION(); + DECLARE_SCRIPT_REGISTER_FUNCTION(CGameObject); }; From a85f83c03b716a3ee2250deb59d9dc4c656189e0 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 20 Nov 2025 15:13:52 +0300 Subject: [PATCH 006/261] Fix another crash on engine start during script export --- src/xrGame/StalkerOutfit.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrGame/StalkerOutfit.h b/src/xrGame/StalkerOutfit.h index 7a759f0ddc8..9abfdc2df0f 100644 --- a/src/xrGame/StalkerOutfit.h +++ b/src/xrGame/StalkerOutfit.h @@ -17,5 +17,5 @@ class CStalkerOutfit : public CCustomOutfit virtual ~CStalkerOutfit(void); private: - DECLARE_SCRIPT_REGISTER_FUNCTION(); + DECLARE_SCRIPT_REGISTER_FUNCTION(CGameObject); }; From c223e52487542b1e328ea8c75d8b4efc42950028 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 20 Nov 2025 15:14:45 +0300 Subject: [PATCH 007/261] xrGame/StalkerOutfit.h|cpp: removed unnecessary empty constructor and destructor And other stuff --- src/xrGame/StalkerOutfit.cpp | 4 ---- src/xrGame/StalkerOutfit.h | 8 ++------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/xrGame/StalkerOutfit.cpp b/src/xrGame/StalkerOutfit.cpp index 1272f49bbdd..2e1ae2d81a5 100644 --- a/src/xrGame/StalkerOutfit.cpp +++ b/src/xrGame/StalkerOutfit.cpp @@ -1,10 +1,6 @@ #include "pch_script.h" #include "StalkerOutfit.h" -#include "ActorHelmet.h" - -CStalkerOutfit::CStalkerOutfit() {} -CStalkerOutfit::~CStalkerOutfit() {} void CStalkerOutfit::script_register(lua_State* luaState) { diff --git a/src/xrGame/StalkerOutfit.h b/src/xrGame/StalkerOutfit.h index 9abfdc2df0f..41e24b40680 100644 --- a/src/xrGame/StalkerOutfit.h +++ b/src/xrGame/StalkerOutfit.h @@ -9,12 +9,8 @@ class CStalkerOutfit : public CCustomOutfit { -private: - typedef CCustomOutfit inherited; - -public: - CStalkerOutfit(void); - virtual ~CStalkerOutfit(void); +protected: + using inherited = CCustomOutfit; private: DECLARE_SCRIPT_REGISTER_FUNCTION(CGameObject); From 6ea77a67d0fe990518e604e035ad147d20b68038 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 20 Nov 2025 15:14:45 +0300 Subject: [PATCH 008/261] xrGame/StalkerOutfit.h|cpp: removed unnecessary empty constructor and destructor And other stuff --- src/xrGame/StalkerOutfit.cpp | 4 ---- src/xrGame/StalkerOutfit.h | 8 ++------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/xrGame/StalkerOutfit.cpp b/src/xrGame/StalkerOutfit.cpp index 1272f49bbdd..2e1ae2d81a5 100644 --- a/src/xrGame/StalkerOutfit.cpp +++ b/src/xrGame/StalkerOutfit.cpp @@ -1,10 +1,6 @@ #include "pch_script.h" #include "StalkerOutfit.h" -#include "ActorHelmet.h" - -CStalkerOutfit::CStalkerOutfit() {} -CStalkerOutfit::~CStalkerOutfit() {} void CStalkerOutfit::script_register(lua_State* luaState) { diff --git a/src/xrGame/StalkerOutfit.h b/src/xrGame/StalkerOutfit.h index 9abfdc2df0f..41e24b40680 100644 --- a/src/xrGame/StalkerOutfit.h +++ b/src/xrGame/StalkerOutfit.h @@ -9,12 +9,8 @@ class CStalkerOutfit : public CCustomOutfit { -private: - typedef CCustomOutfit inherited; - -public: - CStalkerOutfit(void); - virtual ~CStalkerOutfit(void); +protected: + using inherited = CCustomOutfit; private: DECLARE_SCRIPT_REGISTER_FUNCTION(CGameObject); From 93d6e29ae18b487bdff89dbffeb7309cd8a3d07c Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 3 Dec 2025 15:39:04 +0300 Subject: [PATCH 009/261] Clamp denormalized sounds (fixes #1819) --- src/xrSound/SoundRender_Source.cpp | 2 +- src/xrSound/SoundRender_Source.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xrSound/SoundRender_Source.cpp b/src/xrSound/SoundRender_Source.cpp index bb3c060537a..31d505f7055 100644 --- a/src/xrSound/SoundRender_Source.cpp +++ b/src/xrSound/SoundRender_Source.cpp @@ -89,7 +89,7 @@ void CSoundRender_Source::i_decompress(OggVorbis_File* ovf, float* _dest, u32 si for (long j = 0; j < samples; j++) for (long i = 0; i < m_data_info.channels; i++) - *_dest++ = pcm[i][j]; + *_dest++ = clampr(pcm[i][j], -1.0f, 1.0f); left -= samples; } diff --git a/src/xrSound/SoundRender_Source.h b/src/xrSound/SoundRender_Source.h index 3c3c4a65a44..ae6263823a0 100644 --- a/src/xrSound/SoundRender_Source.h +++ b/src/xrSound/SoundRender_Source.h @@ -42,7 +42,7 @@ class XRSOUND_API CSoundRender_Source final : public CSound_source private: void i_decompress(OggVorbis_File* ovf, char* dest, u32 size) const; - void i_decompress(OggVorbis_File* ovf, float* dest, u32 size) const; + void i_decompress(OggVorbis_File* ovf, float* dest, u32 size) const; // this overload clamps denormalized sounds bool LoadWave(pcstr name); From 070a56809608a66797d0f1b66735573083976006 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 3 Dec 2025 15:39:04 +0300 Subject: [PATCH 010/261] Clamp denormalized sounds (fixes #1819) --- src/xrSound/SoundRender_Source.cpp | 2 +- src/xrSound/SoundRender_Source.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xrSound/SoundRender_Source.cpp b/src/xrSound/SoundRender_Source.cpp index bb3c060537a..31d505f7055 100644 --- a/src/xrSound/SoundRender_Source.cpp +++ b/src/xrSound/SoundRender_Source.cpp @@ -89,7 +89,7 @@ void CSoundRender_Source::i_decompress(OggVorbis_File* ovf, float* _dest, u32 si for (long j = 0; j < samples; j++) for (long i = 0; i < m_data_info.channels; i++) - *_dest++ = pcm[i][j]; + *_dest++ = clampr(pcm[i][j], -1.0f, 1.0f); left -= samples; } diff --git a/src/xrSound/SoundRender_Source.h b/src/xrSound/SoundRender_Source.h index 3c3c4a65a44..ae6263823a0 100644 --- a/src/xrSound/SoundRender_Source.h +++ b/src/xrSound/SoundRender_Source.h @@ -42,7 +42,7 @@ class XRSOUND_API CSoundRender_Source final : public CSound_source private: void i_decompress(OggVorbis_File* ovf, char* dest, u32 size) const; - void i_decompress(OggVorbis_File* ovf, float* dest, u32 size) const; + void i_decompress(OggVorbis_File* ovf, float* dest, u32 size) const; // this overload clamps denormalized sounds bool LoadWave(pcstr name); From a3e01ba4b95c7d42c5a55b8fd185b142d6d4cd31 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 3 Dec 2025 15:48:31 +0300 Subject: [PATCH 011/261] Enable 32-bit float sounds by default Since #1819 is fixed and there are no other known issues, we can enable it. --- src/xrSound/SoundRender_Core.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrSound/SoundRender_Core.cpp b/src/xrSound/SoundRender_Core.cpp index 6c96c0084bb..b6d1c2cd0de 100644 --- a/src/xrSound/SoundRender_Core.cpp +++ b/src/xrSound/SoundRender_Core.cpp @@ -14,7 +14,7 @@ XRSOUND_API Flags32 psSoundFlags = { - ss_Hardware | ss_EFX + ss_Hardware | ss_EFX | ss_UseFloat32 }; XRSOUND_API int psSoundTargets = 32; From 1d6b560300bfccccd74c4d18da664e35e19a6c32 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 3 Dec 2025 15:48:31 +0300 Subject: [PATCH 012/261] Enable 32-bit float sounds by default Since #1819 is fixed and there are no other known issues, we can enable it. --- src/xrSound/SoundRender_Core.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrSound/SoundRender_Core.cpp b/src/xrSound/SoundRender_Core.cpp index 6c96c0084bb..b6d1c2cd0de 100644 --- a/src/xrSound/SoundRender_Core.cpp +++ b/src/xrSound/SoundRender_Core.cpp @@ -14,7 +14,7 @@ XRSOUND_API Flags32 psSoundFlags = { - ss_Hardware | ss_EFX + ss_Hardware | ss_EFX | ss_UseFloat32 }; XRSOUND_API int psSoundTargets = 32; From 4457310377c14d61bada222241c53bfac86afbf3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:20:17 +0300 Subject: [PATCH 013/261] build(deps): bump Externals/sse2neon from `42eee28` to `68ec5cb` (#2003) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2neon b/Externals/sse2neon index 42eee28683a..68ec5cb625e 160000 --- a/Externals/sse2neon +++ b/Externals/sse2neon @@ -1 +1 @@ -Subproject commit 42eee28683af2c31dafcc37315f9ba400003754f +Subproject commit 68ec5cb625e473e40533129ce0e0f0fa6952706b From 5632a78bfa7bc5219f6f9ac9e89784e897a3113e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:20:17 +0300 Subject: [PATCH 014/261] build(deps): bump Externals/sse2neon from `42eee28` to `68ec5cb` (#2003) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2neon b/Externals/sse2neon index 42eee28683a..68ec5cb625e 160000 --- a/Externals/sse2neon +++ b/Externals/sse2neon @@ -1 +1 @@ -Subproject commit 42eee28683af2c31dafcc37315f9ba400003754f +Subproject commit 68ec5cb625e473e40533129ce0e0f0fa6952706b From d7cef6f00738c27f269e9348b793847e17b393af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:21:01 +0300 Subject: [PATCH 015/261] build(deps): bump Externals/imgui from `4fa59df` to `6327b63` (#2004) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 4fa59df9fa2..6327b630645 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 4fa59df9fa20e4e2212070c5b1bc6cc52237093f +Subproject commit 6327b6306459e8d8a5919baea538eb845f82b19d From 843c3fe5624dd7a8fcb30740418c5ed9d607bd21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:21:01 +0300 Subject: [PATCH 016/261] build(deps): bump Externals/imgui from `4fa59df` to `6327b63` (#2004) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 4fa59df9fa2..6327b630645 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 4fa59df9fa20e4e2212070c5b1bc6cc52237093f +Subproject commit 6327b6306459e8d8a5919baea538eb845f82b19d From ad75a3e36dfae274c80489c3a00d75485989e5e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:26:22 +0300 Subject: [PATCH 017/261] build(deps): bump actions/checkout from 5 to 6 (#1998) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/mirror.yml | 2 +- .github/workflows/stylecheck.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 182b6eb9429..1311adf8f3f 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -7,7 +7,7 @@ jobs: if: github.repository == 'OpenXRay/xray-16' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/stylecheck.yml b/.github/workflows/stylecheck.yml index b68c4eb2cef..8c28a678410 100644 --- a/.github/workflows/stylecheck.yml +++ b/.github/workflows/stylecheck.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install run: sudo apt-get install moreutils @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install run: sudo apt-get install dos2unix recode @@ -75,7 +75,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 From c3529213594ebad026d377886a2f3c0acae2ec47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:26:22 +0300 Subject: [PATCH 018/261] build(deps): bump actions/checkout from 5 to 6 (#1998) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/mirror.yml | 2 +- .github/workflows/stylecheck.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 182b6eb9429..1311adf8f3f 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -7,7 +7,7 @@ jobs: if: github.repository == 'OpenXRay/xray-16' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/stylecheck.yml b/.github/workflows/stylecheck.yml index b68c4eb2cef..8c28a678410 100644 --- a/.github/workflows/stylecheck.yml +++ b/.github/workflows/stylecheck.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install run: sudo apt-get install moreutils @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install run: sudo apt-get install dos2unix recode @@ -75,7 +75,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 From e85aae434fb7b850dd853cb61cd8a558590bd317 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Sat, 13 Dec 2025 10:50:20 +0100 Subject: [PATCH 019/261] fix: timestamp in PDA messages not being assigned correctly before UI display (#1989) --- src/xrGame/actor_communication.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/xrGame/actor_communication.cpp b/src/xrGame/actor_communication.cpp index 8bc5b7a4599..7f4e403afe1 100644 --- a/src/xrGame/actor_communication.cpp +++ b/src/xrGame/actor_communication.cpp @@ -114,13 +114,15 @@ void CActor::AddGameTask(const CInfoPortion* info_portion) const void CActor::AddGameNews(GAME_NEWS_DATA&& news_data) { + // Assign timestamp before passing the news to UI + news_data.receive_time = Level().GetGameTime(); + if (CurrentGameUI()) { CurrentGameUI()->UIMainIngameWnd->ReceiveNews(&news_data); } GAME_NEWS_VECTOR& news_vector = game_news_registry->registry().objects(); - news_data.receive_time = Level().GetGameTime(); news_vector.emplace_back(std::move(news_data)); } From 931bad1033cd9fdb8948f31a3dc6973d0da87296 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Sat, 13 Dec 2025 10:50:20 +0100 Subject: [PATCH 020/261] fix: timestamp in PDA messages not being assigned correctly before UI display (#1989) --- src/xrGame/actor_communication.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/xrGame/actor_communication.cpp b/src/xrGame/actor_communication.cpp index 8bc5b7a4599..7f4e403afe1 100644 --- a/src/xrGame/actor_communication.cpp +++ b/src/xrGame/actor_communication.cpp @@ -114,13 +114,15 @@ void CActor::AddGameTask(const CInfoPortion* info_portion) const void CActor::AddGameNews(GAME_NEWS_DATA&& news_data) { + // Assign timestamp before passing the news to UI + news_data.receive_time = Level().GetGameTime(); + if (CurrentGameUI()) { CurrentGameUI()->UIMainIngameWnd->ReceiveNews(&news_data); } GAME_NEWS_VECTOR& news_vector = game_news_registry->registry().objects(); - news_data.receive_time = Level().GetGameTime(); news_vector.emplace_back(std::move(news_data)); } From b8131ea792f17e090d351b7974bb0e939ee42195 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Sat, 13 Dec 2025 13:09:23 +0100 Subject: [PATCH 021/261] fix: No upgrades being available in CoC technician dialog (#1991) Co-authored-by: Xottab-DUTY --- src/xrGame/ui/UITalkDialogWnd.cpp | 2 -- src/xrGame/ui/UITalkWnd.cpp | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xrGame/ui/UITalkDialogWnd.cpp b/src/xrGame/ui/UITalkDialogWnd.cpp index f1062084b8c..97478b884d9 100644 --- a/src/xrGame/ui/UITalkDialogWnd.cpp +++ b/src/xrGame/ui/UITalkDialogWnd.cpp @@ -150,7 +150,6 @@ void CUITalkDialogWnd::InitTalkDialogWnd() void CUITalkDialogWnd::Show() { InventoryUtilities::SendInfoToActor("ui_talk_show"); - InventoryUtilities::SendInfoToLuaScripts("ui_talk_show"); inherited::Show(true); inherited::Enable(true); @@ -164,7 +163,6 @@ void CUITalkDialogWnd::Hide() if (UI().Focus().GetLocker() == UIQuestionsList) UI().Focus().Unlock(); InventoryUtilities::SendInfoToActor("ui_talk_hide"); - InventoryUtilities::SendInfoToLuaScripts("ui_talk_hide"); inherited::Show(false); inherited::Enable(false); g_btnHint->Discard(); diff --git a/src/xrGame/ui/UITalkWnd.cpp b/src/xrGame/ui/UITalkWnd.cpp index 6e20f333454..ce57eb257d1 100644 --- a/src/xrGame/ui/UITalkWnd.cpp +++ b/src/xrGame/ui/UITalkWnd.cpp @@ -235,11 +235,13 @@ void CUITalkWnd::Show(bool status) if (status) { InitTalkDialog(); + InventoryUtilities::SendInfoToLuaScripts("ui_talk_show"); } else { StopSnd(); UITalkDialogWnd->Hide(); + InventoryUtilities::SendInfoToLuaScripts("ui_talk_hide"); if (m_pActor) { From 6dce1a51d3fd4434dbb2d9816c4023ce1b504592 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Sat, 13 Dec 2025 13:09:23 +0100 Subject: [PATCH 022/261] fix: No upgrades being available in CoC technician dialog (#1991) Co-authored-by: Xottab-DUTY --- src/xrGame/ui/UITalkDialogWnd.cpp | 2 -- src/xrGame/ui/UITalkWnd.cpp | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xrGame/ui/UITalkDialogWnd.cpp b/src/xrGame/ui/UITalkDialogWnd.cpp index f1062084b8c..97478b884d9 100644 --- a/src/xrGame/ui/UITalkDialogWnd.cpp +++ b/src/xrGame/ui/UITalkDialogWnd.cpp @@ -150,7 +150,6 @@ void CUITalkDialogWnd::InitTalkDialogWnd() void CUITalkDialogWnd::Show() { InventoryUtilities::SendInfoToActor("ui_talk_show"); - InventoryUtilities::SendInfoToLuaScripts("ui_talk_show"); inherited::Show(true); inherited::Enable(true); @@ -164,7 +163,6 @@ void CUITalkDialogWnd::Hide() if (UI().Focus().GetLocker() == UIQuestionsList) UI().Focus().Unlock(); InventoryUtilities::SendInfoToActor("ui_talk_hide"); - InventoryUtilities::SendInfoToLuaScripts("ui_talk_hide"); inherited::Show(false); inherited::Enable(false); g_btnHint->Discard(); diff --git a/src/xrGame/ui/UITalkWnd.cpp b/src/xrGame/ui/UITalkWnd.cpp index 6e20f333454..ce57eb257d1 100644 --- a/src/xrGame/ui/UITalkWnd.cpp +++ b/src/xrGame/ui/UITalkWnd.cpp @@ -235,11 +235,13 @@ void CUITalkWnd::Show(bool status) if (status) { InitTalkDialog(); + InventoryUtilities::SendInfoToLuaScripts("ui_talk_show"); } else { StopSnd(); UITalkDialogWnd->Hide(); + InventoryUtilities::SendInfoToLuaScripts("ui_talk_hide"); if (m_pActor) { From b5851781553bba0ec5e69cdbf396f39d52564c7c Mon Sep 17 00:00:00 2001 From: AMS21 Date: Sun, 14 Dec 2025 15:39:01 +0100 Subject: [PATCH 023/261] fix: handling invalid boxes in `CHOM::visible` (#1979) --- src/Layers/xrRender/HOM.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Layers/xrRender/HOM.cpp b/src/Layers/xrRender/HOM.cpp index 9983bf16054..9322999a058 100644 --- a/src/Layers/xrRender/HOM.cpp +++ b/src/Layers/xrRender/HOM.cpp @@ -365,6 +365,8 @@ BOOL CHOM::visible(vis_data& vis) const return TRUE; // not at this time :) if (!bEnabled) return TRUE; // return - everything visible + if (!vis.box.is_valid() || !_valid(vis.box)) + return TRUE; // invalid box ScopeStatTimer scopeStats(stats.Total, stats.TotalTimerLock); From 82683d92948329f3a78401c320cd4857e10c3e12 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Sun, 14 Dec 2025 15:39:01 +0100 Subject: [PATCH 024/261] fix: handling invalid boxes in `CHOM::visible` (#1979) --- src/Layers/xrRender/HOM.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Layers/xrRender/HOM.cpp b/src/Layers/xrRender/HOM.cpp index 9983bf16054..9322999a058 100644 --- a/src/Layers/xrRender/HOM.cpp +++ b/src/Layers/xrRender/HOM.cpp @@ -365,6 +365,8 @@ BOOL CHOM::visible(vis_data& vis) const return TRUE; // not at this time :) if (!bEnabled) return TRUE; // return - everything visible + if (!vis.box.is_valid() || !_valid(vis.box)) + return TRUE; // invalid box ScopeStatTimer scopeStats(stats.Total, stats.TotalTimerLock); From b4f500bd977ebc9d51a3ff8c066c67264c5dae31 Mon Sep 17 00:00:00 2001 From: Sultan Luchezarniy Date: Tue, 16 Dec 2025 14:24:08 +0300 Subject: [PATCH 025/261] unite generations --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6a12d26ca3c..7830c1125b8 100644 --- a/README.md +++ b/README.md @@ -103,14 +103,13 @@ Thank you for your support! * [GSC Game World](https://gsc-game.com/) – for creating S.T.A.L.K.E.R. and supporting the community. * Loxotron – for making the engine sources available. * [All the OpenXRay contributors](https://github.com/OpenXRay/xray-16/graphs/contributors) – for making the project what it is: - * The first OpenXRay team (2014-2017) – for being at the origins of the project: + * The OpenXRay team: * [nitrocaster](https://github.com/nitrocaster) – original project founder. * [Kaffeine](https://github.com/Kaffeine) – initial work on the Linux port, refactoring, polishing. * [CrossVR](https://github.com/CrossVR) (Armada651) – creation of the OpenGL renderer, work on the build system, other project maintenance work. * [andrew-boyarshin](https://github.com/andrew-boyarshin) – work on the build system. * [CasualDev242](https://github.com/CasualDev242) (Swartz27) – work on renderer features. * [awdavies](https://github.com/awdavies) – project maintenance work. - * The second OpenXRay team (2017-now) – for continuing work on the project: * [Xottab_DUTY](https://github.com/Xottab-DUTY) – current project leader. * [intorr](https://github.com/intorr) – work on the project quality, memory leaks, refactoring and optimizations. * [eagleivg](https://github.com/eagleivg) – main part of the work on Linux port. From 547a271ee9ee60b51b063a0dc26944ce1d386720 Mon Sep 17 00:00:00 2001 From: Sultan Luchezarniy Date: Tue, 16 Dec 2025 14:24:08 +0300 Subject: [PATCH 026/261] unite generations --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6a12d26ca3c..7830c1125b8 100644 --- a/README.md +++ b/README.md @@ -103,14 +103,13 @@ Thank you for your support! * [GSC Game World](https://gsc-game.com/) – for creating S.T.A.L.K.E.R. and supporting the community. * Loxotron – for making the engine sources available. * [All the OpenXRay contributors](https://github.com/OpenXRay/xray-16/graphs/contributors) – for making the project what it is: - * The first OpenXRay team (2014-2017) – for being at the origins of the project: + * The OpenXRay team: * [nitrocaster](https://github.com/nitrocaster) – original project founder. * [Kaffeine](https://github.com/Kaffeine) – initial work on the Linux port, refactoring, polishing. * [CrossVR](https://github.com/CrossVR) (Armada651) – creation of the OpenGL renderer, work on the build system, other project maintenance work. * [andrew-boyarshin](https://github.com/andrew-boyarshin) – work on the build system. * [CasualDev242](https://github.com/CasualDev242) (Swartz27) – work on renderer features. * [awdavies](https://github.com/awdavies) – project maintenance work. - * The second OpenXRay team (2017-now) – for continuing work on the project: * [Xottab_DUTY](https://github.com/Xottab-DUTY) – current project leader. * [intorr](https://github.com/intorr) – work on the project quality, memory leaks, refactoring and optimizations. * [eagleivg](https://github.com/eagleivg) – main part of the work on Linux port. From 10d706a07fabd24a4540120ed0c2de9d568dbe30 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Fri, 19 Dec 2025 18:16:38 +0300 Subject: [PATCH 027/261] xrUICore/Windows/UIFrameLineWnd.h|cpp: new rendering code More human-readable, a bit more compact, works just the same. Improved CUIFrameLineWnd debugging by the way. --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 201 +++++++++++------------- src/xrUICore/Windows/UIFrameLineWnd.h | 9 +- 2 files changed, 95 insertions(+), 115 deletions(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index 83fc83f44cd..e04ce4366f1 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -3,11 +3,7 @@ #include "XML/UITextureMaster.h" #include "xrEngine/editor_helper.h" -CUIFrameLineWnd::CUIFrameLineWnd(pcstr window_name) - : CUIWindow(window_name), m_bTextureVisible(false), bHorizontal(true) -{ - m_texture_color = color_argb(255, 255, 255, 255); -} +CUIFrameLineWnd::CUIFrameLineWnd(pcstr window_name) : CUIWindow(window_name) {} bool CUIFrameLineWnd::InitFrameLineWnd(pcstr base_name, Fvector2 pos, Fvector2 size, bool horizontal, bool fatal /*= true*/) { @@ -99,9 +95,9 @@ void CUIFrameLineWnd::Draw() inherited::Draw(); } -static Fvector2 pt_offset = {-0.5f, -0.5f}; +constexpr Fvector2 pt_offset = { -0.5f, -0.5f }; -void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 clr, Fvector2 const& ts) +ICF void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 clr, Fvector2 const& ts) { UI().AlignPixel(LTp.x); UI().AlignPixel(LTp.y); @@ -121,7 +117,7 @@ void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 clr, GEnv.UIRender->PushPoint(RBp.x, RBp.y, 0, clr, RBt.x, RBt.y); } -void CUIFrameLineWnd::DrawElements() +void CUIFrameLineWnd::DrawElements() const { GEnv.UIRender->SetShader(*m_shader); @@ -133,41 +129,81 @@ void CUIFrameLineWnd::DrawElements() UI().ClientToScreenScaled(rect.lt); UI().ClientToScreenScaled(rect.rb); - float back_len = 0.0f; - u32 prim_count = 6 * 2; // first&second - if (bHorizontal) - { - back_len = rect.width() - m_tex_rect[flFirst].width() - m_tex_rect[flSecond].width(); - if (back_len < 0.0f) - rect.x2 -= back_len; + u32 total_tiles{}; + u32 back_tiles{}; + float back_remainder{}; + + const float line_len = bHorizontal ? rect.width() : rect.height(); + + const float first_len = bHorizontal ? m_tex_rect[flFirst].width() : m_tex_rect[flFirst].height(); + const float second_len = bHorizontal ? m_tex_rect[flSecond].width() : m_tex_rect[flSecond].height(); + const float back_len = bHorizontal ? m_tex_rect[flBack].width() : m_tex_rect[flBack].height(); + + const float back_available_len = line_len - first_len - second_len; - if (back_len > 0.0f) - prim_count += 6 * iCeil(back_len / m_tex_rect[flBack].width()); + if (back_available_len > 0.0f && back_len > 0.0f) + { + const float back_tiles_full = back_available_len / back_len; + total_tiles += iCeil(back_tiles_full); + back_tiles = iFloor(back_tiles_full); + back_remainder = back_tiles_full - float(back_tiles); } - else + else if (back_available_len < 0.0f) { - back_len = rect.height() - m_tex_rect[flFirst].height() - m_tex_rect[flSecond].height(); - if (back_len < 0) + if (bHorizontal) + rect.x2 -= back_len; + else rect.y2 -= back_len; - - if (back_len > 0.0f) - prim_count += 6 * iCeil(back_len / m_tex_rect[flBack].height()); } - GEnv.UIRender->StartPrimitive(prim_count, IUIRender::ptTriList, UI().m_currentPointType); + total_tiles += 2; // first & second - for (int i = 0; i < flMax; ++i) - { - Fvector2 LTt, RBt; - Fvector2 LTp, RBp; - int counter = 0; + GEnv.UIRender->StartPrimitive(6 * total_tiles, IUIRender::ptTriList, UI().m_currentPointType); - while (inc_pos(rect, counter, i, LTp, RBp, LTt, RBt)) + float cursor{}; + + const auto draw_tile = [&](const float length, const Frect tex_rect) + { + Fvector2 lt, rb; + if (bHorizontal) { - draw_rect(LTp, RBp, LTt, RBt, m_texture_color, ts); - ++counter; - }; + lt = { rect.lt.x + cursor, rect.lt.y }; + rb = { lt.x + length, rect.rb.y }; + } + else + { + lt = { rect.lt.x, rect.lt.y + cursor }; + rb = { rect.rb.x, lt.y + length }; + } + + draw_rect(lt, rb, tex_rect.lt, tex_rect.rb, m_texture_color, ts); + cursor += length; + }; + + // first + draw_tile(first_len, m_tex_rect[flFirst]); + + // back + for (u32 i = 0; i < back_tiles; ++i) + { + draw_tile(back_len, m_tex_rect[flBack]); } + + // back remainder + if (back_remainder > 0.0f) + { + Frect remainder_tc = m_tex_rect[flBack]; + if (bHorizontal) + remainder_tc.rb.x = remainder_tc.lt.x + remainder_tc.width() * back_remainder; + else + remainder_tc.rb.y = remainder_tc.lt.y + remainder_tc.height() * back_remainder; + + draw_tile(back_len * back_remainder, remainder_tc); + } + + // second + draw_tile(second_len, m_tex_rect[flSecond]); + GEnv.UIRender->FlushPrimitive(); } @@ -184,91 +220,36 @@ void CUIFrameLineWnd::FillDebugInfo() if (!ImGui::CollapsingHeader(CUIFrameLineWnd::GetDebugType())) return; - ImGui::Checkbox("Texture visible", &m_bTextureVisible); + ImGui::Checkbox("Enable texture", &m_bTextureVisible); ImGui::SameLine(); ImGui::Checkbox("Horizontal", &bHorizontal); xray::imgui::ColorEdit4("Texture color", m_texture_color); - ImGui::DragFloat4("Texture rect: back", reinterpret_cast(&m_tex_rect[flBack])); - ImGui::DragFloat4("Texture rect: first", reinterpret_cast(&m_tex_rect[flFirst])); - ImGui::DragFloat4("Texture rect: second", reinterpret_cast(&m_tex_rect[flSecond])); -#endif -} - -bool CUIFrameLineWnd::inc_pos( - Frect& rect, int counter, int i, Fvector2& LTp, Fvector2& RBp, Fvector2& LTt, Fvector2& RBt) -{ - if (i == flFirst || i == flSecond) - { - if (counter != 0) - return false; - - LTt = m_tex_rect[i].lt; - RBt = m_tex_rect[i].rb; - - LTp = rect.lt; + Fvector2 ts{}; + m_shader->GetBaseTextureResolution(ts); - RBp = rect.lt; - RBp.x += m_tex_rect[i].width(); - RBp.y += m_tex_rect[i].height(); - } - else // i==flBack + const auto showRectAdjust = [ts](pcstr label, Frect& tex_rect) { - if ((bHorizontal && rect.lt.x + m_tex_rect[flSecond].width() + EPS_L >= rect.rb.x) || - (!bHorizontal && rect.lt.y + m_tex_rect[flSecond].height() + EPS_L >= rect.rb.y)) - return false; - - LTt = m_tex_rect[i].lt; - LTp = rect.lt; - - bool b_draw_reminder = (bHorizontal) ? - (rect.lt.x + m_tex_rect[flBack].width() > rect.rb.x - m_tex_rect[flSecond].width()) : - (rect.lt.y + m_tex_rect[flBack].height() > rect.rb.y - m_tex_rect[flSecond].height()); - if (b_draw_reminder) - { // draw reminder - float rem_len = (bHorizontal) ? rect.rb.x - m_tex_rect[flSecond].width() - rect.lt.x : - rect.rb.y - m_tex_rect[flSecond].height() - rect.lt.y; - - if (bHorizontal) - { - RBt.y = m_tex_rect[i].rb.y; - RBt.x = m_tex_rect[i].lt.x + rem_len; - - RBp = rect.lt; - RBp.x += rem_len; - RBp.y += m_tex_rect[i].height(); - } - else - { - RBt.y = m_tex_rect[i].lt.y + rem_len; - RBt.x = m_tex_rect[i].rb.x; - - RBp = rect.lt; - RBp.x += m_tex_rect[i].width(); - RBp.y += rem_len; - } - } - else - { // draw full element - RBt = m_tex_rect[i].rb; - - RBp = rect.lt; - RBp.x += m_tex_rect[i].width(); - RBp.y += m_tex_rect[i].height(); + Frect rect = + { + .lt = tex_rect.lt, + .rb = { tex_rect.width(), tex_rect.height() } + }; + if (ImGui::DragFloat4(label, reinterpret_cast(&rect), 1.0f, 0.0f, std::max(ts.x, ts.y))) + { + tex_rect.lt = rect.lt; + tex_rect.rb = rect.lt.add(rect.rb); + clamp(tex_rect.lt.x, 0.0f, ts.x); + clamp(tex_rect.lt.y, 0.0f, ts.y); + clamp(tex_rect.rb.x, 0.0f, ts.x); + clamp(tex_rect.rb.y, 0.0f, ts.y); } - } - - // stretch always - if (bHorizontal) - RBp.y = rect.rb.y; - else - RBp.x = rect.rb.x; + }; - if (bHorizontal) - rect.lt.x = RBp.x; - else - rect.lt.y = RBp.y; - return true; + showRectAdjust("Texture rect: first", m_tex_rect[flFirst]); + showRectAdjust("Texture rect: middle", m_tex_rect[flBack]); + showRectAdjust("Texture rect: second", m_tex_rect[flSecond]); +#endif } diff --git a/src/xrUICore/Windows/UIFrameLineWnd.h b/src/xrUICore/Windows/UIFrameLineWnd.h index 6a6de1ba276..04f7a259ce5 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.h +++ b/src/xrUICore/Windows/UIFrameLineWnd.h @@ -60,12 +60,11 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner void FillDebugInfo() override; protected: - bool inc_pos(Frect& rect, int counter, int i, Fvector2& LTp, Fvector2& RBp, Fvector2& LTt, Fvector2& RBt); - void DrawElements(); + void DrawElements() const; - u32 m_texture_color; - bool m_bTextureVisible; - bool bHorizontal; + u32 m_texture_color{ color_argb(255, 255, 255, 255) }; + bool m_bTextureVisible{ false }; + bool bHorizontal{ true }; Frect m_tex_rect[flMax]; ui_shader m_shader; From 5afc7d789f0bfce2ade2d535e01afc2d1768ff3e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Fri, 19 Dec 2025 18:16:38 +0300 Subject: [PATCH 028/261] xrUICore/Windows/UIFrameLineWnd.h|cpp: new rendering code More human-readable, a bit more compact, works just the same. Improved CUIFrameLineWnd debugging by the way. --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 201 +++++++++++------------- src/xrUICore/Windows/UIFrameLineWnd.h | 9 +- 2 files changed, 95 insertions(+), 115 deletions(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index 83fc83f44cd..e04ce4366f1 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -3,11 +3,7 @@ #include "XML/UITextureMaster.h" #include "xrEngine/editor_helper.h" -CUIFrameLineWnd::CUIFrameLineWnd(pcstr window_name) - : CUIWindow(window_name), m_bTextureVisible(false), bHorizontal(true) -{ - m_texture_color = color_argb(255, 255, 255, 255); -} +CUIFrameLineWnd::CUIFrameLineWnd(pcstr window_name) : CUIWindow(window_name) {} bool CUIFrameLineWnd::InitFrameLineWnd(pcstr base_name, Fvector2 pos, Fvector2 size, bool horizontal, bool fatal /*= true*/) { @@ -99,9 +95,9 @@ void CUIFrameLineWnd::Draw() inherited::Draw(); } -static Fvector2 pt_offset = {-0.5f, -0.5f}; +constexpr Fvector2 pt_offset = { -0.5f, -0.5f }; -void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 clr, Fvector2 const& ts) +ICF void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 clr, Fvector2 const& ts) { UI().AlignPixel(LTp.x); UI().AlignPixel(LTp.y); @@ -121,7 +117,7 @@ void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 clr, GEnv.UIRender->PushPoint(RBp.x, RBp.y, 0, clr, RBt.x, RBt.y); } -void CUIFrameLineWnd::DrawElements() +void CUIFrameLineWnd::DrawElements() const { GEnv.UIRender->SetShader(*m_shader); @@ -133,41 +129,81 @@ void CUIFrameLineWnd::DrawElements() UI().ClientToScreenScaled(rect.lt); UI().ClientToScreenScaled(rect.rb); - float back_len = 0.0f; - u32 prim_count = 6 * 2; // first&second - if (bHorizontal) - { - back_len = rect.width() - m_tex_rect[flFirst].width() - m_tex_rect[flSecond].width(); - if (back_len < 0.0f) - rect.x2 -= back_len; + u32 total_tiles{}; + u32 back_tiles{}; + float back_remainder{}; + + const float line_len = bHorizontal ? rect.width() : rect.height(); + + const float first_len = bHorizontal ? m_tex_rect[flFirst].width() : m_tex_rect[flFirst].height(); + const float second_len = bHorizontal ? m_tex_rect[flSecond].width() : m_tex_rect[flSecond].height(); + const float back_len = bHorizontal ? m_tex_rect[flBack].width() : m_tex_rect[flBack].height(); + + const float back_available_len = line_len - first_len - second_len; - if (back_len > 0.0f) - prim_count += 6 * iCeil(back_len / m_tex_rect[flBack].width()); + if (back_available_len > 0.0f && back_len > 0.0f) + { + const float back_tiles_full = back_available_len / back_len; + total_tiles += iCeil(back_tiles_full); + back_tiles = iFloor(back_tiles_full); + back_remainder = back_tiles_full - float(back_tiles); } - else + else if (back_available_len < 0.0f) { - back_len = rect.height() - m_tex_rect[flFirst].height() - m_tex_rect[flSecond].height(); - if (back_len < 0) + if (bHorizontal) + rect.x2 -= back_len; + else rect.y2 -= back_len; - - if (back_len > 0.0f) - prim_count += 6 * iCeil(back_len / m_tex_rect[flBack].height()); } - GEnv.UIRender->StartPrimitive(prim_count, IUIRender::ptTriList, UI().m_currentPointType); + total_tiles += 2; // first & second - for (int i = 0; i < flMax; ++i) - { - Fvector2 LTt, RBt; - Fvector2 LTp, RBp; - int counter = 0; + GEnv.UIRender->StartPrimitive(6 * total_tiles, IUIRender::ptTriList, UI().m_currentPointType); - while (inc_pos(rect, counter, i, LTp, RBp, LTt, RBt)) + float cursor{}; + + const auto draw_tile = [&](const float length, const Frect tex_rect) + { + Fvector2 lt, rb; + if (bHorizontal) { - draw_rect(LTp, RBp, LTt, RBt, m_texture_color, ts); - ++counter; - }; + lt = { rect.lt.x + cursor, rect.lt.y }; + rb = { lt.x + length, rect.rb.y }; + } + else + { + lt = { rect.lt.x, rect.lt.y + cursor }; + rb = { rect.rb.x, lt.y + length }; + } + + draw_rect(lt, rb, tex_rect.lt, tex_rect.rb, m_texture_color, ts); + cursor += length; + }; + + // first + draw_tile(first_len, m_tex_rect[flFirst]); + + // back + for (u32 i = 0; i < back_tiles; ++i) + { + draw_tile(back_len, m_tex_rect[flBack]); } + + // back remainder + if (back_remainder > 0.0f) + { + Frect remainder_tc = m_tex_rect[flBack]; + if (bHorizontal) + remainder_tc.rb.x = remainder_tc.lt.x + remainder_tc.width() * back_remainder; + else + remainder_tc.rb.y = remainder_tc.lt.y + remainder_tc.height() * back_remainder; + + draw_tile(back_len * back_remainder, remainder_tc); + } + + // second + draw_tile(second_len, m_tex_rect[flSecond]); + GEnv.UIRender->FlushPrimitive(); } @@ -184,91 +220,36 @@ void CUIFrameLineWnd::FillDebugInfo() if (!ImGui::CollapsingHeader(CUIFrameLineWnd::GetDebugType())) return; - ImGui::Checkbox("Texture visible", &m_bTextureVisible); + ImGui::Checkbox("Enable texture", &m_bTextureVisible); ImGui::SameLine(); ImGui::Checkbox("Horizontal", &bHorizontal); xray::imgui::ColorEdit4("Texture color", m_texture_color); - ImGui::DragFloat4("Texture rect: back", reinterpret_cast(&m_tex_rect[flBack])); - ImGui::DragFloat4("Texture rect: first", reinterpret_cast(&m_tex_rect[flFirst])); - ImGui::DragFloat4("Texture rect: second", reinterpret_cast(&m_tex_rect[flSecond])); -#endif -} - -bool CUIFrameLineWnd::inc_pos( - Frect& rect, int counter, int i, Fvector2& LTp, Fvector2& RBp, Fvector2& LTt, Fvector2& RBt) -{ - if (i == flFirst || i == flSecond) - { - if (counter != 0) - return false; - - LTt = m_tex_rect[i].lt; - RBt = m_tex_rect[i].rb; - - LTp = rect.lt; + Fvector2 ts{}; + m_shader->GetBaseTextureResolution(ts); - RBp = rect.lt; - RBp.x += m_tex_rect[i].width(); - RBp.y += m_tex_rect[i].height(); - } - else // i==flBack + const auto showRectAdjust = [ts](pcstr label, Frect& tex_rect) { - if ((bHorizontal && rect.lt.x + m_tex_rect[flSecond].width() + EPS_L >= rect.rb.x) || - (!bHorizontal && rect.lt.y + m_tex_rect[flSecond].height() + EPS_L >= rect.rb.y)) - return false; - - LTt = m_tex_rect[i].lt; - LTp = rect.lt; - - bool b_draw_reminder = (bHorizontal) ? - (rect.lt.x + m_tex_rect[flBack].width() > rect.rb.x - m_tex_rect[flSecond].width()) : - (rect.lt.y + m_tex_rect[flBack].height() > rect.rb.y - m_tex_rect[flSecond].height()); - if (b_draw_reminder) - { // draw reminder - float rem_len = (bHorizontal) ? rect.rb.x - m_tex_rect[flSecond].width() - rect.lt.x : - rect.rb.y - m_tex_rect[flSecond].height() - rect.lt.y; - - if (bHorizontal) - { - RBt.y = m_tex_rect[i].rb.y; - RBt.x = m_tex_rect[i].lt.x + rem_len; - - RBp = rect.lt; - RBp.x += rem_len; - RBp.y += m_tex_rect[i].height(); - } - else - { - RBt.y = m_tex_rect[i].lt.y + rem_len; - RBt.x = m_tex_rect[i].rb.x; - - RBp = rect.lt; - RBp.x += m_tex_rect[i].width(); - RBp.y += rem_len; - } - } - else - { // draw full element - RBt = m_tex_rect[i].rb; - - RBp = rect.lt; - RBp.x += m_tex_rect[i].width(); - RBp.y += m_tex_rect[i].height(); + Frect rect = + { + .lt = tex_rect.lt, + .rb = { tex_rect.width(), tex_rect.height() } + }; + if (ImGui::DragFloat4(label, reinterpret_cast(&rect), 1.0f, 0.0f, std::max(ts.x, ts.y))) + { + tex_rect.lt = rect.lt; + tex_rect.rb = rect.lt.add(rect.rb); + clamp(tex_rect.lt.x, 0.0f, ts.x); + clamp(tex_rect.lt.y, 0.0f, ts.y); + clamp(tex_rect.rb.x, 0.0f, ts.x); + clamp(tex_rect.rb.y, 0.0f, ts.y); } - } - - // stretch always - if (bHorizontal) - RBp.y = rect.rb.y; - else - RBp.x = rect.rb.x; + }; - if (bHorizontal) - rect.lt.x = RBp.x; - else - rect.lt.y = RBp.y; - return true; + showRectAdjust("Texture rect: first", m_tex_rect[flFirst]); + showRectAdjust("Texture rect: middle", m_tex_rect[flBack]); + showRectAdjust("Texture rect: second", m_tex_rect[flSecond]); +#endif } diff --git a/src/xrUICore/Windows/UIFrameLineWnd.h b/src/xrUICore/Windows/UIFrameLineWnd.h index 6a6de1ba276..04f7a259ce5 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.h +++ b/src/xrUICore/Windows/UIFrameLineWnd.h @@ -60,12 +60,11 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner void FillDebugInfo() override; protected: - bool inc_pos(Frect& rect, int counter, int i, Fvector2& LTp, Fvector2& RBp, Fvector2& LTt, Fvector2& RBt); - void DrawElements(); + void DrawElements() const; - u32 m_texture_color; - bool m_bTextureVisible; - bool bHorizontal; + u32 m_texture_color{ color_argb(255, 255, 255, 255) }; + bool m_bTextureVisible{ false }; + bool bHorizontal{ true }; Frect m_tex_rect[flMax]; ui_shader m_shader; From 9e827a309e2943863969cecb793101c4365bb1be Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Fri, 19 Dec 2025 22:07:38 +0300 Subject: [PATCH 029/261] Fix weird background stripes in PDA in CS (#382) CS way of rendering tiles in CUIFrameLineWnd --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 79 ++++++++++++++----------- src/xrUICore/Windows/UIFrameLineWnd.h | 10 +++- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index e04ce4366f1..44ff04c5ecc 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -28,9 +28,9 @@ bool CUIFrameLineWnd::InitTextureEx(pcstr texture, pcstr shader, bool fatal /*= { string256 buf; - const bool back_exist = CUITextureMaster::InitTexture(strconcat(sizeof(buf), buf, texture, "_back"), shader, m_shader, m_tex_rect[flBack]); - const bool b_exist = CUITextureMaster::InitTexture(strconcat(sizeof(buf), buf, texture, "_b"), shader, m_shader, m_tex_rect[flFirst]); - const bool e_exist = CUITextureMaster::InitTexture(strconcat(sizeof(buf), buf, texture, "_e"), shader, m_shader, m_tex_rect[flSecond]); + const bool back_exist = CUITextureMaster::InitTexture(strconcat(buf, texture, "_back"), shader, m_shader[flBack], m_tex_rect[flBack]); + const bool b_exist = CUITextureMaster::InitTexture(strconcat(buf, texture, "_b"), shader, m_shader[flFirst], m_tex_rect[flFirst]); + const bool e_exist = CUITextureMaster::InitTexture(strconcat(buf, texture, "_e"), shader, m_shader[flSecond], m_tex_rect[flSecond]); bool failed = false; @@ -119,27 +119,34 @@ ICF void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 c void CUIFrameLineWnd::DrawElements() const { - GEnv.UIRender->SetShader(*m_shader); - - Fvector2 ts{}; - m_shader->GetBaseTextureResolution(ts); - Frect rect; GetAbsoluteRect(rect); UI().ClientToScreenScaled(rect.lt); UI().ClientToScreenScaled(rect.rb); + float first_len = bHorizontal ? m_tex_rect[flFirst].width() : m_tex_rect[flFirst].height(); + float second_len = bHorizontal ? m_tex_rect[flSecond].width() : m_tex_rect[flSecond].height(); + float back_len = bHorizontal ? m_tex_rect[flBack].width() : m_tex_rect[flBack].height(); + + if (bHorizontal) + { + first_len = UI().ClientToScreenScaledX(first_len); + back_len = UI().ClientToScreenScaledX(back_len); + second_len = UI().ClientToScreenScaledX(second_len) * UI().get_current_kx(); + } + else + { + first_len = UI().ClientToScreenScaledY(first_len); + back_len = UI().ClientToScreenScaledY(back_len); + second_len = UI().ClientToScreenScaledY(second_len); + } + u32 total_tiles{}; u32 back_tiles{}; float back_remainder{}; - const float line_len = bHorizontal ? rect.width() : rect.height(); - - const float first_len = bHorizontal ? m_tex_rect[flFirst].width() : m_tex_rect[flFirst].height(); - const float second_len = bHorizontal ? m_tex_rect[flSecond].width() : m_tex_rect[flSecond].height(); - const float back_len = bHorizontal ? m_tex_rect[flBack].width() : m_tex_rect[flBack].height(); - - const float back_available_len = line_len - first_len - second_len; + const float total_line_len = bHorizontal ? rect.width() : rect.height(); + const float back_available_len = total_line_len - first_len - second_len; if (back_available_len > 0.0f && back_len > 0.0f) { @@ -156,13 +163,9 @@ void CUIFrameLineWnd::DrawElements() const rect.y2 -= back_len; } - total_tiles += 2; // first & second - - GEnv.UIRender->StartPrimitive(6 * total_tiles, IUIRender::ptTriList, UI().m_currentPointType); - float cursor{}; - const auto draw_tile = [&](const float length, const Frect tex_rect) + const auto draw_tile = [&](const float length, const Frect tex_rect, const RectSegment segment) { Fvector2 lt, rb; if (bHorizontal) @@ -175,21 +178,27 @@ void CUIFrameLineWnd::DrawElements() const lt = { rect.lt.x, rect.lt.y + cursor }; rb = { rect.rb.x, lt.y + length }; } + cursor += length; + + Fvector2 ts{}; + m_shader[segment]->GetBaseTextureResolution(ts); + GEnv.UIRender->SetShader(*m_shader[segment]); draw_rect(lt, rb, tex_rect.lt, tex_rect.rb, m_texture_color, ts); - cursor += length; }; // first - draw_tile(first_len, m_tex_rect[flFirst]); + GEnv.UIRender->StartPrimitive(6, IUIRender::ptTriList, UI().m_currentPointType); + draw_tile(first_len, m_tex_rect[flFirst], flFirst); + GEnv.UIRender->FlushPrimitive(); // back + GEnv.UIRender->StartPrimitive(6 * total_tiles, IUIRender::ptTriList, UI().m_currentPointType); for (u32 i = 0; i < back_tiles; ++i) { - draw_tile(back_len, m_tex_rect[flBack]); + draw_tile(back_len, m_tex_rect[flBack], flBack); } - // back remainder if (back_remainder > 0.0f) { Frect remainder_tc = m_tex_rect[flBack]; @@ -198,12 +207,13 @@ void CUIFrameLineWnd::DrawElements() const else remainder_tc.rb.y = remainder_tc.lt.y + remainder_tc.height() * back_remainder; - draw_tile(back_len * back_remainder, remainder_tc); + draw_tile(back_len * back_remainder, remainder_tc, flBack); } + GEnv.UIRender->FlushPrimitive(); // second - draw_tile(second_len, m_tex_rect[flSecond]); - + GEnv.UIRender->StartPrimitive(6, IUIRender::ptTriList, UI().m_currentPointType); + draw_tile(second_len, m_tex_rect[flSecond], flSecond); GEnv.UIRender->FlushPrimitive(); } @@ -227,11 +237,12 @@ void CUIFrameLineWnd::FillDebugInfo() xray::imgui::ColorEdit4("Texture color", m_texture_color); - Fvector2 ts{}; - m_shader->GetBaseTextureResolution(ts); - - const auto showRectAdjust = [ts](pcstr label, Frect& tex_rect) + const auto showRectAdjust = [&](pcstr label, RectSegment segment) { + Fvector2 ts{}; + m_shader[segment]->GetBaseTextureResolution(ts); + + Frect& tex_rect = m_tex_rect[segment]; Frect rect = { .lt = tex_rect.lt, @@ -248,8 +259,8 @@ void CUIFrameLineWnd::FillDebugInfo() } }; - showRectAdjust("Texture rect: first", m_tex_rect[flFirst]); - showRectAdjust("Texture rect: middle", m_tex_rect[flBack]); - showRectAdjust("Texture rect: second", m_tex_rect[flSecond]); + showRectAdjust("Texture rect: first", flFirst); + showRectAdjust("Texture rect: middle", flBack); + showRectAdjust("Texture rect: second", flSecond); #endif } diff --git a/src/xrUICore/Windows/UIFrameLineWnd.h b/src/xrUICore/Windows/UIFrameLineWnd.h index 04f7a259ce5..08920bf0c3e 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.h +++ b/src/xrUICore/Windows/UIFrameLineWnd.h @@ -50,7 +50,11 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner bool GetStretchTexture() override { return false; } void SetTextureVisible(bool value) { m_bTextureVisible = value; } - void SetShader(const ui_shader& sh) { m_shader = sh; } + void SetShader(const ui_shader& sh) + { + for (auto& shader : m_shader) + shader = sh; + } bool IsHorizontal() const { return bHorizontal; } void SetHorizontal(bool horiz) { bHorizontal = horiz; } @@ -66,8 +70,8 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner bool m_bTextureVisible{ false }; bool bHorizontal{ true }; - Frect m_tex_rect[flMax]; - ui_shader m_shader; + Frect m_tex_rect[flMax]{}; + ui_shader m_shader[flMax]{}; private: DECLARE_SCRIPT_REGISTER_FUNCTION(CUIWindow); From b126e7bee919dcdd2f35442a6cfc05ef67067bd6 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Fri, 19 Dec 2025 22:07:38 +0300 Subject: [PATCH 030/261] Fix weird background stripes in PDA in CS (#382) CS way of rendering tiles in CUIFrameLineWnd --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 79 ++++++++++++++----------- src/xrUICore/Windows/UIFrameLineWnd.h | 10 +++- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index e04ce4366f1..44ff04c5ecc 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -28,9 +28,9 @@ bool CUIFrameLineWnd::InitTextureEx(pcstr texture, pcstr shader, bool fatal /*= { string256 buf; - const bool back_exist = CUITextureMaster::InitTexture(strconcat(sizeof(buf), buf, texture, "_back"), shader, m_shader, m_tex_rect[flBack]); - const bool b_exist = CUITextureMaster::InitTexture(strconcat(sizeof(buf), buf, texture, "_b"), shader, m_shader, m_tex_rect[flFirst]); - const bool e_exist = CUITextureMaster::InitTexture(strconcat(sizeof(buf), buf, texture, "_e"), shader, m_shader, m_tex_rect[flSecond]); + const bool back_exist = CUITextureMaster::InitTexture(strconcat(buf, texture, "_back"), shader, m_shader[flBack], m_tex_rect[flBack]); + const bool b_exist = CUITextureMaster::InitTexture(strconcat(buf, texture, "_b"), shader, m_shader[flFirst], m_tex_rect[flFirst]); + const bool e_exist = CUITextureMaster::InitTexture(strconcat(buf, texture, "_e"), shader, m_shader[flSecond], m_tex_rect[flSecond]); bool failed = false; @@ -119,27 +119,34 @@ ICF void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 c void CUIFrameLineWnd::DrawElements() const { - GEnv.UIRender->SetShader(*m_shader); - - Fvector2 ts{}; - m_shader->GetBaseTextureResolution(ts); - Frect rect; GetAbsoluteRect(rect); UI().ClientToScreenScaled(rect.lt); UI().ClientToScreenScaled(rect.rb); + float first_len = bHorizontal ? m_tex_rect[flFirst].width() : m_tex_rect[flFirst].height(); + float second_len = bHorizontal ? m_tex_rect[flSecond].width() : m_tex_rect[flSecond].height(); + float back_len = bHorizontal ? m_tex_rect[flBack].width() : m_tex_rect[flBack].height(); + + if (bHorizontal) + { + first_len = UI().ClientToScreenScaledX(first_len); + back_len = UI().ClientToScreenScaledX(back_len); + second_len = UI().ClientToScreenScaledX(second_len) * UI().get_current_kx(); + } + else + { + first_len = UI().ClientToScreenScaledY(first_len); + back_len = UI().ClientToScreenScaledY(back_len); + second_len = UI().ClientToScreenScaledY(second_len); + } + u32 total_tiles{}; u32 back_tiles{}; float back_remainder{}; - const float line_len = bHorizontal ? rect.width() : rect.height(); - - const float first_len = bHorizontal ? m_tex_rect[flFirst].width() : m_tex_rect[flFirst].height(); - const float second_len = bHorizontal ? m_tex_rect[flSecond].width() : m_tex_rect[flSecond].height(); - const float back_len = bHorizontal ? m_tex_rect[flBack].width() : m_tex_rect[flBack].height(); - - const float back_available_len = line_len - first_len - second_len; + const float total_line_len = bHorizontal ? rect.width() : rect.height(); + const float back_available_len = total_line_len - first_len - second_len; if (back_available_len > 0.0f && back_len > 0.0f) { @@ -156,13 +163,9 @@ void CUIFrameLineWnd::DrawElements() const rect.y2 -= back_len; } - total_tiles += 2; // first & second - - GEnv.UIRender->StartPrimitive(6 * total_tiles, IUIRender::ptTriList, UI().m_currentPointType); - float cursor{}; - const auto draw_tile = [&](const float length, const Frect tex_rect) + const auto draw_tile = [&](const float length, const Frect tex_rect, const RectSegment segment) { Fvector2 lt, rb; if (bHorizontal) @@ -175,21 +178,27 @@ void CUIFrameLineWnd::DrawElements() const lt = { rect.lt.x, rect.lt.y + cursor }; rb = { rect.rb.x, lt.y + length }; } + cursor += length; + + Fvector2 ts{}; + m_shader[segment]->GetBaseTextureResolution(ts); + GEnv.UIRender->SetShader(*m_shader[segment]); draw_rect(lt, rb, tex_rect.lt, tex_rect.rb, m_texture_color, ts); - cursor += length; }; // first - draw_tile(first_len, m_tex_rect[flFirst]); + GEnv.UIRender->StartPrimitive(6, IUIRender::ptTriList, UI().m_currentPointType); + draw_tile(first_len, m_tex_rect[flFirst], flFirst); + GEnv.UIRender->FlushPrimitive(); // back + GEnv.UIRender->StartPrimitive(6 * total_tiles, IUIRender::ptTriList, UI().m_currentPointType); for (u32 i = 0; i < back_tiles; ++i) { - draw_tile(back_len, m_tex_rect[flBack]); + draw_tile(back_len, m_tex_rect[flBack], flBack); } - // back remainder if (back_remainder > 0.0f) { Frect remainder_tc = m_tex_rect[flBack]; @@ -198,12 +207,13 @@ void CUIFrameLineWnd::DrawElements() const else remainder_tc.rb.y = remainder_tc.lt.y + remainder_tc.height() * back_remainder; - draw_tile(back_len * back_remainder, remainder_tc); + draw_tile(back_len * back_remainder, remainder_tc, flBack); } + GEnv.UIRender->FlushPrimitive(); // second - draw_tile(second_len, m_tex_rect[flSecond]); - + GEnv.UIRender->StartPrimitive(6, IUIRender::ptTriList, UI().m_currentPointType); + draw_tile(second_len, m_tex_rect[flSecond], flSecond); GEnv.UIRender->FlushPrimitive(); } @@ -227,11 +237,12 @@ void CUIFrameLineWnd::FillDebugInfo() xray::imgui::ColorEdit4("Texture color", m_texture_color); - Fvector2 ts{}; - m_shader->GetBaseTextureResolution(ts); - - const auto showRectAdjust = [ts](pcstr label, Frect& tex_rect) + const auto showRectAdjust = [&](pcstr label, RectSegment segment) { + Fvector2 ts{}; + m_shader[segment]->GetBaseTextureResolution(ts); + + Frect& tex_rect = m_tex_rect[segment]; Frect rect = { .lt = tex_rect.lt, @@ -248,8 +259,8 @@ void CUIFrameLineWnd::FillDebugInfo() } }; - showRectAdjust("Texture rect: first", m_tex_rect[flFirst]); - showRectAdjust("Texture rect: middle", m_tex_rect[flBack]); - showRectAdjust("Texture rect: second", m_tex_rect[flSecond]); + showRectAdjust("Texture rect: first", flFirst); + showRectAdjust("Texture rect: middle", flBack); + showRectAdjust("Texture rect: second", flSecond); #endif } diff --git a/src/xrUICore/Windows/UIFrameLineWnd.h b/src/xrUICore/Windows/UIFrameLineWnd.h index 04f7a259ce5..08920bf0c3e 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.h +++ b/src/xrUICore/Windows/UIFrameLineWnd.h @@ -50,7 +50,11 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner bool GetStretchTexture() override { return false; } void SetTextureVisible(bool value) { m_bTextureVisible = value; } - void SetShader(const ui_shader& sh) { m_shader = sh; } + void SetShader(const ui_shader& sh) + { + for (auto& shader : m_shader) + shader = sh; + } bool IsHorizontal() const { return bHorizontal; } void SetHorizontal(bool horiz) { bHorizontal = horiz; } @@ -66,8 +70,8 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner bool m_bTextureVisible{ false }; bool bHorizontal{ true }; - Frect m_tex_rect[flMax]; - ui_shader m_shader; + Frect m_tex_rect[flMax]{}; + ui_shader m_shader[flMax]{}; private: DECLARE_SCRIPT_REGISTER_FUNCTION(CUIWindow); From 878cfd355887428006cd7d91dc0e758fbf5a2f34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:41:47 +0300 Subject: [PATCH 031/261] build(deps): bump Externals/sse2neon from `68ec5cb` to `d352d24` (#2007) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2neon b/Externals/sse2neon index 68ec5cb625e..d352d244aa0 160000 --- a/Externals/sse2neon +++ b/Externals/sse2neon @@ -1 +1 @@ -Subproject commit 68ec5cb625e473e40533129ce0e0f0fa6952706b +Subproject commit d352d244aa0ecd1aabe5d20ffbb4b86fec74cded From fde166f614c29758d3edf6769537b99d540cf97b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:41:47 +0300 Subject: [PATCH 032/261] build(deps): bump Externals/sse2neon from `68ec5cb` to `d352d24` (#2007) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2neon b/Externals/sse2neon index 68ec5cb625e..d352d244aa0 160000 --- a/Externals/sse2neon +++ b/Externals/sse2neon @@ -1 +1 @@ -Subproject commit 68ec5cb625e473e40533129ce0e0f0fa6952706b +Subproject commit d352d244aa0ecd1aabe5d20ffbb4b86fec74cded From ec4c7beb347672cd9a60b90093e142dee4467d83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:41:56 +0300 Subject: [PATCH 033/261] build(deps): bump Externals/imgui from `6327b63` to `9ca7ea0` (#2008) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 6327b630645..9ca7ea00c80 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 6327b6306459e8d8a5919baea538eb845f82b19d +Subproject commit 9ca7ea00c80c1cb95b31cb3efa6f88cea33edf2a From 68fc9d3cac6d8ff6acb37c4fb0ce8fcaa7e298ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:41:56 +0300 Subject: [PATCH 034/261] build(deps): bump Externals/imgui from `6327b63` to `9ca7ea0` (#2008) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 6327b630645..9ca7ea00c80 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 6327b6306459e8d8a5919baea538eb845f82b19d +Subproject commit 9ca7ea00c80c1cb95b31cb3efa6f88cea33edf2a From f928a5d8a6596e3dacb3e8f866d61009f656d88d Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 09:22:17 +0300 Subject: [PATCH 035/261] xrGame/ui/UIServerInfo.h|cpp: use UIHelper, simplify class Works the same --- src/xrGame/ui/UIServerInfo.cpp | 83 ++++++++++------------------------ src/xrGame/ui/UIServerInfo.h | 28 +++++------- 2 files changed, 34 insertions(+), 77 deletions(-) diff --git a/src/xrGame/ui/UIServerInfo.cpp b/src/xrGame/ui/UIServerInfo.cpp index d1fc54c1772..1c29254b792 100644 --- a/src/xrGame/ui/UIServerInfo.cpp +++ b/src/xrGame/ui/UIServerInfo.cpp @@ -8,84 +8,42 @@ #include "UIGameCustom.h" #include "Level.h" #include "game_cl_mp.h" +#include "UIHelper.h" #include "xrCore/Media/Image.hpp" -CUIServerInfo::CUIServerInfo() - : CUIDialogWnd(CUIServerInfo::GetDebugType()) -{ - m_dds_file_created = false; - - m_background = xr_new("Background"); - AttachChild(m_background); - m_background->SetAutoDelete(true); - - m_caption = xr_new("Caption"); - AttachChild(m_caption); - m_caption->SetAutoDelete(true); - - m_image = xr_new("Image"); - AttachChild(m_image); - m_image->SetAutoDelete(true); - - m_text_desc = xr_new(); - AttachChild(m_text_desc); - m_text_desc->SetAutoDelete(true); - - m_text_body = xr_new("Text body"); - // m_text_desc->AttachChild (m_text_body); - // m_text_body->SetAutoDelete (true); - - m_btn_spectator = xr_new(); - AttachChild(m_btn_spectator); - m_btn_spectator->SetAutoDelete(true); - - m_btn_next = xr_new(); - AttachChild(m_btn_next); - m_btn_next->SetAutoDelete(true); - - Init(); -} - -CUIServerInfo::~CUIServerInfo() {} -void CUIServerInfo::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) { CUIWndCallback::OnEvent(pWnd, msg, pData); } -void CUIServerInfo::Init() +CUIServerInfo::CUIServerInfo() : CUIDialogWnd(CUIServerInfo::GetDebugType()) { CUIXml xml_doc; xml_doc.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "server_info.xml"); CUIXmlInit::InitWindow(xml_doc, "server_info", 0, this); - CUIXmlInit::InitStatic(xml_doc, "server_info:caption", 0, m_caption); - CUIXmlInit::InitStatic(xml_doc, "server_info:background", 0, m_background); - CUIXmlInit::InitScrollView(xml_doc, "server_info:text_desc", 0, m_text_desc); - CUIXmlInit::InitStatic(xml_doc, "server_info:image", 0, m_image); - CUIXmlInit::InitStatic(xml_doc, "server_info:text_body", 0, m_text_body); + std::ignore = UIHelper::CreateStatic(xml_doc, "server_info:background", 0, this); + std::ignore = UIHelper::CreateStatic(xml_doc, "server_info:caption", 0, this); + m_image = UIHelper::CreateStatic(xml_doc, "server_info:image", this); + + const auto text_desc = UIHelper::CreateScrollView(xml_doc, "server_info:text_desc", this); + + m_text_body = UIHelper::CreateStatic(xml_doc, "server_info:text_body", nullptr); m_text_body->SetTextComplexMode(true); - m_text_body->SetWidth(m_text_desc->GetDesiredChildWidth()); - m_text_desc->AddWindow(m_text_body, true); + m_text_body->SetWidth(text_desc->GetDesiredChildWidth()); + text_desc->AddWindow(m_text_body, true); Frect orig_rect = m_image->GetTextureRect(); m_image->InitTexture("ui" DELIMITER "ui_noise"); m_image->SetTextureRect(orig_rect); m_image->SetStretchTexture(true); - CUIXmlInit::Init3tButton(xml_doc, "server_info:btn_next", 0, m_btn_next); - CUIXmlInit::Init3tButton(xml_doc, "server_info:btn_spectator", 0, m_btn_spectator); + const auto btn_next = UIHelper::Create3tButton(xml_doc, "server_info:btn_next", this); + const auto btn_spectator = UIHelper::Create3tButton(xml_doc, "server_info:btn_spectator", this); - InitCallbacks(); -} + Register(btn_next); + Register(btn_spectator); -void CUIServerInfo::InitCallbacks() -{ - Register(m_btn_next); - Register(m_btn_spectator); - - AddCallback(m_btn_next, BUTTON_CLICKED, CUIWndCallback::void_function(this, &CUIServerInfo::OnNextBtnClick)); - AddCallback( - m_btn_spectator, BUTTON_CLICKED, CUIWndCallback::void_function(this, &CUIServerInfo::OnSpectatorBtnClick)); - // AddCallback (this, WINDOW_KEY_PRESSED, CUIWndCallback::void_function(this, - // &CUIServerInfo::OnNextBtnClick)); + AddCallback(btn_next, BUTTON_CLICKED, CUIWndCallback::void_function(this, &CUIServerInfo::OnNextBtnClick)); + AddCallback(btn_spectator, BUTTON_CLICKED, CUIWndCallback::void_function(this, &CUIServerInfo::OnSpectatorBtnClick)); + // AddCallback(this, WINDOW_KEY_PRESSED, CUIWndCallback::void_function(this, &CUIServerInfo::OnNextBtnClick)); } char const* CUIServerInfo::tmp_logo_file_name = "tmp_sv_logo.dds"; @@ -137,6 +95,11 @@ void CUIServerInfo::SetServerRules(u8 const* data_ptr, u32 const data_size) m_text_body->AdjustHeightToText(); } +void CUIServerInfo::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) +{ + CUIWndCallback::OnEvent(pWnd, msg, pData); +} + void CUIServerInfo::OnSpectatorBtnClick(CUIWindow* w, void* d) { game_cl_mp* mp_game = smart_cast(&Game()); diff --git a/src/xrGame/ui/UIServerInfo.h b/src/xrGame/ui/UIServerInfo.h index 3c25bc8ef6b..ca2def3ca05 100644 --- a/src/xrGame/ui/UIServerInfo.h +++ b/src/xrGame/ui/UIServerInfo.h @@ -12,33 +12,27 @@ class CUIServerInfo final : public CUIDialogWnd, public CUIWndCallback { public: CUIServerInfo(); - virtual ~CUIServerInfo(); - virtual void SendMessage(CUIWindow* pWnd, s16 msg, void* pData = NULL); - void Init(); - void SetServerLogo(u8 const* data_ptr, u32 const data_size); - void SetServerRules(u8 const* data_ptr, u32 const data_size); - bool HasInfo() { return m_dds_file_created; }; - virtual bool OnKeyboardAction(int dik, EUIMessages keyboard_action); + void SetServerLogo(u8 const* data_ptr, u32 data_size); + void SetServerRules(u8 const* data_ptr, u32 data_size); + + [[nodiscard]] + bool HasInfo() const { return m_dds_file_created; } + + virtual void SendMessage(CUIWindow* pWnd, s16 msg, void* pData = nullptr); + bool OnKeyboardAction(int dik, EUIMessages keyboard_action) override; pcstr GetDebugType() override { return "CUIServerInfo"; } private: - void InitCallbacks(); - void OnSpectatorBtnClick(CUIWindow* w, void* d); void OnNextBtnClick(CUIWindow* w, void* d); static char const* tmp_logo_file_name; - bool m_dds_file_created; - CUIStatic* m_caption; - CUIStatic* m_background; - CUIScrollView* m_text_desc; - CUIStatic* m_text_body; - CUIStatic* m_image; - CUI3tButton* m_btn_spectator; - CUI3tButton* m_btn_next; + bool m_dds_file_created{}; + CUIStatic* m_text_body{}; + CUIStatic* m_image{}; }; // class CUIServerInfo #endif //#ifndef UISERVER_INFO_INCLUDED From 9cfeefa4c0237cf36acade8b5aede27d40ede2a2 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 09:22:17 +0300 Subject: [PATCH 036/261] xrGame/ui/UIServerInfo.h|cpp: use UIHelper, simplify class Works the same --- src/xrGame/ui/UIServerInfo.cpp | 83 ++++++++++------------------------ src/xrGame/ui/UIServerInfo.h | 28 +++++------- 2 files changed, 34 insertions(+), 77 deletions(-) diff --git a/src/xrGame/ui/UIServerInfo.cpp b/src/xrGame/ui/UIServerInfo.cpp index d1fc54c1772..1c29254b792 100644 --- a/src/xrGame/ui/UIServerInfo.cpp +++ b/src/xrGame/ui/UIServerInfo.cpp @@ -8,84 +8,42 @@ #include "UIGameCustom.h" #include "Level.h" #include "game_cl_mp.h" +#include "UIHelper.h" #include "xrCore/Media/Image.hpp" -CUIServerInfo::CUIServerInfo() - : CUIDialogWnd(CUIServerInfo::GetDebugType()) -{ - m_dds_file_created = false; - - m_background = xr_new("Background"); - AttachChild(m_background); - m_background->SetAutoDelete(true); - - m_caption = xr_new("Caption"); - AttachChild(m_caption); - m_caption->SetAutoDelete(true); - - m_image = xr_new("Image"); - AttachChild(m_image); - m_image->SetAutoDelete(true); - - m_text_desc = xr_new(); - AttachChild(m_text_desc); - m_text_desc->SetAutoDelete(true); - - m_text_body = xr_new("Text body"); - // m_text_desc->AttachChild (m_text_body); - // m_text_body->SetAutoDelete (true); - - m_btn_spectator = xr_new(); - AttachChild(m_btn_spectator); - m_btn_spectator->SetAutoDelete(true); - - m_btn_next = xr_new(); - AttachChild(m_btn_next); - m_btn_next->SetAutoDelete(true); - - Init(); -} - -CUIServerInfo::~CUIServerInfo() {} -void CUIServerInfo::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) { CUIWndCallback::OnEvent(pWnd, msg, pData); } -void CUIServerInfo::Init() +CUIServerInfo::CUIServerInfo() : CUIDialogWnd(CUIServerInfo::GetDebugType()) { CUIXml xml_doc; xml_doc.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "server_info.xml"); CUIXmlInit::InitWindow(xml_doc, "server_info", 0, this); - CUIXmlInit::InitStatic(xml_doc, "server_info:caption", 0, m_caption); - CUIXmlInit::InitStatic(xml_doc, "server_info:background", 0, m_background); - CUIXmlInit::InitScrollView(xml_doc, "server_info:text_desc", 0, m_text_desc); - CUIXmlInit::InitStatic(xml_doc, "server_info:image", 0, m_image); - CUIXmlInit::InitStatic(xml_doc, "server_info:text_body", 0, m_text_body); + std::ignore = UIHelper::CreateStatic(xml_doc, "server_info:background", 0, this); + std::ignore = UIHelper::CreateStatic(xml_doc, "server_info:caption", 0, this); + m_image = UIHelper::CreateStatic(xml_doc, "server_info:image", this); + + const auto text_desc = UIHelper::CreateScrollView(xml_doc, "server_info:text_desc", this); + + m_text_body = UIHelper::CreateStatic(xml_doc, "server_info:text_body", nullptr); m_text_body->SetTextComplexMode(true); - m_text_body->SetWidth(m_text_desc->GetDesiredChildWidth()); - m_text_desc->AddWindow(m_text_body, true); + m_text_body->SetWidth(text_desc->GetDesiredChildWidth()); + text_desc->AddWindow(m_text_body, true); Frect orig_rect = m_image->GetTextureRect(); m_image->InitTexture("ui" DELIMITER "ui_noise"); m_image->SetTextureRect(orig_rect); m_image->SetStretchTexture(true); - CUIXmlInit::Init3tButton(xml_doc, "server_info:btn_next", 0, m_btn_next); - CUIXmlInit::Init3tButton(xml_doc, "server_info:btn_spectator", 0, m_btn_spectator); + const auto btn_next = UIHelper::Create3tButton(xml_doc, "server_info:btn_next", this); + const auto btn_spectator = UIHelper::Create3tButton(xml_doc, "server_info:btn_spectator", this); - InitCallbacks(); -} + Register(btn_next); + Register(btn_spectator); -void CUIServerInfo::InitCallbacks() -{ - Register(m_btn_next); - Register(m_btn_spectator); - - AddCallback(m_btn_next, BUTTON_CLICKED, CUIWndCallback::void_function(this, &CUIServerInfo::OnNextBtnClick)); - AddCallback( - m_btn_spectator, BUTTON_CLICKED, CUIWndCallback::void_function(this, &CUIServerInfo::OnSpectatorBtnClick)); - // AddCallback (this, WINDOW_KEY_PRESSED, CUIWndCallback::void_function(this, - // &CUIServerInfo::OnNextBtnClick)); + AddCallback(btn_next, BUTTON_CLICKED, CUIWndCallback::void_function(this, &CUIServerInfo::OnNextBtnClick)); + AddCallback(btn_spectator, BUTTON_CLICKED, CUIWndCallback::void_function(this, &CUIServerInfo::OnSpectatorBtnClick)); + // AddCallback(this, WINDOW_KEY_PRESSED, CUIWndCallback::void_function(this, &CUIServerInfo::OnNextBtnClick)); } char const* CUIServerInfo::tmp_logo_file_name = "tmp_sv_logo.dds"; @@ -137,6 +95,11 @@ void CUIServerInfo::SetServerRules(u8 const* data_ptr, u32 const data_size) m_text_body->AdjustHeightToText(); } +void CUIServerInfo::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) +{ + CUIWndCallback::OnEvent(pWnd, msg, pData); +} + void CUIServerInfo::OnSpectatorBtnClick(CUIWindow* w, void* d) { game_cl_mp* mp_game = smart_cast(&Game()); diff --git a/src/xrGame/ui/UIServerInfo.h b/src/xrGame/ui/UIServerInfo.h index 3c25bc8ef6b..ca2def3ca05 100644 --- a/src/xrGame/ui/UIServerInfo.h +++ b/src/xrGame/ui/UIServerInfo.h @@ -12,33 +12,27 @@ class CUIServerInfo final : public CUIDialogWnd, public CUIWndCallback { public: CUIServerInfo(); - virtual ~CUIServerInfo(); - virtual void SendMessage(CUIWindow* pWnd, s16 msg, void* pData = NULL); - void Init(); - void SetServerLogo(u8 const* data_ptr, u32 const data_size); - void SetServerRules(u8 const* data_ptr, u32 const data_size); - bool HasInfo() { return m_dds_file_created; }; - virtual bool OnKeyboardAction(int dik, EUIMessages keyboard_action); + void SetServerLogo(u8 const* data_ptr, u32 data_size); + void SetServerRules(u8 const* data_ptr, u32 data_size); + + [[nodiscard]] + bool HasInfo() const { return m_dds_file_created; } + + virtual void SendMessage(CUIWindow* pWnd, s16 msg, void* pData = nullptr); + bool OnKeyboardAction(int dik, EUIMessages keyboard_action) override; pcstr GetDebugType() override { return "CUIServerInfo"; } private: - void InitCallbacks(); - void OnSpectatorBtnClick(CUIWindow* w, void* d); void OnNextBtnClick(CUIWindow* w, void* d); static char const* tmp_logo_file_name; - bool m_dds_file_created; - CUIStatic* m_caption; - CUIStatic* m_background; - CUIScrollView* m_text_desc; - CUIStatic* m_text_body; - CUIStatic* m_image; - CUI3tButton* m_btn_spectator; - CUI3tButton* m_btn_next; + bool m_dds_file_created{}; + CUIStatic* m_text_body{}; + CUIStatic* m_image{}; }; // class CUIServerInfo #endif //#ifndef UISERVER_INFO_INCLUDED From a4a1c2438a4363966f86f7988d8e7deb7de8f682 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 10:40:23 +0300 Subject: [PATCH 037/261] Make CUIServerInfo work with CS gamedata (#382) Now fully replaces CUIMapDesc in CS (they were almost the same) --- src/xrGame/CMakeLists.txt | 2 - src/xrGame/UIGameCTA.cpp | 11 -- src/xrGame/UIGameCTA.h | 1 - src/xrGame/UIGameDM.cpp | 1 - src/xrGame/UIGameDM.h | 1 - src/xrGame/game_cl_artefacthunt.cpp | 2 - src/xrGame/game_cl_capture_the_artefact.cpp | 1 - src/xrGame/game_cl_deathmatch.cpp | 1 - src/xrGame/game_cl_deathmatch.h | 1 - src/xrGame/game_cl_teamdeathmatch.cpp | 2 - src/xrGame/ui/UIMapDesc.cpp | 157 -------------------- src/xrGame/ui/UIMapDesc.h | 33 ---- src/xrGame/ui/UIServerInfo.cpp | 96 +++++++++--- src/xrGame/ui/UIServerInfo.h | 4 +- src/xrGame/xrGame.vcxproj | 2 - src/xrGame/xrGame.vcxproj.filters | 21 +-- 16 files changed, 82 insertions(+), 254 deletions(-) delete mode 100644 src/xrGame/ui/UIMapDesc.cpp delete mode 100644 src/xrGame/ui/UIMapDesc.h diff --git a/src/xrGame/CMakeLists.txt b/src/xrGame/CMakeLists.txt index 212a3a6f9c3..4e66aec4146 100644 --- a/src/xrGame/CMakeLists.txt +++ b/src/xrGame/CMakeLists.txt @@ -2233,8 +2233,6 @@ target_sources(xrGame PRIVATE ui/UIMainIngameWnd.cpp ui/UIMainIngameWnd.h ui/UIMap.cpp - ui/UIMapDesc.cpp - ui/UIMapDesc.h ui/UIMapFilters.cpp ui/UIMapFilters.h ui/UIMap.h diff --git a/src/xrGame/UIGameCTA.cpp b/src/xrGame/UIGameCTA.cpp index 88c81bfd01f..f7c0cf0b153 100644 --- a/src/xrGame/UIGameCTA.cpp +++ b/src/xrGame/UIGameCTA.cpp @@ -24,7 +24,6 @@ #include "ui/UISkinSelector.h" //.#include "ui/UIInventoryWnd.h" #include "ui/UIPdaWnd.h" -#include "ui/UIMapDesc.h" #include "ui/UISpawnWnd.h" #include "ui/UIBuyWndBase.h" #include "ui/UIMpTradeWnd.h" @@ -155,16 +154,6 @@ void CUIGameCTA::SetClGame(game_cl_GameState* g) m_game = smart_cast(g); VERIFY(m_game); - /*if (m_pMapDesc) - { - if (m_pMapDesc->IsShown()) - { - m_pMapDesc->HideDialog(); - } - delete_data(m_pMapDesc); - } - m_pMapDesc = new CUIMapDesc();*/ - if (m_pBuySpawnMsgBox) { if (m_pBuySpawnMsgBox->IsShown()) diff --git a/src/xrGame/UIGameCTA.h b/src/xrGame/UIGameCTA.h index 674b875895e..5c9459d7284 100644 --- a/src/xrGame/UIGameCTA.h +++ b/src/xrGame/UIGameCTA.h @@ -10,7 +10,6 @@ class UITeamPanels; -class CUIMapDesc; class CUIMoneyIndicator; class CUIRankIndicator; class UITeamPanels; diff --git a/src/xrGame/UIGameDM.cpp b/src/xrGame/UIGameDM.cpp index 60a62096406..c28ecb1eaa0 100644 --- a/src/xrGame/UIGameDM.cpp +++ b/src/xrGame/UIGameDM.cpp @@ -4,7 +4,6 @@ #include "ui/UISkinSelector.h" #include "ui/UIPdaWnd.h" -#include "ui/UIMapDesc.h" #include "ui/KillMessageStruct.h" #include "Level.h" #include "game_cl_base.h" diff --git a/src/xrGame/UIGameDM.h b/src/xrGame/UIGameDM.h index d7e400c22ae..7f489950eb1 100644 --- a/src/xrGame/UIGameDM.h +++ b/src/xrGame/UIGameDM.h @@ -9,7 +9,6 @@ class game_cl_Deathmatch; class CUIMoneyIndicator; class CUIRankIndicator; class UIVoteStatusWnd; -class CUIMapDesc; class UITeamPanels; class CUIGameDM : public UIGameMP diff --git a/src/xrGame/game_cl_artefacthunt.cpp b/src/xrGame/game_cl_artefacthunt.cpp index 812b21ff3d3..a1ba5335aa1 100644 --- a/src/xrGame/game_cl_artefacthunt.cpp +++ b/src/xrGame/game_cl_artefacthunt.cpp @@ -11,7 +11,6 @@ #include "ui/UIMainIngameWnd.h" #include "ui/UISkinSelector.h" #include "ui/UIPdaWnd.h" -#include "ui/UIMapDesc.h" #include "ui/UIMessageBoxEx.h" #include "xrUICore/Static/UIStatic.h" #include "xrEngine/xr_level_controller.h" @@ -58,7 +57,6 @@ void game_cl_ArtefactHunt::Init() { // pInventoryMenu = new CUIInventoryWnd(); // pPdaMenu = new CUIPdaWnd(); - // pMapDesc = new CUIMapDesc(); LoadTeamData(::detail::mp::ahunt::TEAM1_MENU); LoadTeamData(::detail::mp::ahunt::TEAM2_MENU); diff --git a/src/xrGame/game_cl_capture_the_artefact.cpp b/src/xrGame/game_cl_capture_the_artefact.cpp index bca34bc726e..fd2831b7731 100644 --- a/src/xrGame/game_cl_capture_the_artefact.cpp +++ b/src/xrGame/game_cl_capture_the_artefact.cpp @@ -8,7 +8,6 @@ #include "ui/UIMainIngameWnd.h" #include "ui/UISkinSelector.h" #include "ui/UIPdaWnd.h" -#include "ui/UIMapDesc.h" #include "ui/UIVote.h" #include "ui/TeamInfo.h" #include "game_base_menu_events.h" diff --git a/src/xrGame/game_cl_deathmatch.cpp b/src/xrGame/game_cl_deathmatch.cpp index e5353e9da1f..f0df7764d53 100644 --- a/src/xrGame/game_cl_deathmatch.cpp +++ b/src/xrGame/game_cl_deathmatch.cpp @@ -9,7 +9,6 @@ #include "ui/UIMainIngameWnd.h" #include "ui/UISkinSelector.h" #include "ui/UIPdaWnd.h" -#include "ui/UIMapDesc.h" #include "ui/UIMessageBoxEx.h" #include "ui/UIVote.h" #include "GamePersistent.h" diff --git a/src/xrGame/game_cl_deathmatch.h b/src/xrGame/game_cl_deathmatch.h index e8ffa7926af..56e1777fb20 100644 --- a/src/xrGame/game_cl_deathmatch.h +++ b/src/xrGame/game_cl_deathmatch.h @@ -10,7 +10,6 @@ class CUIDialogWnd; class CUIGameDM; class CUIPdaWnd; class CUIInventoryWnd; -class CUIMapDesc; class CWeaponMagazinedWGrenade; class game_cl_Deathmatch : public game_cl_mp diff --git a/src/xrGame/game_cl_teamdeathmatch.cpp b/src/xrGame/game_cl_teamdeathmatch.cpp index 963c3c0fdbc..5bd7e135b28 100644 --- a/src/xrGame/game_cl_teamdeathmatch.cpp +++ b/src/xrGame/game_cl_teamdeathmatch.cpp @@ -10,7 +10,6 @@ #include "ui/UIMainIngameWnd.h" #include "ui/UISkinSelector.h" #include "ui/UIPdaWnd.h" -#include "ui/UIMapDesc.h" #include "game_base_menu_events.h" #include "ui/TeamInfo.h" #include "clsid_game.h" @@ -58,7 +57,6 @@ void game_cl_TeamDeathmatch::Init() { // pInventoryMenu = new CUIInventoryWnd(); // pPdaMenu = new CUIPdaWnd(); - // pMapDesc = new CUIMapDesc(); //----------------------------------------------------------- LoadTeamData(GetTeamMenu(1)); LoadTeamData(GetTeamMenu(2)); diff --git a/src/xrGame/ui/UIMapDesc.cpp b/src/xrGame/ui/UIMapDesc.cpp deleted file mode 100644 index db912f97dff..00000000000 --- a/src/xrGame/ui/UIMapDesc.cpp +++ /dev/null @@ -1,157 +0,0 @@ -#include "StdAfx.h" -#include "UIMapDesc.h" -#include "xrUICore/Static/UIStatic.h" -#include "xrUICore/Cursor/UICursor.h" -#include "xrUICore/ScrollView/UIScrollView.h" -#include "UIXmlInit.h" -#include "xrUICore/Buttons/UI3tButton.h" -#include "UIGameCustom.h" -#include "Level.h" -#include "game_cl_teamdeathmatch.h" -#include "UIMapInfo.h" -#include "xrEngine/xr_level_controller.h" -#include "UIStatsPlayerList.h" - -CUIMapDesc::CUIMapDesc() : CUIDialogWnd(CUIMapDesc::GetDebugType()) -{ - m_pBackground = xr_new("Background"); - AttachChild(m_pBackground); - m_pCaption = xr_new("Caption"); - AttachChild(m_pCaption); - - m_pImage = xr_new("Image"); - AttachChild(m_pImage); - m_pTextDesc = xr_new(); - AttachChild(m_pTextDesc); - - m_pBtnSpectator = xr_new(); - AttachChild(m_pBtnSpectator); - m_pBtnNext = xr_new(); - AttachChild(m_pBtnNext); - - for (int i = 0; i < 3; i++) - { - m_pFrame[i] = xr_new("Frame"); - AttachChild(m_pFrame[i]); - } - - m_pMapInfo = xr_new(); - AttachChild(m_pMapInfo); - - Init(); -} - -CUIMapDesc::~CUIMapDesc() -{ - xr_delete(m_pCaption); - xr_delete(m_pBackground); - - xr_delete(m_pImage); - xr_delete(m_pTextDesc); - - xr_delete(m_pBtnSpectator); - xr_delete(m_pBtnNext); - - xr_delete(m_pMapInfo); - - for (int i = 0; i < 3; i++) - xr_delete(m_pFrame[i]); -} - -void CUIMapDesc::Init() -{ - CUIXml xml_doc; - xml_doc.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "map_desc.xml"); - - CUIXmlInit::InitWindow(xml_doc, "map_desc", 0, this); - CUIXmlInit::InitStatic(xml_doc, "map_desc:caption", 0, m_pCaption); - CUIXmlInit::InitStatic(xml_doc, "map_desc:background", 0, m_pBackground); - CUIXmlInit::InitScrollView(xml_doc, "map_desc:text_desc", 0, m_pTextDesc); - - // init map picture - CUIXmlInit::InitStatic(xml_doc, "map_desc:image", 0, m_pImage); - - xr_string map_name = "intro" DELIMITER "intro_map_pic_"; - - map_name += Level().name().c_str(); - xr_string full_name = map_name + ".dds"; - - Frect orig_rect = m_pImage->GetTextureRect(); - if (FS.exist("$game_textures$", full_name.c_str())) - m_pImage->InitTexture(map_name.c_str()); - else - m_pImage->InitTexture("ui" DELIMITER "ui_noise"); - m_pImage->SetTextureRect(orig_rect); - - CUIXmlInit::InitWindow(xml_doc, "map_desc:map_info", 0, m_pMapInfo); - m_pMapInfo->InitMapInfo(m_pMapInfo->GetWndPos(), m_pMapInfo->GetWndSize()); - m_pMapInfo->InitMap(Level().name().c_str(), NULL); - - ADD_TEXT_TO_VIEW2(m_pMapInfo->GetLargeDesc(), m_pTextDesc); - - CUIXmlInit::InitStatic(xml_doc, "map_desc:image_frames_1", 0, m_pFrame[0]); - CUIXmlInit::InitStatic(xml_doc, "map_desc:image_frames_2", 0, m_pFrame[1]); - CUIXmlInit::InitStatic(xml_doc, "map_desc:image_frames_3", 0, m_pFrame[2]); - - CUIXmlInit::Init3tButton(xml_doc, "map_desc:btn_next", 0, m_pBtnNext); - CUIXmlInit::Init3tButton(xml_doc, "map_desc:btn_spectator", 0, m_pBtnSpectator); - m_pImage->SetStretchTexture(true); -} - -void CUIMapDesc::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) -{ - if (BUTTON_CLICKED == msg) - { - game_cl_mp* dm = smart_cast(&(Game())); - HideDialog(); - if (pWnd == m_pBtnSpectator) - dm->OnSpectatorSelect(); - else if (pWnd == m_pBtnNext) - dm->OnMapInfoAccept(); - } -} - -bool CUIMapDesc::OnKeyboardAction(int dik, EUIMessages keyboard_action) -{ - auto action = GetBindedAction(dik); - - if (WINDOW_KEY_RELEASED == keyboard_action) - { - if (action == kSCORES) - { - ShowChildren(true); - game_cl_mp* game = smart_cast(&Game()); - game->OnKeyboardRelease(kSCORES); - GetUICursor().Show(); - } - - return false; - } - - if (action == kSCORES) - { - ShowChildren(false); - game_cl_mp* game = smart_cast(&Game()); - game->OnKeyboardPress(kSCORES); - UI().GetUICursor().Hide(); - return false; - } - - game_cl_mp* dm = smart_cast(&(Game())); - - switch (action) - { - case kQUIT: - HideDialog(); - dm->OnSpectatorSelect(); - return true; - - case kENTER: - case kJUMP: - HideDialog(); - dm->OnMapInfoAccept(); - return true; - } - - return false; -} diff --git a/src/xrGame/ui/UIMapDesc.h b/src/xrGame/ui/UIMapDesc.h deleted file mode 100644 index 14dca71505c..00000000000 --- a/src/xrGame/ui/UIMapDesc.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "UIDialogWnd.h" - -class CUIStatic; -class CUIScrollView; -class CUI3tButton; -class CUIMapInfo; -class CUIStatsPlayerList; - -class CUIMapDesc final : public CUIDialogWnd -{ -public: - CUIMapDesc(); - ~CUIMapDesc() override; - - void SendMessage(CUIWindow* pWnd, s16 msg, void* pData = 0) override; - bool OnKeyboardAction(int dik, EUIMessages keyboard_action) override; - - pcstr GetDebugType() override { return "CUIMapDesc"; } - -private: - void Init(); - - CUIStatic* m_pCaption; - CUIStatic* m_pBackground; - CUIStatic* m_pFrame[3]; - CUIScrollView* m_pTextDesc; - CUIStatic* m_pImage; - CUI3tButton* m_pBtnSpectator; - CUI3tButton* m_pBtnNext; - CUIMapInfo* m_pMapInfo; -}; diff --git a/src/xrGame/ui/UIServerInfo.cpp b/src/xrGame/ui/UIServerInfo.cpp index 1c29254b792..6948be94913 100644 --- a/src/xrGame/ui/UIServerInfo.cpp +++ b/src/xrGame/ui/UIServerInfo.cpp @@ -1,42 +1,90 @@ #include "StdAfx.h" + #include "UIServerInfo.h" -#include "xrUICore/Static/UIStatic.h" + +#include "xrUICore/Buttons/UI3tButton.h" #include "xrUICore/Cursor/UICursor.h" #include "xrUICore/ScrollView/UIScrollView.h" -#include "UIXmlInit.h" -#include "xrUICore/Buttons/UI3tButton.h" -#include "UIGameCustom.h" -#include "Level.h" +#include "xrUICore/Static/UIStatic.h" + #include "game_cl_mp.h" +#include "Level.h" +#include "UIGameCustom.h" #include "UIHelper.h" +#include "UIMapInfo.h" +#include "UIXmlInit.h" #include "xrCore/Media/Image.hpp" CUIServerInfo::CUIServerInfo() : CUIDialogWnd(CUIServerInfo::GetDebugType()) { CUIXml xml_doc; - xml_doc.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "server_info.xml"); + if (xml_doc.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "server_info.xml", false)) + { + CUIXmlInit::InitWindow(xml_doc, "server_info", 0, this); + xml_doc.SetLocalRoot(xml_doc.NavigateToNode("server_info", 0)); + } + else + { + xml_doc.ClearInternal(); + xml_doc.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "map_desc.xml"); - CUIXmlInit::InitWindow(xml_doc, "server_info", 0, this); + CUIXmlInit::InitWindow(xml_doc, "map_desc", 0, this); + xml_doc.SetLocalRoot(xml_doc.NavigateToNode("map_desc", 0)); + } - std::ignore = UIHelper::CreateStatic(xml_doc, "server_info:background", 0, this); - std::ignore = UIHelper::CreateStatic(xml_doc, "server_info:caption", 0, this); - m_image = UIHelper::CreateStatic(xml_doc, "server_info:image", this); + std::ignore = UIHelper::CreateStatic(xml_doc, "background", 0, this); + std::ignore = UIHelper::CreateStatic(xml_doc, "caption", 0, this); - const auto text_desc = UIHelper::CreateScrollView(xml_doc, "server_info:text_desc", this); + m_image = UIHelper::CreateStatic(xml_doc, "image", this); + { + xr_string map_name = "intro\\intro_map_pic_"; + + map_name += Level().name().c_str(); + xr_string full_name = map_name + ".dds"; + + Frect orig_rect = m_image->GetTextureRect(); + if (FS.exist("$game_textures$", full_name.c_str())) + m_image->InitTexture(map_name.c_str()); + else + m_image->InitTexture("ui\\ui_noise"); + m_image->SetTextureRect(orig_rect); + m_image->SetStretchTexture(true); + } - m_text_body = UIHelper::CreateStatic(xml_doc, "server_info:text_body", nullptr); - m_text_body->SetTextComplexMode(true); - m_text_body->SetWidth(text_desc->GetDesiredChildWidth()); - text_desc->AddWindow(m_text_body, true); + const auto text_desc = UIHelper::CreateScrollView(xml_doc, "text_desc", this); - Frect orig_rect = m_image->GetTextureRect(); - m_image->InitTexture("ui" DELIMITER "ui_noise"); - m_image->SetTextureRect(orig_rect); - m_image->SetStretchTexture(true); + m_text_body = UIHelper::CreateStatic(xml_doc, "text_body", nullptr, false); + if (m_text_body) + { + m_text_body->SetTextComplexMode(true); + m_text_body->SetWidth(text_desc->GetDesiredChildWidth()); + text_desc->AddWindow(m_text_body, true); + } + else + { + auto map_info = xr_new(); + map_info->SetAutoDelete(true); + AttachChild(map_info); + + CUIXmlInit::InitWindow(xml_doc, "map_info", 0, map_info); + map_info->InitMapInfo(map_info->GetWndPos(), map_info->GetWndSize()); + map_info->InitMap(Level().name().c_str(), nullptr); + + pcstr description = map_info->GetLargeDesc(); + if (description && description[0]) + { + ADD_TEXT_TO_VIEW2(map_info->GetLargeDesc(), text_desc); + m_has_info = true; + } + } - const auto btn_next = UIHelper::Create3tButton(xml_doc, "server_info:btn_next", this); - const auto btn_spectator = UIHelper::Create3tButton(xml_doc, "server_info:btn_spectator", this); + std::ignore = UIHelper::CreateStatic(xml_doc, "image_frames_1", this, false); + std::ignore = UIHelper::CreateStatic(xml_doc, "image_frames_2", this, false); + std::ignore = UIHelper::CreateStatic(xml_doc, "image_frames_3", this, false); + + const auto btn_next = UIHelper::Create3tButton(xml_doc, "btn_next", this); + const auto btn_spectator = UIHelper::Create3tButton(xml_doc, "btn_spectator", this); Register(btn_next); Register(btn_spectator); @@ -65,13 +113,16 @@ void CUIServerInfo::SetServerLogo(u8 const* data_ptr, u32 const data_size) // XXX: real convert jpg to dds tmp_writer->w((void*)data_ptr, data_size); // sorry :( FS.w_close(tmp_writer); - m_dds_file_created = true; m_image->InitTexture(tmp_logo_file_name); FS.file_delete("$game_saves$", tmp_logo_file_name); + m_has_info = true; } void CUIServerInfo::SetServerRules(u8 const* data_ptr, u32 const data_size) { + if (!m_text_body) + return; + string4096 tmp_string; u32 new_size = data_size; if (new_size > (sizeof(tmp_string) - 1)) @@ -93,6 +144,7 @@ void CUIServerInfo::SetServerRules(u8 const* data_ptr, u32 const data_size) m_text_body->SetText(tmp_string); // will create shared_str m_text_body->AdjustHeightToText(); + m_has_info = true; } void CUIServerInfo::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) diff --git a/src/xrGame/ui/UIServerInfo.h b/src/xrGame/ui/UIServerInfo.h index ca2def3ca05..2f5b97bef29 100644 --- a/src/xrGame/ui/UIServerInfo.h +++ b/src/xrGame/ui/UIServerInfo.h @@ -17,7 +17,7 @@ class CUIServerInfo final : public CUIDialogWnd, public CUIWndCallback void SetServerRules(u8 const* data_ptr, u32 data_size); [[nodiscard]] - bool HasInfo() const { return m_dds_file_created; } + bool HasInfo() const { return m_has_info; } virtual void SendMessage(CUIWindow* pWnd, s16 msg, void* pData = nullptr); bool OnKeyboardAction(int dik, EUIMessages keyboard_action) override; @@ -30,7 +30,7 @@ class CUIServerInfo final : public CUIDialogWnd, public CUIWndCallback static char const* tmp_logo_file_name; - bool m_dds_file_created{}; + bool m_has_info{}; CUIStatic* m_text_body{}; CUIStatic* m_image{}; }; // class CUIServerInfo diff --git a/src/xrGame/xrGame.vcxproj b/src/xrGame/xrGame.vcxproj index b24dd1c2c20..f6e47441c5f 100644 --- a/src/xrGame/xrGame.vcxproj +++ b/src/xrGame/xrGame.vcxproj @@ -1292,7 +1292,6 @@ - @@ -3158,7 +3157,6 @@ - diff --git a/src/xrGame/xrGame.vcxproj.filters b/src/xrGame/xrGame.vcxproj.filters index dc88506d03f..e15a23b5871 100644 --- a/src/xrGame/xrGame.vcxproj.filters +++ b/src/xrGame/xrGame.vcxproj.filters @@ -1996,12 +1996,6 @@ {fabc6e12-dda3-49a7-94b8-98f7b0cff48b} - - {3854fc6e-819a-453b-9195-71ebeeb12599} - - - {bfa2904a-5c5d-498d-be7d-e3238731f03c} - {775f1493-564c-49e5-b7b9-d6de5e670b05} @@ -2011,6 +2005,9 @@ {5b96c35c-b5b3-409b-987d-161d10d9ad2c} + + {bfa2904a-5c5d-498d-be7d-e3238731f03c} + {e2241740-7d81-4634-a522-bd9ff597994f} @@ -5967,11 +5964,8 @@ UI\Multiplayer\BuyWeaponWnd\TAB - - UI\Multiplayer\team_skin_info\MapDesc - - UI\Multiplayer\team_skin_info\MapDesc\MapInfo + UI\Multiplayer\team_skin_info\ServerInfo\MapInfo UI\Multiplayer\team_skin_info\Spawn @@ -9175,14 +9169,11 @@ UI\Multiplayer\BuyWeaponWnd\TAB - - UI\Multiplayer\team_skin_info\MapDesc - - UI\Multiplayer\team_skin_info\MapDesc\MapInfo + UI\Multiplayer\team_skin_info\ServerInfo\MapInfo - UI\Multiplayer\team_skin_info\MapDesc\MapInfo + UI\Multiplayer\team_skin_info\ServerInfo\MapInfo UI\Multiplayer\team_skin_info\Spawn From 84d833f21d2ec7d932b2078c03cf1571f703f0a9 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 10:40:23 +0300 Subject: [PATCH 038/261] Make CUIServerInfo work with CS gamedata (#382) Now fully replaces CUIMapDesc in CS (they were almost the same) --- src/xrGame/CMakeLists.txt | 2 - src/xrGame/UIGameCTA.cpp | 11 -- src/xrGame/UIGameCTA.h | 1 - src/xrGame/UIGameDM.cpp | 1 - src/xrGame/UIGameDM.h | 1 - src/xrGame/game_cl_artefacthunt.cpp | 2 - src/xrGame/game_cl_capture_the_artefact.cpp | 1 - src/xrGame/game_cl_deathmatch.cpp | 1 - src/xrGame/game_cl_deathmatch.h | 1 - src/xrGame/game_cl_teamdeathmatch.cpp | 2 - src/xrGame/ui/UIMapDesc.cpp | 157 -------------------- src/xrGame/ui/UIMapDesc.h | 33 ---- src/xrGame/ui/UIServerInfo.cpp | 96 +++++++++--- src/xrGame/ui/UIServerInfo.h | 4 +- src/xrGame/xrGame.vcxproj | 2 - src/xrGame/xrGame.vcxproj.filters | 21 +-- 16 files changed, 82 insertions(+), 254 deletions(-) delete mode 100644 src/xrGame/ui/UIMapDesc.cpp delete mode 100644 src/xrGame/ui/UIMapDesc.h diff --git a/src/xrGame/CMakeLists.txt b/src/xrGame/CMakeLists.txt index 212a3a6f9c3..4e66aec4146 100644 --- a/src/xrGame/CMakeLists.txt +++ b/src/xrGame/CMakeLists.txt @@ -2233,8 +2233,6 @@ target_sources(xrGame PRIVATE ui/UIMainIngameWnd.cpp ui/UIMainIngameWnd.h ui/UIMap.cpp - ui/UIMapDesc.cpp - ui/UIMapDesc.h ui/UIMapFilters.cpp ui/UIMapFilters.h ui/UIMap.h diff --git a/src/xrGame/UIGameCTA.cpp b/src/xrGame/UIGameCTA.cpp index 88c81bfd01f..f7c0cf0b153 100644 --- a/src/xrGame/UIGameCTA.cpp +++ b/src/xrGame/UIGameCTA.cpp @@ -24,7 +24,6 @@ #include "ui/UISkinSelector.h" //.#include "ui/UIInventoryWnd.h" #include "ui/UIPdaWnd.h" -#include "ui/UIMapDesc.h" #include "ui/UISpawnWnd.h" #include "ui/UIBuyWndBase.h" #include "ui/UIMpTradeWnd.h" @@ -155,16 +154,6 @@ void CUIGameCTA::SetClGame(game_cl_GameState* g) m_game = smart_cast(g); VERIFY(m_game); - /*if (m_pMapDesc) - { - if (m_pMapDesc->IsShown()) - { - m_pMapDesc->HideDialog(); - } - delete_data(m_pMapDesc); - } - m_pMapDesc = new CUIMapDesc();*/ - if (m_pBuySpawnMsgBox) { if (m_pBuySpawnMsgBox->IsShown()) diff --git a/src/xrGame/UIGameCTA.h b/src/xrGame/UIGameCTA.h index 674b875895e..5c9459d7284 100644 --- a/src/xrGame/UIGameCTA.h +++ b/src/xrGame/UIGameCTA.h @@ -10,7 +10,6 @@ class UITeamPanels; -class CUIMapDesc; class CUIMoneyIndicator; class CUIRankIndicator; class UITeamPanels; diff --git a/src/xrGame/UIGameDM.cpp b/src/xrGame/UIGameDM.cpp index 60a62096406..c28ecb1eaa0 100644 --- a/src/xrGame/UIGameDM.cpp +++ b/src/xrGame/UIGameDM.cpp @@ -4,7 +4,6 @@ #include "ui/UISkinSelector.h" #include "ui/UIPdaWnd.h" -#include "ui/UIMapDesc.h" #include "ui/KillMessageStruct.h" #include "Level.h" #include "game_cl_base.h" diff --git a/src/xrGame/UIGameDM.h b/src/xrGame/UIGameDM.h index d7e400c22ae..7f489950eb1 100644 --- a/src/xrGame/UIGameDM.h +++ b/src/xrGame/UIGameDM.h @@ -9,7 +9,6 @@ class game_cl_Deathmatch; class CUIMoneyIndicator; class CUIRankIndicator; class UIVoteStatusWnd; -class CUIMapDesc; class UITeamPanels; class CUIGameDM : public UIGameMP diff --git a/src/xrGame/game_cl_artefacthunt.cpp b/src/xrGame/game_cl_artefacthunt.cpp index 812b21ff3d3..a1ba5335aa1 100644 --- a/src/xrGame/game_cl_artefacthunt.cpp +++ b/src/xrGame/game_cl_artefacthunt.cpp @@ -11,7 +11,6 @@ #include "ui/UIMainIngameWnd.h" #include "ui/UISkinSelector.h" #include "ui/UIPdaWnd.h" -#include "ui/UIMapDesc.h" #include "ui/UIMessageBoxEx.h" #include "xrUICore/Static/UIStatic.h" #include "xrEngine/xr_level_controller.h" @@ -58,7 +57,6 @@ void game_cl_ArtefactHunt::Init() { // pInventoryMenu = new CUIInventoryWnd(); // pPdaMenu = new CUIPdaWnd(); - // pMapDesc = new CUIMapDesc(); LoadTeamData(::detail::mp::ahunt::TEAM1_MENU); LoadTeamData(::detail::mp::ahunt::TEAM2_MENU); diff --git a/src/xrGame/game_cl_capture_the_artefact.cpp b/src/xrGame/game_cl_capture_the_artefact.cpp index bca34bc726e..fd2831b7731 100644 --- a/src/xrGame/game_cl_capture_the_artefact.cpp +++ b/src/xrGame/game_cl_capture_the_artefact.cpp @@ -8,7 +8,6 @@ #include "ui/UIMainIngameWnd.h" #include "ui/UISkinSelector.h" #include "ui/UIPdaWnd.h" -#include "ui/UIMapDesc.h" #include "ui/UIVote.h" #include "ui/TeamInfo.h" #include "game_base_menu_events.h" diff --git a/src/xrGame/game_cl_deathmatch.cpp b/src/xrGame/game_cl_deathmatch.cpp index e5353e9da1f..f0df7764d53 100644 --- a/src/xrGame/game_cl_deathmatch.cpp +++ b/src/xrGame/game_cl_deathmatch.cpp @@ -9,7 +9,6 @@ #include "ui/UIMainIngameWnd.h" #include "ui/UISkinSelector.h" #include "ui/UIPdaWnd.h" -#include "ui/UIMapDesc.h" #include "ui/UIMessageBoxEx.h" #include "ui/UIVote.h" #include "GamePersistent.h" diff --git a/src/xrGame/game_cl_deathmatch.h b/src/xrGame/game_cl_deathmatch.h index e8ffa7926af..56e1777fb20 100644 --- a/src/xrGame/game_cl_deathmatch.h +++ b/src/xrGame/game_cl_deathmatch.h @@ -10,7 +10,6 @@ class CUIDialogWnd; class CUIGameDM; class CUIPdaWnd; class CUIInventoryWnd; -class CUIMapDesc; class CWeaponMagazinedWGrenade; class game_cl_Deathmatch : public game_cl_mp diff --git a/src/xrGame/game_cl_teamdeathmatch.cpp b/src/xrGame/game_cl_teamdeathmatch.cpp index 963c3c0fdbc..5bd7e135b28 100644 --- a/src/xrGame/game_cl_teamdeathmatch.cpp +++ b/src/xrGame/game_cl_teamdeathmatch.cpp @@ -10,7 +10,6 @@ #include "ui/UIMainIngameWnd.h" #include "ui/UISkinSelector.h" #include "ui/UIPdaWnd.h" -#include "ui/UIMapDesc.h" #include "game_base_menu_events.h" #include "ui/TeamInfo.h" #include "clsid_game.h" @@ -58,7 +57,6 @@ void game_cl_TeamDeathmatch::Init() { // pInventoryMenu = new CUIInventoryWnd(); // pPdaMenu = new CUIPdaWnd(); - // pMapDesc = new CUIMapDesc(); //----------------------------------------------------------- LoadTeamData(GetTeamMenu(1)); LoadTeamData(GetTeamMenu(2)); diff --git a/src/xrGame/ui/UIMapDesc.cpp b/src/xrGame/ui/UIMapDesc.cpp deleted file mode 100644 index db912f97dff..00000000000 --- a/src/xrGame/ui/UIMapDesc.cpp +++ /dev/null @@ -1,157 +0,0 @@ -#include "StdAfx.h" -#include "UIMapDesc.h" -#include "xrUICore/Static/UIStatic.h" -#include "xrUICore/Cursor/UICursor.h" -#include "xrUICore/ScrollView/UIScrollView.h" -#include "UIXmlInit.h" -#include "xrUICore/Buttons/UI3tButton.h" -#include "UIGameCustom.h" -#include "Level.h" -#include "game_cl_teamdeathmatch.h" -#include "UIMapInfo.h" -#include "xrEngine/xr_level_controller.h" -#include "UIStatsPlayerList.h" - -CUIMapDesc::CUIMapDesc() : CUIDialogWnd(CUIMapDesc::GetDebugType()) -{ - m_pBackground = xr_new("Background"); - AttachChild(m_pBackground); - m_pCaption = xr_new("Caption"); - AttachChild(m_pCaption); - - m_pImage = xr_new("Image"); - AttachChild(m_pImage); - m_pTextDesc = xr_new(); - AttachChild(m_pTextDesc); - - m_pBtnSpectator = xr_new(); - AttachChild(m_pBtnSpectator); - m_pBtnNext = xr_new(); - AttachChild(m_pBtnNext); - - for (int i = 0; i < 3; i++) - { - m_pFrame[i] = xr_new("Frame"); - AttachChild(m_pFrame[i]); - } - - m_pMapInfo = xr_new(); - AttachChild(m_pMapInfo); - - Init(); -} - -CUIMapDesc::~CUIMapDesc() -{ - xr_delete(m_pCaption); - xr_delete(m_pBackground); - - xr_delete(m_pImage); - xr_delete(m_pTextDesc); - - xr_delete(m_pBtnSpectator); - xr_delete(m_pBtnNext); - - xr_delete(m_pMapInfo); - - for (int i = 0; i < 3; i++) - xr_delete(m_pFrame[i]); -} - -void CUIMapDesc::Init() -{ - CUIXml xml_doc; - xml_doc.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "map_desc.xml"); - - CUIXmlInit::InitWindow(xml_doc, "map_desc", 0, this); - CUIXmlInit::InitStatic(xml_doc, "map_desc:caption", 0, m_pCaption); - CUIXmlInit::InitStatic(xml_doc, "map_desc:background", 0, m_pBackground); - CUIXmlInit::InitScrollView(xml_doc, "map_desc:text_desc", 0, m_pTextDesc); - - // init map picture - CUIXmlInit::InitStatic(xml_doc, "map_desc:image", 0, m_pImage); - - xr_string map_name = "intro" DELIMITER "intro_map_pic_"; - - map_name += Level().name().c_str(); - xr_string full_name = map_name + ".dds"; - - Frect orig_rect = m_pImage->GetTextureRect(); - if (FS.exist("$game_textures$", full_name.c_str())) - m_pImage->InitTexture(map_name.c_str()); - else - m_pImage->InitTexture("ui" DELIMITER "ui_noise"); - m_pImage->SetTextureRect(orig_rect); - - CUIXmlInit::InitWindow(xml_doc, "map_desc:map_info", 0, m_pMapInfo); - m_pMapInfo->InitMapInfo(m_pMapInfo->GetWndPos(), m_pMapInfo->GetWndSize()); - m_pMapInfo->InitMap(Level().name().c_str(), NULL); - - ADD_TEXT_TO_VIEW2(m_pMapInfo->GetLargeDesc(), m_pTextDesc); - - CUIXmlInit::InitStatic(xml_doc, "map_desc:image_frames_1", 0, m_pFrame[0]); - CUIXmlInit::InitStatic(xml_doc, "map_desc:image_frames_2", 0, m_pFrame[1]); - CUIXmlInit::InitStatic(xml_doc, "map_desc:image_frames_3", 0, m_pFrame[2]); - - CUIXmlInit::Init3tButton(xml_doc, "map_desc:btn_next", 0, m_pBtnNext); - CUIXmlInit::Init3tButton(xml_doc, "map_desc:btn_spectator", 0, m_pBtnSpectator); - m_pImage->SetStretchTexture(true); -} - -void CUIMapDesc::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) -{ - if (BUTTON_CLICKED == msg) - { - game_cl_mp* dm = smart_cast(&(Game())); - HideDialog(); - if (pWnd == m_pBtnSpectator) - dm->OnSpectatorSelect(); - else if (pWnd == m_pBtnNext) - dm->OnMapInfoAccept(); - } -} - -bool CUIMapDesc::OnKeyboardAction(int dik, EUIMessages keyboard_action) -{ - auto action = GetBindedAction(dik); - - if (WINDOW_KEY_RELEASED == keyboard_action) - { - if (action == kSCORES) - { - ShowChildren(true); - game_cl_mp* game = smart_cast(&Game()); - game->OnKeyboardRelease(kSCORES); - GetUICursor().Show(); - } - - return false; - } - - if (action == kSCORES) - { - ShowChildren(false); - game_cl_mp* game = smart_cast(&Game()); - game->OnKeyboardPress(kSCORES); - UI().GetUICursor().Hide(); - return false; - } - - game_cl_mp* dm = smart_cast(&(Game())); - - switch (action) - { - case kQUIT: - HideDialog(); - dm->OnSpectatorSelect(); - return true; - - case kENTER: - case kJUMP: - HideDialog(); - dm->OnMapInfoAccept(); - return true; - } - - return false; -} diff --git a/src/xrGame/ui/UIMapDesc.h b/src/xrGame/ui/UIMapDesc.h deleted file mode 100644 index 14dca71505c..00000000000 --- a/src/xrGame/ui/UIMapDesc.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "UIDialogWnd.h" - -class CUIStatic; -class CUIScrollView; -class CUI3tButton; -class CUIMapInfo; -class CUIStatsPlayerList; - -class CUIMapDesc final : public CUIDialogWnd -{ -public: - CUIMapDesc(); - ~CUIMapDesc() override; - - void SendMessage(CUIWindow* pWnd, s16 msg, void* pData = 0) override; - bool OnKeyboardAction(int dik, EUIMessages keyboard_action) override; - - pcstr GetDebugType() override { return "CUIMapDesc"; } - -private: - void Init(); - - CUIStatic* m_pCaption; - CUIStatic* m_pBackground; - CUIStatic* m_pFrame[3]; - CUIScrollView* m_pTextDesc; - CUIStatic* m_pImage; - CUI3tButton* m_pBtnSpectator; - CUI3tButton* m_pBtnNext; - CUIMapInfo* m_pMapInfo; -}; diff --git a/src/xrGame/ui/UIServerInfo.cpp b/src/xrGame/ui/UIServerInfo.cpp index 1c29254b792..6948be94913 100644 --- a/src/xrGame/ui/UIServerInfo.cpp +++ b/src/xrGame/ui/UIServerInfo.cpp @@ -1,42 +1,90 @@ #include "StdAfx.h" + #include "UIServerInfo.h" -#include "xrUICore/Static/UIStatic.h" + +#include "xrUICore/Buttons/UI3tButton.h" #include "xrUICore/Cursor/UICursor.h" #include "xrUICore/ScrollView/UIScrollView.h" -#include "UIXmlInit.h" -#include "xrUICore/Buttons/UI3tButton.h" -#include "UIGameCustom.h" -#include "Level.h" +#include "xrUICore/Static/UIStatic.h" + #include "game_cl_mp.h" +#include "Level.h" +#include "UIGameCustom.h" #include "UIHelper.h" +#include "UIMapInfo.h" +#include "UIXmlInit.h" #include "xrCore/Media/Image.hpp" CUIServerInfo::CUIServerInfo() : CUIDialogWnd(CUIServerInfo::GetDebugType()) { CUIXml xml_doc; - xml_doc.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "server_info.xml"); + if (xml_doc.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "server_info.xml", false)) + { + CUIXmlInit::InitWindow(xml_doc, "server_info", 0, this); + xml_doc.SetLocalRoot(xml_doc.NavigateToNode("server_info", 0)); + } + else + { + xml_doc.ClearInternal(); + xml_doc.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "map_desc.xml"); - CUIXmlInit::InitWindow(xml_doc, "server_info", 0, this); + CUIXmlInit::InitWindow(xml_doc, "map_desc", 0, this); + xml_doc.SetLocalRoot(xml_doc.NavigateToNode("map_desc", 0)); + } - std::ignore = UIHelper::CreateStatic(xml_doc, "server_info:background", 0, this); - std::ignore = UIHelper::CreateStatic(xml_doc, "server_info:caption", 0, this); - m_image = UIHelper::CreateStatic(xml_doc, "server_info:image", this); + std::ignore = UIHelper::CreateStatic(xml_doc, "background", 0, this); + std::ignore = UIHelper::CreateStatic(xml_doc, "caption", 0, this); - const auto text_desc = UIHelper::CreateScrollView(xml_doc, "server_info:text_desc", this); + m_image = UIHelper::CreateStatic(xml_doc, "image", this); + { + xr_string map_name = "intro\\intro_map_pic_"; + + map_name += Level().name().c_str(); + xr_string full_name = map_name + ".dds"; + + Frect orig_rect = m_image->GetTextureRect(); + if (FS.exist("$game_textures$", full_name.c_str())) + m_image->InitTexture(map_name.c_str()); + else + m_image->InitTexture("ui\\ui_noise"); + m_image->SetTextureRect(orig_rect); + m_image->SetStretchTexture(true); + } - m_text_body = UIHelper::CreateStatic(xml_doc, "server_info:text_body", nullptr); - m_text_body->SetTextComplexMode(true); - m_text_body->SetWidth(text_desc->GetDesiredChildWidth()); - text_desc->AddWindow(m_text_body, true); + const auto text_desc = UIHelper::CreateScrollView(xml_doc, "text_desc", this); - Frect orig_rect = m_image->GetTextureRect(); - m_image->InitTexture("ui" DELIMITER "ui_noise"); - m_image->SetTextureRect(orig_rect); - m_image->SetStretchTexture(true); + m_text_body = UIHelper::CreateStatic(xml_doc, "text_body", nullptr, false); + if (m_text_body) + { + m_text_body->SetTextComplexMode(true); + m_text_body->SetWidth(text_desc->GetDesiredChildWidth()); + text_desc->AddWindow(m_text_body, true); + } + else + { + auto map_info = xr_new(); + map_info->SetAutoDelete(true); + AttachChild(map_info); + + CUIXmlInit::InitWindow(xml_doc, "map_info", 0, map_info); + map_info->InitMapInfo(map_info->GetWndPos(), map_info->GetWndSize()); + map_info->InitMap(Level().name().c_str(), nullptr); + + pcstr description = map_info->GetLargeDesc(); + if (description && description[0]) + { + ADD_TEXT_TO_VIEW2(map_info->GetLargeDesc(), text_desc); + m_has_info = true; + } + } - const auto btn_next = UIHelper::Create3tButton(xml_doc, "server_info:btn_next", this); - const auto btn_spectator = UIHelper::Create3tButton(xml_doc, "server_info:btn_spectator", this); + std::ignore = UIHelper::CreateStatic(xml_doc, "image_frames_1", this, false); + std::ignore = UIHelper::CreateStatic(xml_doc, "image_frames_2", this, false); + std::ignore = UIHelper::CreateStatic(xml_doc, "image_frames_3", this, false); + + const auto btn_next = UIHelper::Create3tButton(xml_doc, "btn_next", this); + const auto btn_spectator = UIHelper::Create3tButton(xml_doc, "btn_spectator", this); Register(btn_next); Register(btn_spectator); @@ -65,13 +113,16 @@ void CUIServerInfo::SetServerLogo(u8 const* data_ptr, u32 const data_size) // XXX: real convert jpg to dds tmp_writer->w((void*)data_ptr, data_size); // sorry :( FS.w_close(tmp_writer); - m_dds_file_created = true; m_image->InitTexture(tmp_logo_file_name); FS.file_delete("$game_saves$", tmp_logo_file_name); + m_has_info = true; } void CUIServerInfo::SetServerRules(u8 const* data_ptr, u32 const data_size) { + if (!m_text_body) + return; + string4096 tmp_string; u32 new_size = data_size; if (new_size > (sizeof(tmp_string) - 1)) @@ -93,6 +144,7 @@ void CUIServerInfo::SetServerRules(u8 const* data_ptr, u32 const data_size) m_text_body->SetText(tmp_string); // will create shared_str m_text_body->AdjustHeightToText(); + m_has_info = true; } void CUIServerInfo::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) diff --git a/src/xrGame/ui/UIServerInfo.h b/src/xrGame/ui/UIServerInfo.h index ca2def3ca05..2f5b97bef29 100644 --- a/src/xrGame/ui/UIServerInfo.h +++ b/src/xrGame/ui/UIServerInfo.h @@ -17,7 +17,7 @@ class CUIServerInfo final : public CUIDialogWnd, public CUIWndCallback void SetServerRules(u8 const* data_ptr, u32 data_size); [[nodiscard]] - bool HasInfo() const { return m_dds_file_created; } + bool HasInfo() const { return m_has_info; } virtual void SendMessage(CUIWindow* pWnd, s16 msg, void* pData = nullptr); bool OnKeyboardAction(int dik, EUIMessages keyboard_action) override; @@ -30,7 +30,7 @@ class CUIServerInfo final : public CUIDialogWnd, public CUIWndCallback static char const* tmp_logo_file_name; - bool m_dds_file_created{}; + bool m_has_info{}; CUIStatic* m_text_body{}; CUIStatic* m_image{}; }; // class CUIServerInfo diff --git a/src/xrGame/xrGame.vcxproj b/src/xrGame/xrGame.vcxproj index b24dd1c2c20..f6e47441c5f 100644 --- a/src/xrGame/xrGame.vcxproj +++ b/src/xrGame/xrGame.vcxproj @@ -1292,7 +1292,6 @@ - @@ -3158,7 +3157,6 @@ - diff --git a/src/xrGame/xrGame.vcxproj.filters b/src/xrGame/xrGame.vcxproj.filters index dc88506d03f..e15a23b5871 100644 --- a/src/xrGame/xrGame.vcxproj.filters +++ b/src/xrGame/xrGame.vcxproj.filters @@ -1996,12 +1996,6 @@ {fabc6e12-dda3-49a7-94b8-98f7b0cff48b} - - {3854fc6e-819a-453b-9195-71ebeeb12599} - - - {bfa2904a-5c5d-498d-be7d-e3238731f03c} - {775f1493-564c-49e5-b7b9-d6de5e670b05} @@ -2011,6 +2005,9 @@ {5b96c35c-b5b3-409b-987d-161d10d9ad2c} + + {bfa2904a-5c5d-498d-be7d-e3238731f03c} + {e2241740-7d81-4634-a522-bd9ff597994f} @@ -5967,11 +5964,8 @@ UI\Multiplayer\BuyWeaponWnd\TAB - - UI\Multiplayer\team_skin_info\MapDesc - - UI\Multiplayer\team_skin_info\MapDesc\MapInfo + UI\Multiplayer\team_skin_info\ServerInfo\MapInfo UI\Multiplayer\team_skin_info\Spawn @@ -9175,14 +9169,11 @@ UI\Multiplayer\BuyWeaponWnd\TAB - - UI\Multiplayer\team_skin_info\MapDesc - - UI\Multiplayer\team_skin_info\MapDesc\MapInfo + UI\Multiplayer\team_skin_info\ServerInfo\MapInfo - UI\Multiplayer\team_skin_info\MapDesc\MapInfo + UI\Multiplayer\team_skin_info\ServerInfo\MapInfo UI\Multiplayer\team_skin_info\Spawn From 41fbf3bdedd5c0e606afbb9dd26dc3a2ea52e0a9 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 10:52:41 +0300 Subject: [PATCH 039/261] xrGame/ui/UIServerInfo.cpp: support for SOC game_maps_mp.ltx images (#392) --- src/xrGame/ui/UIServerInfo.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/xrGame/ui/UIServerInfo.cpp b/src/xrGame/ui/UIServerInfo.cpp index 6948be94913..06e72a4c1bd 100644 --- a/src/xrGame/ui/UIServerInfo.cpp +++ b/src/xrGame/ui/UIServerInfo.cpp @@ -44,7 +44,11 @@ CUIServerInfo::CUIServerInfo() : CUIDialogWnd(CUIServerInfo::GetDebugType()) xr_string full_name = map_name + ".dds"; Frect orig_rect = m_image->GetTextureRect(); - if (FS.exist("$game_textures$", full_name.c_str())) + if (pGameIni->section_exist(Level().name()) && pGameIni->line_exist(Level().name(), "texture")) + { + m_image->InitTexture(pGameIni->r_string(Level().name(), "texture")); + } + else if (FS.exist("$game_textures$", full_name.c_str())) m_image->InitTexture(map_name.c_str()); else m_image->InitTexture("ui\\ui_noise"); From 65c4a6394e4669d3fd76f1464fadf0d3bc1cf42c Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 10:52:41 +0300 Subject: [PATCH 040/261] xrGame/ui/UIServerInfo.cpp: support for SOC game_maps_mp.ltx images (#392) --- src/xrGame/ui/UIServerInfo.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/xrGame/ui/UIServerInfo.cpp b/src/xrGame/ui/UIServerInfo.cpp index 6948be94913..06e72a4c1bd 100644 --- a/src/xrGame/ui/UIServerInfo.cpp +++ b/src/xrGame/ui/UIServerInfo.cpp @@ -44,7 +44,11 @@ CUIServerInfo::CUIServerInfo() : CUIDialogWnd(CUIServerInfo::GetDebugType()) xr_string full_name = map_name + ".dds"; Frect orig_rect = m_image->GetTextureRect(); - if (FS.exist("$game_textures$", full_name.c_str())) + if (pGameIni->section_exist(Level().name()) && pGameIni->line_exist(Level().name(), "texture")) + { + m_image->InitTexture(pGameIni->r_string(Level().name(), "texture")); + } + else if (FS.exist("$game_textures$", full_name.c_str())) m_image->InitTexture(map_name.c_str()); else m_image->InitTexture("ui\\ui_noise"); From c105d1fce1868086be36140f8e01666ae1a7cb89 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 11:05:23 +0300 Subject: [PATCH 041/261] xrGame/Level_Bullet_Manager.cpp: use normal bullet_manager section if mp_bullet_manager doesn't exist. --- src/xrGame/Level_Bullet_Manager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/xrGame/Level_Bullet_Manager.cpp b/src/xrGame/Level_Bullet_Manager.cpp index 75306cce6db..56ba44aa2bf 100644 --- a/src/xrGame/Level_Bullet_Manager.cpp +++ b/src/xrGame/Level_Bullet_Manager.cpp @@ -108,10 +108,11 @@ void CBulletManager::Load() ZoneScoped; char const* bullet_manager_sect = "bullet_manager"; - if (!IsGameTypeSingle()) + if (!IsGameTypeSingle() && pSettings->section_exist("mp_bullet_manager")) { bullet_manager_sect = "mp_bullet_manager"; } + m_fTracerWidth = pSettings->r_float(bullet_manager_sect, "tracer_width"); m_fTracerLengthMax = pSettings->r_float(bullet_manager_sect, "tracer_length_max"); m_fTracerLengthMin = pSettings->r_float(bullet_manager_sect, "tracer_length_min"); From 5d0d3e233cf022599e4a67a5c4e34c35dc3229dc Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 11:05:23 +0300 Subject: [PATCH 042/261] xrGame/Level_Bullet_Manager.cpp: use normal bullet_manager section if mp_bullet_manager doesn't exist. --- src/xrGame/Level_Bullet_Manager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/xrGame/Level_Bullet_Manager.cpp b/src/xrGame/Level_Bullet_Manager.cpp index 75306cce6db..56ba44aa2bf 100644 --- a/src/xrGame/Level_Bullet_Manager.cpp +++ b/src/xrGame/Level_Bullet_Manager.cpp @@ -108,10 +108,11 @@ void CBulletManager::Load() ZoneScoped; char const* bullet_manager_sect = "bullet_manager"; - if (!IsGameTypeSingle()) + if (!IsGameTypeSingle() && pSettings->section_exist("mp_bullet_manager")) { bullet_manager_sect = "mp_bullet_manager"; } + m_fTracerWidth = pSettings->r_float(bullet_manager_sect, "tracer_width"); m_fTracerLengthMax = pSettings->r_float(bullet_manager_sect, "tracer_length_max"); m_fTracerLengthMin = pSettings->r_float(bullet_manager_sect, "tracer_length_min"); From 6ce7d15bb3b15654e5ac8e269e258af63d7faf05 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 11:17:56 +0300 Subject: [PATCH 043/261] Remove old MP UI code remnants [skip ci] --- src/xrGame/UIGameCTA.cpp | 1 - src/xrGame/UIGameMP.cpp | 17 ----------------- src/xrGame/game_cl_deathmatch.cpp | 9 --------- src/xrGame/game_cl_deathmatch.h | 1 - src/xrGame/game_cl_teamdeathmatch.cpp | 12 +----------- 5 files changed, 1 insertion(+), 39 deletions(-) diff --git a/src/xrGame/UIGameCTA.cpp b/src/xrGame/UIGameCTA.cpp index f7c0cf0b153..e111ea00f6a 100644 --- a/src/xrGame/UIGameCTA.cpp +++ b/src/xrGame/UIGameCTA.cpp @@ -22,7 +22,6 @@ #include "WeaponKnife.h" #include "ui/UISkinSelector.h" -//.#include "ui/UIInventoryWnd.h" #include "ui/UIPdaWnd.h" #include "ui/UISpawnWnd.h" #include "ui/UIBuyWndBase.h" diff --git a/src/xrGame/UIGameMP.cpp b/src/xrGame/UIGameMP.cpp index 350ae43bbf1..48464594271 100644 --- a/src/xrGame/UIGameMP.cpp +++ b/src/xrGame/UIGameMP.cpp @@ -35,23 +35,6 @@ bool UIGameMP::IR_UIOnKeyboardPress(int dik) } bool UIGameMP::IR_UIOnKeyboardRelease(int dik) { return inherited::IR_UIOnKeyboardRelease(dik); } -/* -bool UIGameMP::IsMapDescShown() -{ - VERIFY(m_pMapDesc); - return m_pMapDesc->IsShown(); -} -void UIGameMP::ShowMapDesc() -{ - if (Level().IsDemoPlay()) - return; - - VERIFY(m_pMapDesc); - if (!m_pMapDesc->IsShown()) - { - m_pMapDesc->ShowDialog(true); - } -}*/ bool UIGameMP::IsServerInfoShown() { diff --git a/src/xrGame/game_cl_deathmatch.cpp b/src/xrGame/game_cl_deathmatch.cpp index f0df7764d53..3221f860816 100644 --- a/src/xrGame/game_cl_deathmatch.cpp +++ b/src/xrGame/game_cl_deathmatch.cpp @@ -648,8 +648,6 @@ void game_cl_Deathmatch::shedule_Update(u32 dt) //----------------------------------------------- //----------------------------------------------- - // if (m_game_ui->m_pInventoryMenu && m_game_ui->m_pInventoryMenu->IsShown() && !CanCallInventoryMenu()) - // StartStopMenu(m_game_ui->m_pInventoryMenu,true); if (m_game_ui && m_game_ui->GetActorMenu().IsShown() && !CanCallInventoryMenu()) { m_game_ui->HideActorMenu(); @@ -658,10 +656,6 @@ void game_cl_Deathmatch::shedule_Update(u32 dt) //----------------------------------------- u32 cur_game_state = Phase(); - // if(m_game_ui->m_pMapDesc && m_game_ui->m_pMapDesc->IsShown() && cur_game_state!=GAME_PHASE_INPROGRESS) - //{ - // m_game_ui->m_pMapDesc->HideDialog(); - //} if (pCurSkinMenu && pCurSkinMenu->IsShown() && cur_game_state != GAME_PHASE_INPROGRESS) { @@ -1134,9 +1128,6 @@ void game_cl_Deathmatch::OnGameMenuRespond_ChangeSkin(NET_Packet& P) if (pCurSkinMenu && pCurSkinMenu->IsShown()) pCurSkinMenu->HideDialog(); - // if (m_game_ui->m_pMapDesc && m_game_ui->m_pMapDesc->IsShown()) - // m_game_ui->m_pMapDesc->HideDialog(); - SetCurrentSkinMenu(); if (pCurSkinMenu) pCurSkinMenu->SetCurSkin(local_player->skin); diff --git a/src/xrGame/game_cl_deathmatch.h b/src/xrGame/game_cl_deathmatch.h index 56e1777fb20..46949ff767a 100644 --- a/src/xrGame/game_cl_deathmatch.h +++ b/src/xrGame/game_cl_deathmatch.h @@ -9,7 +9,6 @@ class CUISkinSelectorWnd; class CUIDialogWnd; class CUIGameDM; class CUIPdaWnd; -class CUIInventoryWnd; class CWeaponMagazinedWGrenade; class game_cl_Deathmatch : public game_cl_mp diff --git a/src/xrGame/game_cl_teamdeathmatch.cpp b/src/xrGame/game_cl_teamdeathmatch.cpp index 5bd7e135b28..eb93ac084ee 100644 --- a/src/xrGame/game_cl_teamdeathmatch.cpp +++ b/src/xrGame/game_cl_teamdeathmatch.cpp @@ -208,7 +208,6 @@ void game_cl_TeamDeathmatch::OnMapInfoAccept() { if (CanCallTeamSelectMenu()) m_game_ui->m_pUITeamSelectWnd->ShowDialog(true); - //. m_game_ui->StartStopMenu(m_game_ui->m_pUITeamSelectWnd, true); }; void game_cl_TeamDeathmatch::OnTeamMenuBack() @@ -216,21 +215,18 @@ void game_cl_TeamDeathmatch::OnTeamMenuBack() if (local_player->testFlag(GAME_PLAYER_FLAG_SPECTATOR)) { m_game_ui->ShowServerInfo(); - //. m_game_ui->StartStopMenu(m_game_ui->m_pMapDesc, true); } }; void game_cl_TeamDeathmatch::OnTeamMenu_Cancel() { m_game_ui->m_pUITeamSelectWnd->HideDialog(); - //. m_game_ui->StartStopMenu(m_game_ui->m_pUITeamSelectWnd, true); if (!m_bTeamSelected && !m_bSpectatorSelected) { if (CanCallTeamSelectMenu() && !m_game_ui->m_pUITeamSelectWnd->IsShown()) { m_game_ui->m_pUITeamSelectWnd->ShowDialog(true); - //. m_game_ui->StartStopMenu(m_game_ui->m_pUITeamSelectWnd, true); return; } } @@ -241,7 +237,6 @@ void game_cl_TeamDeathmatch::OnSkinMenuBack() { if (CanCallTeamSelectMenu()) m_game_ui->m_pUITeamSelectWnd->ShowDialog(true); - //. m_game_ui->StartStopMenu(m_game_ui->m_pUITeamSelectWnd, true); }; void game_cl_TeamDeathmatch::OnSpectatorSelect() @@ -347,7 +342,6 @@ void game_cl_TeamDeathmatch::SetCurrentSkinMenu() if (pCurSkinMenu && new_team != pCurSkinMenu->GetTeam()) if (pCurSkinMenu->IsShown()) pCurSkinMenu->HideDialog(); - //. m_game_ui->StartStopMenu(pCurSkinMenu,true); xr_delete(pCurSkinMenu); pCurSkinMenu = InitSkinMenu(new_team); @@ -365,7 +359,6 @@ bool game_cl_TeamDeathmatch::CanBeReady() m_bMenuCalledFromReady = FALSE; if (CanCallTeamSelectMenu()) m_game_ui->m_pUITeamSelectWnd->ShowDialog(true); - //. m_game_ui->StartStopMenu(m_game_ui->m_pUITeamSelectWnd,true); return false; } @@ -393,11 +386,9 @@ void game_cl_TeamDeathmatch::shedule_Update(u32 dt) if (!m_game_ui) return; - //--------------------------------------------------------- + if (m_game_ui->m_pUITeamSelectWnd && m_game_ui->m_pUITeamSelectWnd->IsShown() && !CanCallTeamSelectMenu()) m_game_ui->m_pUITeamSelectWnd->HideDialog(); - //. m_game_ui->StartStopMenu(m_game_ui->m_pUITeamSelectWnd,true); - //--------------------------------------------------------- if (m_game_ui) m_game_ui->SetBuyMsgCaption(NULL); @@ -754,7 +745,6 @@ void game_cl_TeamDeathmatch::OnGameMenuRespond_ChangeTeam(NET_Packet& P) pCurSkinMenu->SetCurSkin(local_player->skin); if (CanCallSkinMenu()) pCurSkinMenu->ShowDialog(true); - //. m_game_ui->StartStopMenu(pCurSkinMenu, true); } }; From 9cca74d4897987fef534069c803f491397fe25a0 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 11:17:56 +0300 Subject: [PATCH 044/261] Remove old MP UI code remnants [skip ci] --- src/xrGame/UIGameCTA.cpp | 1 - src/xrGame/UIGameMP.cpp | 17 ----------------- src/xrGame/game_cl_deathmatch.cpp | 9 --------- src/xrGame/game_cl_deathmatch.h | 1 - src/xrGame/game_cl_teamdeathmatch.cpp | 12 +----------- 5 files changed, 1 insertion(+), 39 deletions(-) diff --git a/src/xrGame/UIGameCTA.cpp b/src/xrGame/UIGameCTA.cpp index f7c0cf0b153..e111ea00f6a 100644 --- a/src/xrGame/UIGameCTA.cpp +++ b/src/xrGame/UIGameCTA.cpp @@ -22,7 +22,6 @@ #include "WeaponKnife.h" #include "ui/UISkinSelector.h" -//.#include "ui/UIInventoryWnd.h" #include "ui/UIPdaWnd.h" #include "ui/UISpawnWnd.h" #include "ui/UIBuyWndBase.h" diff --git a/src/xrGame/UIGameMP.cpp b/src/xrGame/UIGameMP.cpp index 350ae43bbf1..48464594271 100644 --- a/src/xrGame/UIGameMP.cpp +++ b/src/xrGame/UIGameMP.cpp @@ -35,23 +35,6 @@ bool UIGameMP::IR_UIOnKeyboardPress(int dik) } bool UIGameMP::IR_UIOnKeyboardRelease(int dik) { return inherited::IR_UIOnKeyboardRelease(dik); } -/* -bool UIGameMP::IsMapDescShown() -{ - VERIFY(m_pMapDesc); - return m_pMapDesc->IsShown(); -} -void UIGameMP::ShowMapDesc() -{ - if (Level().IsDemoPlay()) - return; - - VERIFY(m_pMapDesc); - if (!m_pMapDesc->IsShown()) - { - m_pMapDesc->ShowDialog(true); - } -}*/ bool UIGameMP::IsServerInfoShown() { diff --git a/src/xrGame/game_cl_deathmatch.cpp b/src/xrGame/game_cl_deathmatch.cpp index f0df7764d53..3221f860816 100644 --- a/src/xrGame/game_cl_deathmatch.cpp +++ b/src/xrGame/game_cl_deathmatch.cpp @@ -648,8 +648,6 @@ void game_cl_Deathmatch::shedule_Update(u32 dt) //----------------------------------------------- //----------------------------------------------- - // if (m_game_ui->m_pInventoryMenu && m_game_ui->m_pInventoryMenu->IsShown() && !CanCallInventoryMenu()) - // StartStopMenu(m_game_ui->m_pInventoryMenu,true); if (m_game_ui && m_game_ui->GetActorMenu().IsShown() && !CanCallInventoryMenu()) { m_game_ui->HideActorMenu(); @@ -658,10 +656,6 @@ void game_cl_Deathmatch::shedule_Update(u32 dt) //----------------------------------------- u32 cur_game_state = Phase(); - // if(m_game_ui->m_pMapDesc && m_game_ui->m_pMapDesc->IsShown() && cur_game_state!=GAME_PHASE_INPROGRESS) - //{ - // m_game_ui->m_pMapDesc->HideDialog(); - //} if (pCurSkinMenu && pCurSkinMenu->IsShown() && cur_game_state != GAME_PHASE_INPROGRESS) { @@ -1134,9 +1128,6 @@ void game_cl_Deathmatch::OnGameMenuRespond_ChangeSkin(NET_Packet& P) if (pCurSkinMenu && pCurSkinMenu->IsShown()) pCurSkinMenu->HideDialog(); - // if (m_game_ui->m_pMapDesc && m_game_ui->m_pMapDesc->IsShown()) - // m_game_ui->m_pMapDesc->HideDialog(); - SetCurrentSkinMenu(); if (pCurSkinMenu) pCurSkinMenu->SetCurSkin(local_player->skin); diff --git a/src/xrGame/game_cl_deathmatch.h b/src/xrGame/game_cl_deathmatch.h index 56e1777fb20..46949ff767a 100644 --- a/src/xrGame/game_cl_deathmatch.h +++ b/src/xrGame/game_cl_deathmatch.h @@ -9,7 +9,6 @@ class CUISkinSelectorWnd; class CUIDialogWnd; class CUIGameDM; class CUIPdaWnd; -class CUIInventoryWnd; class CWeaponMagazinedWGrenade; class game_cl_Deathmatch : public game_cl_mp diff --git a/src/xrGame/game_cl_teamdeathmatch.cpp b/src/xrGame/game_cl_teamdeathmatch.cpp index 5bd7e135b28..eb93ac084ee 100644 --- a/src/xrGame/game_cl_teamdeathmatch.cpp +++ b/src/xrGame/game_cl_teamdeathmatch.cpp @@ -208,7 +208,6 @@ void game_cl_TeamDeathmatch::OnMapInfoAccept() { if (CanCallTeamSelectMenu()) m_game_ui->m_pUITeamSelectWnd->ShowDialog(true); - //. m_game_ui->StartStopMenu(m_game_ui->m_pUITeamSelectWnd, true); }; void game_cl_TeamDeathmatch::OnTeamMenuBack() @@ -216,21 +215,18 @@ void game_cl_TeamDeathmatch::OnTeamMenuBack() if (local_player->testFlag(GAME_PLAYER_FLAG_SPECTATOR)) { m_game_ui->ShowServerInfo(); - //. m_game_ui->StartStopMenu(m_game_ui->m_pMapDesc, true); } }; void game_cl_TeamDeathmatch::OnTeamMenu_Cancel() { m_game_ui->m_pUITeamSelectWnd->HideDialog(); - //. m_game_ui->StartStopMenu(m_game_ui->m_pUITeamSelectWnd, true); if (!m_bTeamSelected && !m_bSpectatorSelected) { if (CanCallTeamSelectMenu() && !m_game_ui->m_pUITeamSelectWnd->IsShown()) { m_game_ui->m_pUITeamSelectWnd->ShowDialog(true); - //. m_game_ui->StartStopMenu(m_game_ui->m_pUITeamSelectWnd, true); return; } } @@ -241,7 +237,6 @@ void game_cl_TeamDeathmatch::OnSkinMenuBack() { if (CanCallTeamSelectMenu()) m_game_ui->m_pUITeamSelectWnd->ShowDialog(true); - //. m_game_ui->StartStopMenu(m_game_ui->m_pUITeamSelectWnd, true); }; void game_cl_TeamDeathmatch::OnSpectatorSelect() @@ -347,7 +342,6 @@ void game_cl_TeamDeathmatch::SetCurrentSkinMenu() if (pCurSkinMenu && new_team != pCurSkinMenu->GetTeam()) if (pCurSkinMenu->IsShown()) pCurSkinMenu->HideDialog(); - //. m_game_ui->StartStopMenu(pCurSkinMenu,true); xr_delete(pCurSkinMenu); pCurSkinMenu = InitSkinMenu(new_team); @@ -365,7 +359,6 @@ bool game_cl_TeamDeathmatch::CanBeReady() m_bMenuCalledFromReady = FALSE; if (CanCallTeamSelectMenu()) m_game_ui->m_pUITeamSelectWnd->ShowDialog(true); - //. m_game_ui->StartStopMenu(m_game_ui->m_pUITeamSelectWnd,true); return false; } @@ -393,11 +386,9 @@ void game_cl_TeamDeathmatch::shedule_Update(u32 dt) if (!m_game_ui) return; - //--------------------------------------------------------- + if (m_game_ui->m_pUITeamSelectWnd && m_game_ui->m_pUITeamSelectWnd->IsShown() && !CanCallTeamSelectMenu()) m_game_ui->m_pUITeamSelectWnd->HideDialog(); - //. m_game_ui->StartStopMenu(m_game_ui->m_pUITeamSelectWnd,true); - //--------------------------------------------------------- if (m_game_ui) m_game_ui->SetBuyMsgCaption(NULL); @@ -754,7 +745,6 @@ void game_cl_TeamDeathmatch::OnGameMenuRespond_ChangeTeam(NET_Packet& P) pCurSkinMenu->SetCurSkin(local_player->skin); if (CanCallSkinMenu()) pCurSkinMenu->ShowDialog(true); - //. m_game_ui->StartStopMenu(pCurSkinMenu, true); } }; From 95927b31ddacdc588105a9726e00f8cb15aee52c Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 13:07:09 +0300 Subject: [PATCH 045/261] xrGame/ui/UISkinSelector.h|cpp: use UIHelper, simplify class Use predefined p_image_count constant instead of hardcoded number --- src/xrGame/ui/UISkinSelector.cpp | 71 ++++++++++++-------------------- src/xrGame/ui/UISkinSelector.h | 7 ++-- 2 files changed, 29 insertions(+), 49 deletions(-) diff --git a/src/xrGame/ui/UISkinSelector.cpp b/src/xrGame/ui/UISkinSelector.cpp index 7814c87590e..e64dc453fc9 100644 --- a/src/xrGame/ui/UISkinSelector.cpp +++ b/src/xrGame/ui/UISkinSelector.cpp @@ -9,6 +9,7 @@ #include "game_cl_deathmatch.h" #include "xrEngine/xr_level_controller.h" #include "Level.h" +#include "UIHelper.h" #include "Common/object_broker.h" CUISkinSelectorWnd::CUISkinSelectorWnd(const char* strSectionName, s16 team) @@ -16,19 +17,7 @@ CUISkinSelectorWnd::CUISkinSelectorWnd(const char* strSectionName, s16 team) { m_team = team; m_iActiveIndex = -1; - m_pBackground = xr_new("Background"); - AttachChild(m_pBackground); - m_pCaption = xr_new("Caption"); - AttachChild(m_pCaption); - m_pFrames = xr_new("Frames"); - AttachChild(m_pFrames); - - for (int i = 0; i < 6; i++) - { - m_pImage[i] = xr_new(); - AttachChild(m_pImage[i]); - } // m_pAnims[0] = new CUIAnimatedStatic(); m_pFrames->AttachChild(m_pAnims[0]); // m_pAnims[1] = new CUIAnimatedStatic(); m_pFrames->AttachChild(m_pAnims[1]); // m_pButtons[0] = new CUI3tButton(); m_pFrames->AttachChild(m_pButtons[0]); @@ -36,31 +25,16 @@ CUISkinSelectorWnd::CUISkinSelectorWnd(const char* strSectionName, s16 team) // m_pButtons[1] = new CUI3tButton(); m_pFrames->AttachChild(m_pButtons[1]); // m_pButtons[1]->SetMessageTarget(this); - m_pBtnAutoSelect = xr_new(); - AttachChild(m_pBtnAutoSelect); - m_pBtnSpectator = xr_new(); - AttachChild(m_pBtnSpectator); - m_pBtnBack = xr_new(); - AttachChild(m_pBtnBack); - m_firstSkin = 0; Init(strSectionName); } CUISkinSelectorWnd::~CUISkinSelectorWnd() { - xr_delete(m_pCaption); - xr_delete(m_pBackground); - xr_delete(m_pFrames); // xr_delete(m_pButtons[0]); // xr_delete(m_pButtons[1]); // xr_delete(m_pAnims[0]); // xr_delete(m_pAnims[1]); - xr_delete(m_pBtnAutoSelect); - xr_delete(m_pBtnSpectator); - xr_delete(m_pBtnBack); - for (int i = 0; i < p_image_count; i++) - xr_delete(m_pImage[i]); delete_data(m_skinsEnabled); } @@ -84,7 +58,7 @@ void CUISkinSelectorWnd::InitSkins() void CUISkinSelectorWnd::UpdateSkins() { - for (int i = 0; i < 6; i++) + for (int i = 0; i < p_image_count; i++) { if (!!m_shader) m_pImage[i]->InitTextureEx(m_skins[i + m_firstSkin].c_str(), m_shader.c_str()); @@ -111,7 +85,7 @@ void CUISkinSelectorWnd::UpdateSkins() } // m_pButtons[0]->Enable(m_firstSkin > 0); - // m_pButtons[1]->Enable(m_firstSkin + 4 < (int)m_skins.size()); + // m_pButtons[1]->Enable(m_firstSkin + p_image_count < (int)m_skins.size()); } void CUISkinSelectorWnd::Init(const char* strSectionName) @@ -123,9 +97,22 @@ void CUISkinSelectorWnd::Init(const char* strSectionName) xml_doc.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "skin_selector.xml"); CUIXmlInit::InitWindow(xml_doc, "skin_selector", 0, this); - CUIXmlInit::InitStatic(xml_doc, "skin_selector:caption", 0, m_pCaption); - CUIXmlInit::InitStatic(xml_doc, "skin_selector:background", 0, m_pBackground); - CUIXmlInit::InitStatic(xml_doc, "skin_selector:image_frames", 0, m_pFrames); + + std::ignore = UIHelper::CreateStatic(xml_doc, "skin_selector:background", this); + std::ignore = UIHelper::CreateStatic(xml_doc, "skin_selector:caption", this); + std::ignore = UIHelper::CreateStatic(xml_doc, "skin_selector:image_frames", this); + + string64 buff; + for (int i = 0; i < p_image_count; i++) + { + xr_sprintf(buff, "skin_selector:image_%d", i); + + m_pImage[i] = xr_new(); + m_pImage[i]->SetAutoDelete(true); + AttachChild(m_pImage[i]); + + CUIXmlInit::InitStatic(xml_doc, buff, 0, m_pImage[i]); + } // CUIXmlInit::Init3tButton(xml_doc,"skin_selector:image_frames:btn_left", 0, m_pButtons[0]); // CUIXmlInit::Init3tButton(xml_doc,"skin_selector:image_frames:btn_right",0, m_pButtons[1]); @@ -133,20 +120,14 @@ void CUISkinSelectorWnd::Init(const char* strSectionName) // CUIXmlInit::InitAnimatedStatic(xml_doc,"skin_selector:image_frames:a_static_1", 0, m_pAnims[0]); // CUIXmlInit::InitAnimatedStatic(xml_doc,"skin_selector:image_frames:a_static_2", 0, m_pAnims[1]); - CUIXmlInit::Init3tButton(xml_doc, "skin_selector:btn_spectator", 0, m_pBtnSpectator); - CUIXmlInit::Init3tButton(xml_doc, "skin_selector:btn_autoselect", 0, m_pBtnAutoSelect); - CUIXmlInit::Init3tButton(xml_doc, "skin_selector:btn_back", 0, m_pBtnBack); + m_pBtnAutoSelect = UIHelper::Create3tButton(xml_doc, "skin_selector:btn_autoselect", this); + m_pBtnSpectator = UIHelper::Create3tButton(xml_doc, "skin_selector:btn_spectator", this); + m_pBtnBack = UIHelper::Create3tButton(xml_doc, "skin_selector:btn_back", this); if (xml_doc.NavigateToNode("skin_selector:skin_shader", 0)) m_shader = xml_doc.Read("skin_selector:skin_shader", 0, ""); InitSkins(); - string64 buff; - for (int i = 0; i < 6; i++) - { - xr_sprintf(buff, "skin_selector:image_%d", i); - CUIXmlInit::InitStatic(xml_doc, buff, 0, m_pImage[i]); - } UpdateSkins(); } @@ -180,7 +161,7 @@ void CUISkinSelectorWnd::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) game->OnSkinMenuBack(); } else - for (int i = 0; i < 6; i++) + for (int i = 0; i < p_image_count; i++) if (pWnd == m_pImage[i]) { m_iActiveIndex = m_firstSkin + i; @@ -314,7 +295,7 @@ void CUISkinSelectorWnd::OnKeyLeft() void CUISkinSelectorWnd::OnKeyRight() { - if (m_firstSkin + 6 < (int)m_skins.size()) + if (m_firstSkin + p_image_count < (int)m_skins.size()) { m_firstSkin++; UpdateSkins(); @@ -348,8 +329,8 @@ void CUISkinSelectorWnd::SetCurSkin(int skin) if (m_iActiveIndex != -1 && (m_iActiveIndex < m_firstSkin || m_iActiveIndex > m_firstSkin + 5)) { - if (m_iActiveIndex > (int)m_skins.size() - 6) - m_firstSkin = (int)m_skins.size() - 6; + if (m_iActiveIndex > (int)m_skins.size() - p_image_count) + m_firstSkin = (int)m_skins.size() - p_image_count; else m_firstSkin = m_iActiveIndex; } diff --git a/src/xrGame/ui/UISkinSelector.h b/src/xrGame/ui/UISkinSelector.h index fe3a7da0e7c..069264a6b92 100644 --- a/src/xrGame/ui/UISkinSelector.h +++ b/src/xrGame/ui/UISkinSelector.h @@ -45,10 +45,9 @@ class CUISkinSelectorWnd final : public CUIDialogWnd void InitSkins(); void UpdateSkins(); - CUIStatic* m_pCaption; - CUIStatic* m_pBackground; - CUIStatic* m_pFrames; -#define p_image_count 6 + + static constexpr size_t p_image_count = 6; + CUIStatix* m_pImage[p_image_count]; // CUI3tButton* m_pButtons[2]; // CUIAnimatedStatic* m_pAnims[2]; From a5fbe3411cda324305025b10abe8bf67ccecd65e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 13:07:09 +0300 Subject: [PATCH 046/261] xrGame/ui/UISkinSelector.h|cpp: use UIHelper, simplify class Use predefined p_image_count constant instead of hardcoded number --- src/xrGame/ui/UISkinSelector.cpp | 71 ++++++++++++-------------------- src/xrGame/ui/UISkinSelector.h | 7 ++-- 2 files changed, 29 insertions(+), 49 deletions(-) diff --git a/src/xrGame/ui/UISkinSelector.cpp b/src/xrGame/ui/UISkinSelector.cpp index 7814c87590e..e64dc453fc9 100644 --- a/src/xrGame/ui/UISkinSelector.cpp +++ b/src/xrGame/ui/UISkinSelector.cpp @@ -9,6 +9,7 @@ #include "game_cl_deathmatch.h" #include "xrEngine/xr_level_controller.h" #include "Level.h" +#include "UIHelper.h" #include "Common/object_broker.h" CUISkinSelectorWnd::CUISkinSelectorWnd(const char* strSectionName, s16 team) @@ -16,19 +17,7 @@ CUISkinSelectorWnd::CUISkinSelectorWnd(const char* strSectionName, s16 team) { m_team = team; m_iActiveIndex = -1; - m_pBackground = xr_new("Background"); - AttachChild(m_pBackground); - m_pCaption = xr_new("Caption"); - AttachChild(m_pCaption); - m_pFrames = xr_new("Frames"); - AttachChild(m_pFrames); - - for (int i = 0; i < 6; i++) - { - m_pImage[i] = xr_new(); - AttachChild(m_pImage[i]); - } // m_pAnims[0] = new CUIAnimatedStatic(); m_pFrames->AttachChild(m_pAnims[0]); // m_pAnims[1] = new CUIAnimatedStatic(); m_pFrames->AttachChild(m_pAnims[1]); // m_pButtons[0] = new CUI3tButton(); m_pFrames->AttachChild(m_pButtons[0]); @@ -36,31 +25,16 @@ CUISkinSelectorWnd::CUISkinSelectorWnd(const char* strSectionName, s16 team) // m_pButtons[1] = new CUI3tButton(); m_pFrames->AttachChild(m_pButtons[1]); // m_pButtons[1]->SetMessageTarget(this); - m_pBtnAutoSelect = xr_new(); - AttachChild(m_pBtnAutoSelect); - m_pBtnSpectator = xr_new(); - AttachChild(m_pBtnSpectator); - m_pBtnBack = xr_new(); - AttachChild(m_pBtnBack); - m_firstSkin = 0; Init(strSectionName); } CUISkinSelectorWnd::~CUISkinSelectorWnd() { - xr_delete(m_pCaption); - xr_delete(m_pBackground); - xr_delete(m_pFrames); // xr_delete(m_pButtons[0]); // xr_delete(m_pButtons[1]); // xr_delete(m_pAnims[0]); // xr_delete(m_pAnims[1]); - xr_delete(m_pBtnAutoSelect); - xr_delete(m_pBtnSpectator); - xr_delete(m_pBtnBack); - for (int i = 0; i < p_image_count; i++) - xr_delete(m_pImage[i]); delete_data(m_skinsEnabled); } @@ -84,7 +58,7 @@ void CUISkinSelectorWnd::InitSkins() void CUISkinSelectorWnd::UpdateSkins() { - for (int i = 0; i < 6; i++) + for (int i = 0; i < p_image_count; i++) { if (!!m_shader) m_pImage[i]->InitTextureEx(m_skins[i + m_firstSkin].c_str(), m_shader.c_str()); @@ -111,7 +85,7 @@ void CUISkinSelectorWnd::UpdateSkins() } // m_pButtons[0]->Enable(m_firstSkin > 0); - // m_pButtons[1]->Enable(m_firstSkin + 4 < (int)m_skins.size()); + // m_pButtons[1]->Enable(m_firstSkin + p_image_count < (int)m_skins.size()); } void CUISkinSelectorWnd::Init(const char* strSectionName) @@ -123,9 +97,22 @@ void CUISkinSelectorWnd::Init(const char* strSectionName) xml_doc.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "skin_selector.xml"); CUIXmlInit::InitWindow(xml_doc, "skin_selector", 0, this); - CUIXmlInit::InitStatic(xml_doc, "skin_selector:caption", 0, m_pCaption); - CUIXmlInit::InitStatic(xml_doc, "skin_selector:background", 0, m_pBackground); - CUIXmlInit::InitStatic(xml_doc, "skin_selector:image_frames", 0, m_pFrames); + + std::ignore = UIHelper::CreateStatic(xml_doc, "skin_selector:background", this); + std::ignore = UIHelper::CreateStatic(xml_doc, "skin_selector:caption", this); + std::ignore = UIHelper::CreateStatic(xml_doc, "skin_selector:image_frames", this); + + string64 buff; + for (int i = 0; i < p_image_count; i++) + { + xr_sprintf(buff, "skin_selector:image_%d", i); + + m_pImage[i] = xr_new(); + m_pImage[i]->SetAutoDelete(true); + AttachChild(m_pImage[i]); + + CUIXmlInit::InitStatic(xml_doc, buff, 0, m_pImage[i]); + } // CUIXmlInit::Init3tButton(xml_doc,"skin_selector:image_frames:btn_left", 0, m_pButtons[0]); // CUIXmlInit::Init3tButton(xml_doc,"skin_selector:image_frames:btn_right",0, m_pButtons[1]); @@ -133,20 +120,14 @@ void CUISkinSelectorWnd::Init(const char* strSectionName) // CUIXmlInit::InitAnimatedStatic(xml_doc,"skin_selector:image_frames:a_static_1", 0, m_pAnims[0]); // CUIXmlInit::InitAnimatedStatic(xml_doc,"skin_selector:image_frames:a_static_2", 0, m_pAnims[1]); - CUIXmlInit::Init3tButton(xml_doc, "skin_selector:btn_spectator", 0, m_pBtnSpectator); - CUIXmlInit::Init3tButton(xml_doc, "skin_selector:btn_autoselect", 0, m_pBtnAutoSelect); - CUIXmlInit::Init3tButton(xml_doc, "skin_selector:btn_back", 0, m_pBtnBack); + m_pBtnAutoSelect = UIHelper::Create3tButton(xml_doc, "skin_selector:btn_autoselect", this); + m_pBtnSpectator = UIHelper::Create3tButton(xml_doc, "skin_selector:btn_spectator", this); + m_pBtnBack = UIHelper::Create3tButton(xml_doc, "skin_selector:btn_back", this); if (xml_doc.NavigateToNode("skin_selector:skin_shader", 0)) m_shader = xml_doc.Read("skin_selector:skin_shader", 0, ""); InitSkins(); - string64 buff; - for (int i = 0; i < 6; i++) - { - xr_sprintf(buff, "skin_selector:image_%d", i); - CUIXmlInit::InitStatic(xml_doc, buff, 0, m_pImage[i]); - } UpdateSkins(); } @@ -180,7 +161,7 @@ void CUISkinSelectorWnd::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) game->OnSkinMenuBack(); } else - for (int i = 0; i < 6; i++) + for (int i = 0; i < p_image_count; i++) if (pWnd == m_pImage[i]) { m_iActiveIndex = m_firstSkin + i; @@ -314,7 +295,7 @@ void CUISkinSelectorWnd::OnKeyLeft() void CUISkinSelectorWnd::OnKeyRight() { - if (m_firstSkin + 6 < (int)m_skins.size()) + if (m_firstSkin + p_image_count < (int)m_skins.size()) { m_firstSkin++; UpdateSkins(); @@ -348,8 +329,8 @@ void CUISkinSelectorWnd::SetCurSkin(int skin) if (m_iActiveIndex != -1 && (m_iActiveIndex < m_firstSkin || m_iActiveIndex > m_firstSkin + 5)) { - if (m_iActiveIndex > (int)m_skins.size() - 6) - m_firstSkin = (int)m_skins.size() - 6; + if (m_iActiveIndex > (int)m_skins.size() - p_image_count) + m_firstSkin = (int)m_skins.size() - p_image_count; else m_firstSkin = m_iActiveIndex; } diff --git a/src/xrGame/ui/UISkinSelector.h b/src/xrGame/ui/UISkinSelector.h index fe3a7da0e7c..069264a6b92 100644 --- a/src/xrGame/ui/UISkinSelector.h +++ b/src/xrGame/ui/UISkinSelector.h @@ -45,10 +45,9 @@ class CUISkinSelectorWnd final : public CUIDialogWnd void InitSkins(); void UpdateSkins(); - CUIStatic* m_pCaption; - CUIStatic* m_pBackground; - CUIStatic* m_pFrames; -#define p_image_count 6 + + static constexpr size_t p_image_count = 6; + CUIStatix* m_pImage[p_image_count]; // CUI3tButton* m_pButtons[2]; // CUIAnimatedStatic* m_pAnims[2]; From 76d1c1c484a712fc5063613981498e299878116b Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 13:51:57 +0300 Subject: [PATCH 047/261] Make UISkinSelector work with CS gamedata (#382) --- src/xrGame/ui/UISkinSelector.cpp | 127 ++++++++++++++++++------------- src/xrGame/ui/UISkinSelector.h | 11 +-- 2 files changed, 78 insertions(+), 60 deletions(-) diff --git a/src/xrGame/ui/UISkinSelector.cpp b/src/xrGame/ui/UISkinSelector.cpp index e64dc453fc9..dc1185138d3 100644 --- a/src/xrGame/ui/UISkinSelector.cpp +++ b/src/xrGame/ui/UISkinSelector.cpp @@ -18,24 +18,12 @@ CUISkinSelectorWnd::CUISkinSelectorWnd(const char* strSectionName, s16 team) m_team = team; m_iActiveIndex = -1; - // m_pAnims[0] = new CUIAnimatedStatic(); m_pFrames->AttachChild(m_pAnims[0]); - // m_pAnims[1] = new CUIAnimatedStatic(); m_pFrames->AttachChild(m_pAnims[1]); - // m_pButtons[0] = new CUI3tButton(); m_pFrames->AttachChild(m_pButtons[0]); - // m_pButtons[0]->SetMessageTarget(this); - // m_pButtons[1] = new CUI3tButton(); m_pFrames->AttachChild(m_pButtons[1]); - // m_pButtons[1]->SetMessageTarget(this); - m_firstSkin = 0; Init(strSectionName); } CUISkinSelectorWnd::~CUISkinSelectorWnd() { - // xr_delete(m_pButtons[0]); - // xr_delete(m_pButtons[1]); - // xr_delete(m_pAnims[0]); - // xr_delete(m_pAnims[1]); - delete_data(m_skinsEnabled); } @@ -58,7 +46,7 @@ void CUISkinSelectorWnd::InitSkins() void CUISkinSelectorWnd::UpdateSkins() { - for (int i = 0; i < p_image_count; i++) + for (int i = 0; i < (int)m_pImage.size(); i++) { if (!!m_shader) m_pImage[i]->InitTextureEx(m_skins[i + m_firstSkin].c_str(), m_shader.c_str()); @@ -84,8 +72,10 @@ void CUISkinSelectorWnd::UpdateSkins() m_pImage[i]->Enable(it != m_skinsEnabled.end()); } - // m_pButtons[0]->Enable(m_firstSkin > 0); - // m_pButtons[1]->Enable(m_firstSkin + p_image_count < (int)m_skins.size()); + if (m_pButtons[0]) + m_pButtons[0]->Enable(m_firstSkin > 0); + if (m_pButtons[1]) + m_pButtons[1]->Enable(m_firstSkin + (int)m_pImage.size() < (int)m_skins.size()); } void CUISkinSelectorWnd::Init(const char* strSectionName) @@ -100,25 +90,50 @@ void CUISkinSelectorWnd::Init(const char* strSectionName) std::ignore = UIHelper::CreateStatic(xml_doc, "skin_selector:background", this); std::ignore = UIHelper::CreateStatic(xml_doc, "skin_selector:caption", this); - std::ignore = UIHelper::CreateStatic(xml_doc, "skin_selector:image_frames", this); - string64 buff; - for (int i = 0; i < p_image_count; i++) - { - xr_sprintf(buff, "skin_selector:image_%d", i); + const auto frames = UIHelper::CreateStatic(xml_doc, "skin_selector:image_frames", this); - m_pImage[i] = xr_new(); - m_pImage[i]->SetAutoDelete(true); - AttachChild(m_pImage[i]); + if (xml_doc.NavigateToNode("skin_selector:image_frames:a_static_1")) + { + m_pAnims[0] = xr_new(); + m_pAnims[0]->SetAutoDelete(true); + frames->AttachChild(m_pAnims[0]); + CUIXmlInit::InitAnimatedStatic(xml_doc, "skin_selector:image_frames:a_static_1", 0, m_pAnims[0]); + } - CUIXmlInit::InitStatic(xml_doc, buff, 0, m_pImage[i]); + if (xml_doc.NavigateToNode("skin_selector:image_frames:a_static_2")) + { + m_pAnims[1] = xr_new(); + m_pAnims[1]->SetAutoDelete(true); + frames->AttachChild(m_pAnims[1]); + CUIXmlInit::InitAnimatedStatic(xml_doc, "skin_selector:image_frames:a_static_2", 0, m_pAnims[1]); } - // CUIXmlInit::Init3tButton(xml_doc,"skin_selector:image_frames:btn_left", 0, m_pButtons[0]); - // CUIXmlInit::Init3tButton(xml_doc,"skin_selector:image_frames:btn_right",0, m_pButtons[1]); + m_pButtons[0] = UIHelper::Create3tButton(xml_doc,"skin_selector:image_frames:btn_left", frames, false); + m_pButtons[1] = UIHelper::Create3tButton(xml_doc,"skin_selector:image_frames:btn_right", frames, false); + + if (m_pButtons[0]) + m_pButtons[0]->SetMessageTarget(this); + if (m_pButtons[1]) + m_pButtons[1]->SetMessageTarget(this); + + int i = 0; + while (true) + { + string64 buff; + xr_sprintf(buff, "skin_selector:image_%d", i); + + if (!xml_doc.NavigateToNode(buff) && i != 0) // must have at least one image + break; + + const auto image = xr_new(); + image->SetAutoDelete(true); + AttachChild(image); - // CUIXmlInit::InitAnimatedStatic(xml_doc,"skin_selector:image_frames:a_static_1", 0, m_pAnims[0]); - // CUIXmlInit::InitAnimatedStatic(xml_doc,"skin_selector:image_frames:a_static_2", 0, m_pAnims[1]); + CUIXmlInit::InitStatic(xml_doc, buff, 0, image); + m_pImage.emplace_back(image); + ++i; + } m_pBtnAutoSelect = UIHelper::Create3tButton(xml_doc, "skin_selector:btn_autoselect", this); m_pBtnSpectator = UIHelper::Create3tButton(xml_doc, "skin_selector:btn_spectator", this); @@ -140,12 +155,12 @@ void CUISkinSelectorWnd::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) case BUTTON_CLICKED: game = smart_cast(&(Game())); // dm = smart_cast(&(Game())); - /* - if (pWnd == m_pButtons[0]) - OnKeyLeft(); - else if (pWnd == m_pButtons[1]) - OnKeyRight(); - else */ if (pWnd == m_pBtnAutoSelect) + + if (pWnd == m_pButtons[0]) + OnKeyLeft(); + else if (pWnd == m_pButtons[1]) + OnKeyRight(); + else if (pWnd == m_pBtnAutoSelect) { m_iActiveIndex = -1; OnBtnOK(); @@ -161,28 +176,36 @@ void CUISkinSelectorWnd::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) game->OnSkinMenuBack(); } else - for (int i = 0; i < p_image_count; i++) + { + for (int i = 0; i < (int)m_pImage.size(); i++) + { if (pWnd == m_pImage[i]) { m_iActiveIndex = m_firstSkin + i; OnBtnOK(); } + } + } break; case WINDOW_FOCUS_RECEIVED: - /* - if (pWnd == m_pButtons[0]) - { - m_pAnims[0]->Rewind(0); - m_pAnims[0]->Play(); - } - else if (pWnd == m_pButtons[1]) - { - m_pAnims[1]->Rewind(0); - m_pAnims[1]->Play(); - } - */ + if (m_pButtons[0] && pWnd == m_pButtons[0]) + { + if (m_pAnims[0]) + { + m_pAnims[0]->Rewind(0); + m_pAnims[0]->Play(); + } + } + else if (m_pButtons[1] && pWnd == m_pButtons[1]) + { + if (m_pAnims[1]) + { + m_pAnims[1]->Rewind(0); + m_pAnims[1]->Play(); + } + } break; - } + } // switch (msg) } void CUISkinSelectorWnd::OnBtnCancel() @@ -295,7 +318,7 @@ void CUISkinSelectorWnd::OnKeyLeft() void CUISkinSelectorWnd::OnKeyRight() { - if (m_firstSkin + p_image_count < (int)m_skins.size()) + if (m_firstSkin + (int)m_pImage.size() < (int)m_skins.size()) { m_firstSkin++; UpdateSkins(); @@ -327,10 +350,10 @@ void CUISkinSelectorWnd::SetCurSkin(int skin) m_iActiveIndex = skin; - if (m_iActiveIndex != -1 && (m_iActiveIndex < m_firstSkin || m_iActiveIndex > m_firstSkin + 5)) + if (m_iActiveIndex != -1 && (m_iActiveIndex < m_firstSkin || m_iActiveIndex > m_firstSkin + (m_pImage.size() - 1))) { - if (m_iActiveIndex > (int)m_skins.size() - p_image_count) - m_firstSkin = (int)m_skins.size() - p_image_count; + if (m_iActiveIndex > (int)m_skins.size() - (int)m_pImage.size()) + m_firstSkin = (int)m_skins.size() - (int)m_pImage.size(); else m_firstSkin = m_iActiveIndex; } diff --git a/src/xrGame/ui/UISkinSelector.h b/src/xrGame/ui/UISkinSelector.h index 069264a6b92..8e4f2c4dddd 100644 --- a/src/xrGame/ui/UISkinSelector.h +++ b/src/xrGame/ui/UISkinSelector.h @@ -6,9 +6,6 @@ #include "UIDialogWnd.h" -const u32 SKIN_TEX_HEIGHT = 232; -const u32 SKIN_TEX_WIDTH = 111; - class CUIStatic; class CUIStatix; class CUI3tButton; @@ -46,11 +43,9 @@ class CUISkinSelectorWnd final : public CUIDialogWnd void InitSkins(); void UpdateSkins(); - static constexpr size_t p_image_count = 6; - - CUIStatix* m_pImage[p_image_count]; - // CUI3tButton* m_pButtons[2]; - // CUIAnimatedStatic* m_pAnims[2]; + xr_vector m_pImage; + CUI3tButton* m_pButtons[2]; + CUIAnimatedStatic* m_pAnims[2]; CUI3tButton* m_pBtnAutoSelect; CUI3tButton* m_pBtnSpectator; CUI3tButton* m_pBtnBack; From dce006e01fd10657451c7174666e15292fbabecc Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 13:51:57 +0300 Subject: [PATCH 048/261] Make UISkinSelector work with CS gamedata (#382) --- src/xrGame/ui/UISkinSelector.cpp | 127 ++++++++++++++++++------------- src/xrGame/ui/UISkinSelector.h | 11 +-- 2 files changed, 78 insertions(+), 60 deletions(-) diff --git a/src/xrGame/ui/UISkinSelector.cpp b/src/xrGame/ui/UISkinSelector.cpp index e64dc453fc9..dc1185138d3 100644 --- a/src/xrGame/ui/UISkinSelector.cpp +++ b/src/xrGame/ui/UISkinSelector.cpp @@ -18,24 +18,12 @@ CUISkinSelectorWnd::CUISkinSelectorWnd(const char* strSectionName, s16 team) m_team = team; m_iActiveIndex = -1; - // m_pAnims[0] = new CUIAnimatedStatic(); m_pFrames->AttachChild(m_pAnims[0]); - // m_pAnims[1] = new CUIAnimatedStatic(); m_pFrames->AttachChild(m_pAnims[1]); - // m_pButtons[0] = new CUI3tButton(); m_pFrames->AttachChild(m_pButtons[0]); - // m_pButtons[0]->SetMessageTarget(this); - // m_pButtons[1] = new CUI3tButton(); m_pFrames->AttachChild(m_pButtons[1]); - // m_pButtons[1]->SetMessageTarget(this); - m_firstSkin = 0; Init(strSectionName); } CUISkinSelectorWnd::~CUISkinSelectorWnd() { - // xr_delete(m_pButtons[0]); - // xr_delete(m_pButtons[1]); - // xr_delete(m_pAnims[0]); - // xr_delete(m_pAnims[1]); - delete_data(m_skinsEnabled); } @@ -58,7 +46,7 @@ void CUISkinSelectorWnd::InitSkins() void CUISkinSelectorWnd::UpdateSkins() { - for (int i = 0; i < p_image_count; i++) + for (int i = 0; i < (int)m_pImage.size(); i++) { if (!!m_shader) m_pImage[i]->InitTextureEx(m_skins[i + m_firstSkin].c_str(), m_shader.c_str()); @@ -84,8 +72,10 @@ void CUISkinSelectorWnd::UpdateSkins() m_pImage[i]->Enable(it != m_skinsEnabled.end()); } - // m_pButtons[0]->Enable(m_firstSkin > 0); - // m_pButtons[1]->Enable(m_firstSkin + p_image_count < (int)m_skins.size()); + if (m_pButtons[0]) + m_pButtons[0]->Enable(m_firstSkin > 0); + if (m_pButtons[1]) + m_pButtons[1]->Enable(m_firstSkin + (int)m_pImage.size() < (int)m_skins.size()); } void CUISkinSelectorWnd::Init(const char* strSectionName) @@ -100,25 +90,50 @@ void CUISkinSelectorWnd::Init(const char* strSectionName) std::ignore = UIHelper::CreateStatic(xml_doc, "skin_selector:background", this); std::ignore = UIHelper::CreateStatic(xml_doc, "skin_selector:caption", this); - std::ignore = UIHelper::CreateStatic(xml_doc, "skin_selector:image_frames", this); - string64 buff; - for (int i = 0; i < p_image_count; i++) - { - xr_sprintf(buff, "skin_selector:image_%d", i); + const auto frames = UIHelper::CreateStatic(xml_doc, "skin_selector:image_frames", this); - m_pImage[i] = xr_new(); - m_pImage[i]->SetAutoDelete(true); - AttachChild(m_pImage[i]); + if (xml_doc.NavigateToNode("skin_selector:image_frames:a_static_1")) + { + m_pAnims[0] = xr_new(); + m_pAnims[0]->SetAutoDelete(true); + frames->AttachChild(m_pAnims[0]); + CUIXmlInit::InitAnimatedStatic(xml_doc, "skin_selector:image_frames:a_static_1", 0, m_pAnims[0]); + } - CUIXmlInit::InitStatic(xml_doc, buff, 0, m_pImage[i]); + if (xml_doc.NavigateToNode("skin_selector:image_frames:a_static_2")) + { + m_pAnims[1] = xr_new(); + m_pAnims[1]->SetAutoDelete(true); + frames->AttachChild(m_pAnims[1]); + CUIXmlInit::InitAnimatedStatic(xml_doc, "skin_selector:image_frames:a_static_2", 0, m_pAnims[1]); } - // CUIXmlInit::Init3tButton(xml_doc,"skin_selector:image_frames:btn_left", 0, m_pButtons[0]); - // CUIXmlInit::Init3tButton(xml_doc,"skin_selector:image_frames:btn_right",0, m_pButtons[1]); + m_pButtons[0] = UIHelper::Create3tButton(xml_doc,"skin_selector:image_frames:btn_left", frames, false); + m_pButtons[1] = UIHelper::Create3tButton(xml_doc,"skin_selector:image_frames:btn_right", frames, false); + + if (m_pButtons[0]) + m_pButtons[0]->SetMessageTarget(this); + if (m_pButtons[1]) + m_pButtons[1]->SetMessageTarget(this); + + int i = 0; + while (true) + { + string64 buff; + xr_sprintf(buff, "skin_selector:image_%d", i); + + if (!xml_doc.NavigateToNode(buff) && i != 0) // must have at least one image + break; + + const auto image = xr_new(); + image->SetAutoDelete(true); + AttachChild(image); - // CUIXmlInit::InitAnimatedStatic(xml_doc,"skin_selector:image_frames:a_static_1", 0, m_pAnims[0]); - // CUIXmlInit::InitAnimatedStatic(xml_doc,"skin_selector:image_frames:a_static_2", 0, m_pAnims[1]); + CUIXmlInit::InitStatic(xml_doc, buff, 0, image); + m_pImage.emplace_back(image); + ++i; + } m_pBtnAutoSelect = UIHelper::Create3tButton(xml_doc, "skin_selector:btn_autoselect", this); m_pBtnSpectator = UIHelper::Create3tButton(xml_doc, "skin_selector:btn_spectator", this); @@ -140,12 +155,12 @@ void CUISkinSelectorWnd::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) case BUTTON_CLICKED: game = smart_cast(&(Game())); // dm = smart_cast(&(Game())); - /* - if (pWnd == m_pButtons[0]) - OnKeyLeft(); - else if (pWnd == m_pButtons[1]) - OnKeyRight(); - else */ if (pWnd == m_pBtnAutoSelect) + + if (pWnd == m_pButtons[0]) + OnKeyLeft(); + else if (pWnd == m_pButtons[1]) + OnKeyRight(); + else if (pWnd == m_pBtnAutoSelect) { m_iActiveIndex = -1; OnBtnOK(); @@ -161,28 +176,36 @@ void CUISkinSelectorWnd::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) game->OnSkinMenuBack(); } else - for (int i = 0; i < p_image_count; i++) + { + for (int i = 0; i < (int)m_pImage.size(); i++) + { if (pWnd == m_pImage[i]) { m_iActiveIndex = m_firstSkin + i; OnBtnOK(); } + } + } break; case WINDOW_FOCUS_RECEIVED: - /* - if (pWnd == m_pButtons[0]) - { - m_pAnims[0]->Rewind(0); - m_pAnims[0]->Play(); - } - else if (pWnd == m_pButtons[1]) - { - m_pAnims[1]->Rewind(0); - m_pAnims[1]->Play(); - } - */ + if (m_pButtons[0] && pWnd == m_pButtons[0]) + { + if (m_pAnims[0]) + { + m_pAnims[0]->Rewind(0); + m_pAnims[0]->Play(); + } + } + else if (m_pButtons[1] && pWnd == m_pButtons[1]) + { + if (m_pAnims[1]) + { + m_pAnims[1]->Rewind(0); + m_pAnims[1]->Play(); + } + } break; - } + } // switch (msg) } void CUISkinSelectorWnd::OnBtnCancel() @@ -295,7 +318,7 @@ void CUISkinSelectorWnd::OnKeyLeft() void CUISkinSelectorWnd::OnKeyRight() { - if (m_firstSkin + p_image_count < (int)m_skins.size()) + if (m_firstSkin + (int)m_pImage.size() < (int)m_skins.size()) { m_firstSkin++; UpdateSkins(); @@ -327,10 +350,10 @@ void CUISkinSelectorWnd::SetCurSkin(int skin) m_iActiveIndex = skin; - if (m_iActiveIndex != -1 && (m_iActiveIndex < m_firstSkin || m_iActiveIndex > m_firstSkin + 5)) + if (m_iActiveIndex != -1 && (m_iActiveIndex < m_firstSkin || m_iActiveIndex > m_firstSkin + (m_pImage.size() - 1))) { - if (m_iActiveIndex > (int)m_skins.size() - p_image_count) - m_firstSkin = (int)m_skins.size() - p_image_count; + if (m_iActiveIndex > (int)m_skins.size() - (int)m_pImage.size()) + m_firstSkin = (int)m_skins.size() - (int)m_pImage.size(); else m_firstSkin = m_iActiveIndex; } diff --git a/src/xrGame/ui/UISkinSelector.h b/src/xrGame/ui/UISkinSelector.h index 069264a6b92..8e4f2c4dddd 100644 --- a/src/xrGame/ui/UISkinSelector.h +++ b/src/xrGame/ui/UISkinSelector.h @@ -6,9 +6,6 @@ #include "UIDialogWnd.h" -const u32 SKIN_TEX_HEIGHT = 232; -const u32 SKIN_TEX_WIDTH = 111; - class CUIStatic; class CUIStatix; class CUI3tButton; @@ -46,11 +43,9 @@ class CUISkinSelectorWnd final : public CUIDialogWnd void InitSkins(); void UpdateSkins(); - static constexpr size_t p_image_count = 6; - - CUIStatix* m_pImage[p_image_count]; - // CUI3tButton* m_pButtons[2]; - // CUIAnimatedStatic* m_pAnims[2]; + xr_vector m_pImage; + CUI3tButton* m_pButtons[2]; + CUIAnimatedStatic* m_pAnims[2]; CUI3tButton* m_pBtnAutoSelect; CUI3tButton* m_pBtnSpectator; CUI3tButton* m_pBtnBack; From c9c086d728181295cd8accd5ab7ebd96a0ff59fe Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 16:59:45 +0300 Subject: [PATCH 049/261] Support no texture stretching in CUIFrameLineWnd (#382, #392) --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 19 +++++++++++++++++-- src/xrUICore/Windows/UIFrameLineWnd.h | 5 +++-- src/xrUICore/XML/UIXmlInitBase.cpp | 10 ++++------ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index 44ff04c5ecc..3cc5618e436 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -170,13 +170,25 @@ void CUIFrameLineWnd::DrawElements() const Fvector2 lt, rb; if (bHorizontal) { + float height; + if (m_bStretchTexture && rect.height() > 0.0f) + height = rect.rb.y; + else + height = rect.lt.y + UI().ClientToScreenScaledY(tex_rect.height()); + lt = { rect.lt.x + cursor, rect.lt.y }; - rb = { lt.x + length, rect.rb.y }; + rb = { lt.x + length, height }; } else { + float width; + if (m_bStretchTexture && rect.width() > 0.0f) + width = rect.rb.x; + else + width = rect.lt.x + UI().ClientToScreenScaledX(tex_rect.width()); + lt = { rect.lt.x, rect.lt.y + cursor }; - rb = { rect.rb.x, lt.y + length }; + rb = { width, lt.y + length }; } cursor += length; @@ -232,6 +244,9 @@ void CUIFrameLineWnd::FillDebugInfo() ImGui::Checkbox("Enable texture", &m_bTextureVisible); + ImGui::SameLine(); + ImGui::Checkbox("Stretch texture", &m_bStretchTexture); + ImGui::SameLine(); ImGui::Checkbox("Horizontal", &bHorizontal); diff --git a/src/xrUICore/Windows/UIFrameLineWnd.h b/src/xrUICore/Windows/UIFrameLineWnd.h index 08920bf0c3e..8259b9d9cdf 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.h +++ b/src/xrUICore/Windows/UIFrameLineWnd.h @@ -46,8 +46,8 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner void SetTextureColor(u32 cl) override { m_texture_color = cl; } u32 GetTextureColor() const override { return m_texture_color; } - void SetStretchTexture(bool /*stretch*/) override {} - bool GetStretchTexture() override { return false; } + void SetStretchTexture(bool stretch) override { m_bStretchTexture = stretch;} + bool GetStretchTexture() override { return m_bStretchTexture; } void SetTextureVisible(bool value) { m_bTextureVisible = value; } void SetShader(const ui_shader& sh) @@ -68,6 +68,7 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner u32 m_texture_color{ color_argb(255, 255, 255, 255) }; bool m_bTextureVisible{ false }; + bool m_bStretchTexture{ true }; bool bHorizontal{ true }; Frect m_tex_rect[flMax]{}; diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 627ab7f1fe0..4fa47441047 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -784,12 +784,9 @@ bool CUIXmlInitBase::InitFrameLine(CUIXml& xml_doc, pcstr path, int index, CUIFr return false; } - string256 buf; - - if (xml_doc.ReadAttribInt(path, index, "stretch")) - { - //. pWnd->SetStretchTexture( stretch_flag ); - } + const int stretch_default = ClearSkyMode || ShadowOfChernobylMode ? 0 : 1; + const int stretch_flag = xml_doc.ReadAttribInt(path, index, "stretch", stretch_default); + pWnd->SetStretchTexture(stretch_flag); Fvector2 pos, size; pos.x = xml_doc.ReadAttribFlt(path, index, "x"); @@ -801,6 +798,7 @@ bool CUIXmlInitBase::InitFrameLine(CUIXml& xml_doc, pcstr path, int index, CUIFr size.y = xml_doc.ReadAttribFlt(path, index, "height"); const bool vertical = !!xml_doc.ReadAttribInt(path, index, "vertical"); + string256 buf; strconcat(buf, path, ":texture"); const shared_str base_name = xml_doc.Read(buf, index, nullptr); From 8a5b131d3f95ab26dd492cb2683865b44c18db5d Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 16:59:45 +0300 Subject: [PATCH 050/261] Support no texture stretching in CUIFrameLineWnd (#382, #392) --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 19 +++++++++++++++++-- src/xrUICore/Windows/UIFrameLineWnd.h | 5 +++-- src/xrUICore/XML/UIXmlInitBase.cpp | 10 ++++------ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index 44ff04c5ecc..3cc5618e436 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -170,13 +170,25 @@ void CUIFrameLineWnd::DrawElements() const Fvector2 lt, rb; if (bHorizontal) { + float height; + if (m_bStretchTexture && rect.height() > 0.0f) + height = rect.rb.y; + else + height = rect.lt.y + UI().ClientToScreenScaledY(tex_rect.height()); + lt = { rect.lt.x + cursor, rect.lt.y }; - rb = { lt.x + length, rect.rb.y }; + rb = { lt.x + length, height }; } else { + float width; + if (m_bStretchTexture && rect.width() > 0.0f) + width = rect.rb.x; + else + width = rect.lt.x + UI().ClientToScreenScaledX(tex_rect.width()); + lt = { rect.lt.x, rect.lt.y + cursor }; - rb = { rect.rb.x, lt.y + length }; + rb = { width, lt.y + length }; } cursor += length; @@ -232,6 +244,9 @@ void CUIFrameLineWnd::FillDebugInfo() ImGui::Checkbox("Enable texture", &m_bTextureVisible); + ImGui::SameLine(); + ImGui::Checkbox("Stretch texture", &m_bStretchTexture); + ImGui::SameLine(); ImGui::Checkbox("Horizontal", &bHorizontal); diff --git a/src/xrUICore/Windows/UIFrameLineWnd.h b/src/xrUICore/Windows/UIFrameLineWnd.h index 08920bf0c3e..8259b9d9cdf 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.h +++ b/src/xrUICore/Windows/UIFrameLineWnd.h @@ -46,8 +46,8 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner void SetTextureColor(u32 cl) override { m_texture_color = cl; } u32 GetTextureColor() const override { return m_texture_color; } - void SetStretchTexture(bool /*stretch*/) override {} - bool GetStretchTexture() override { return false; } + void SetStretchTexture(bool stretch) override { m_bStretchTexture = stretch;} + bool GetStretchTexture() override { return m_bStretchTexture; } void SetTextureVisible(bool value) { m_bTextureVisible = value; } void SetShader(const ui_shader& sh) @@ -68,6 +68,7 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner u32 m_texture_color{ color_argb(255, 255, 255, 255) }; bool m_bTextureVisible{ false }; + bool m_bStretchTexture{ true }; bool bHorizontal{ true }; Frect m_tex_rect[flMax]{}; diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 627ab7f1fe0..4fa47441047 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -784,12 +784,9 @@ bool CUIXmlInitBase::InitFrameLine(CUIXml& xml_doc, pcstr path, int index, CUIFr return false; } - string256 buf; - - if (xml_doc.ReadAttribInt(path, index, "stretch")) - { - //. pWnd->SetStretchTexture( stretch_flag ); - } + const int stretch_default = ClearSkyMode || ShadowOfChernobylMode ? 0 : 1; + const int stretch_flag = xml_doc.ReadAttribInt(path, index, "stretch", stretch_default); + pWnd->SetStretchTexture(stretch_flag); Fvector2 pos, size; pos.x = xml_doc.ReadAttribFlt(path, index, "x"); @@ -801,6 +798,7 @@ bool CUIXmlInitBase::InitFrameLine(CUIXml& xml_doc, pcstr path, int index, CUIFr size.y = xml_doc.ReadAttribFlt(path, index, "height"); const bool vertical = !!xml_doc.ReadAttribInt(path, index, "vertical"); + string256 buf; strconcat(buf, path, ":texture"); const shared_str base_name = xml_doc.Read(buf, index, nullptr); From 4cc5d958e0d13e8599f08dd69703cf5883745f4f Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 18:09:15 +0300 Subject: [PATCH 051/261] UI Debugger: ability to trigger a breakpoint on selected element. --- src/xrUICore/ui_debug.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/xrUICore/ui_debug.cpp b/src/xrUICore/ui_debug.cpp index 9664cacd9fc..c2129dc17a4 100644 --- a/src/xrUICore/ui_debug.cpp +++ b/src/xrUICore/ui_debug.cpp @@ -170,8 +170,18 @@ void CUIDebugger::on_tool_frame() } ImGui::TableNextColumn(); if (m_state.selected) - m_state.selected->FillDebugInfo(); + { + const bool debug = xrDebug::DebuggerIsPresent(); + if (ImGui::Button("Debug break at element")) + { + if (debug) + DEBUG_BREAK; + } + if (!debug) + ImGui::SetItemTooltip("No debugger attached to the process"); + m_state.selected->FillDebugInfo(); + } ImGui::EndTable(); } } From b0221fb3e0959539433eeb6c409fea137e257b1f Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 18:09:15 +0300 Subject: [PATCH 052/261] UI Debugger: ability to trigger a breakpoint on selected element. --- src/xrUICore/ui_debug.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/xrUICore/ui_debug.cpp b/src/xrUICore/ui_debug.cpp index 9664cacd9fc..c2129dc17a4 100644 --- a/src/xrUICore/ui_debug.cpp +++ b/src/xrUICore/ui_debug.cpp @@ -170,8 +170,18 @@ void CUIDebugger::on_tool_frame() } ImGui::TableNextColumn(); if (m_state.selected) - m_state.selected->FillDebugInfo(); + { + const bool debug = xrDebug::DebuggerIsPresent(); + if (ImGui::Button("Debug break at element")) + { + if (debug) + DEBUG_BREAK; + } + if (!debug) + ImGui::SetItemTooltip("No debugger attached to the process"); + m_state.selected->FillDebugInfo(); + } ImGui::EndTable(); } } From 7dbe689faf6cc72b5cba55611dcfeffac6c60abc Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 20:55:45 +0300 Subject: [PATCH 053/261] Include/xrRender/FactoryPtr.h: make operator bool explicit --- src/Include/xrRender/FactoryPtr.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Include/xrRender/FactoryPtr.h b/src/Include/xrRender/FactoryPtr.h index 849ff849b20..f5af705f915 100644 --- a/src/Include/xrRender/FactoryPtr.h +++ b/src/Include/xrRender/FactoryPtr.h @@ -64,7 +64,11 @@ class FactoryPtr T& operator*() const { return *m_pObject; } T* operator->() const { return m_pObject; } - operator bool() const { return m_pObject; } + + [[nodiscard]] + explicit operator bool() const { return m_pObject; } + + [[nodiscard]] bool operator!() const { return m_pObject == nullptr; } private: From 6705fcb9c0f1e51950039e2cf9b3d9e40bd587c7 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 20:55:45 +0300 Subject: [PATCH 054/261] Include/xrRender/FactoryPtr.h: make operator bool explicit --- src/Include/xrRender/FactoryPtr.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Include/xrRender/FactoryPtr.h b/src/Include/xrRender/FactoryPtr.h index 849ff849b20..f5af705f915 100644 --- a/src/Include/xrRender/FactoryPtr.h +++ b/src/Include/xrRender/FactoryPtr.h @@ -64,7 +64,11 @@ class FactoryPtr T& operator*() const { return *m_pObject; } T* operator->() const { return m_pObject; } - operator bool() const { return m_pObject; } + + [[nodiscard]] + explicit operator bool() const { return m_pObject; } + + [[nodiscard]] bool operator!() const { return m_pObject == nullptr; } private: From dc0ee8233a80a152f8a92386cda174a1473c1f7e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 21:08:45 +0300 Subject: [PATCH 055/261] Ability to compare ui_shader instances --- src/Include/xrRender/FactoryPtr.h | 6 ++++++ src/Include/xrRender/UIShader.h | 3 +++ src/Layers/xrRender/dxUIShader.cpp | 5 +++++ src/Layers/xrRender/dxUIShader.h | 2 ++ 4 files changed, 16 insertions(+) diff --git a/src/Include/xrRender/FactoryPtr.h b/src/Include/xrRender/FactoryPtr.h index f5af705f915..4f3c3461e65 100644 --- a/src/Include/xrRender/FactoryPtr.h +++ b/src/Include/xrRender/FactoryPtr.h @@ -71,6 +71,12 @@ class FactoryPtr [[nodiscard]] bool operator!() const { return m_pObject == nullptr; } + [[nodiscard]] + bool operator==(const FactoryPtr& other) const + { + return *m_pObject == *other.m_pObject; + } + private: void CreateObject(); void DestroyObject(); diff --git a/src/Include/xrRender/UIShader.h b/src/Include/xrRender/UIShader.h index 5db35eb1314..5acc895df18 100644 --- a/src/Include/xrRender/UIShader.h +++ b/src/Include/xrRender/UIShader.h @@ -11,6 +11,9 @@ class IUIShader virtual bool inited() = 0; virtual void destroy() = 0; + [[nodiscard]] + virtual bool operator==(const IUIShader& other) const = 0; + virtual bool GetBaseTextureResolution(Fvector2& res) = 0; virtual xrImTextureData GetImGuiTextureId() = 0; }; diff --git a/src/Layers/xrRender/dxUIShader.cpp b/src/Layers/xrRender/dxUIShader.cpp index cdb22fde13a..26d273e2980 100644 --- a/src/Layers/xrRender/dxUIShader.cpp +++ b/src/Layers/xrRender/dxUIShader.cpp @@ -7,6 +7,11 @@ void dxUIShader::Copy(IUIShader& _in) { *this = *((dxUIShader*)&_in); } void dxUIShader::create(LPCSTR sh, LPCSTR tex) { hShader.create(sh, tex); } void dxUIShader::destroy() { hShader.destroy(); } +bool dxUIShader::operator==(const IUIShader& other) const +{ + return hShader == static_cast(other).hShader; +} + CTexture* dxUIShader::GetBaseTexture() const { if (!hShader) diff --git a/src/Layers/xrRender/dxUIShader.h b/src/Layers/xrRender/dxUIShader.h index 14d9c6618b8..0a6b9946e72 100644 --- a/src/Layers/xrRender/dxUIShader.h +++ b/src/Layers/xrRender/dxUIShader.h @@ -17,6 +17,8 @@ class dxUIShader : public IUIShader virtual bool inited() { return hShader; } virtual void destroy(); + bool operator==(const IUIShader& other) const override; + CTexture* GetBaseTexture() const; bool GetBaseTextureResolution(Fvector2& res) override; xrImTextureData GetImGuiTextureId() override; From ac28831e39ea80382a7edb679ae2a5ba0ce40122 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 21:08:45 +0300 Subject: [PATCH 056/261] Ability to compare ui_shader instances --- src/Include/xrRender/FactoryPtr.h | 6 ++++++ src/Include/xrRender/UIShader.h | 3 +++ src/Layers/xrRender/dxUIShader.cpp | 5 +++++ src/Layers/xrRender/dxUIShader.h | 2 ++ 4 files changed, 16 insertions(+) diff --git a/src/Include/xrRender/FactoryPtr.h b/src/Include/xrRender/FactoryPtr.h index f5af705f915..4f3c3461e65 100644 --- a/src/Include/xrRender/FactoryPtr.h +++ b/src/Include/xrRender/FactoryPtr.h @@ -71,6 +71,12 @@ class FactoryPtr [[nodiscard]] bool operator!() const { return m_pObject == nullptr; } + [[nodiscard]] + bool operator==(const FactoryPtr& other) const + { + return *m_pObject == *other.m_pObject; + } + private: void CreateObject(); void DestroyObject(); diff --git a/src/Include/xrRender/UIShader.h b/src/Include/xrRender/UIShader.h index 5db35eb1314..5acc895df18 100644 --- a/src/Include/xrRender/UIShader.h +++ b/src/Include/xrRender/UIShader.h @@ -11,6 +11,9 @@ class IUIShader virtual bool inited() = 0; virtual void destroy() = 0; + [[nodiscard]] + virtual bool operator==(const IUIShader& other) const = 0; + virtual bool GetBaseTextureResolution(Fvector2& res) = 0; virtual xrImTextureData GetImGuiTextureId() = 0; }; diff --git a/src/Layers/xrRender/dxUIShader.cpp b/src/Layers/xrRender/dxUIShader.cpp index cdb22fde13a..26d273e2980 100644 --- a/src/Layers/xrRender/dxUIShader.cpp +++ b/src/Layers/xrRender/dxUIShader.cpp @@ -7,6 +7,11 @@ void dxUIShader::Copy(IUIShader& _in) { *this = *((dxUIShader*)&_in); } void dxUIShader::create(LPCSTR sh, LPCSTR tex) { hShader.create(sh, tex); } void dxUIShader::destroy() { hShader.destroy(); } +bool dxUIShader::operator==(const IUIShader& other) const +{ + return hShader == static_cast(other).hShader; +} + CTexture* dxUIShader::GetBaseTexture() const { if (!hShader) diff --git a/src/Layers/xrRender/dxUIShader.h b/src/Layers/xrRender/dxUIShader.h index 14d9c6618b8..0a6b9946e72 100644 --- a/src/Layers/xrRender/dxUIShader.h +++ b/src/Layers/xrRender/dxUIShader.h @@ -17,6 +17,8 @@ class dxUIShader : public IUIShader virtual bool inited() { return hShader; } virtual void destroy(); + bool operator==(const IUIShader& other) const override; + CTexture* GetBaseTexture() const; bool GetBaseTextureResolution(Fvector2& res) override; xrImTextureData GetImGuiTextureId() override; From 101764269fb617f5857c0ab44f6d1dd05715d93e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 21:13:01 +0300 Subject: [PATCH 057/261] xrUICore/Windows/UIFrameLineWnd.cpp: reapply one shader optimization Original COP code calls StartPrimitive and FlushPrimitive only once. We now do the same again, if all texture fragments all in the same file. --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index 3cc5618e436..a3b03711097 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -199,13 +199,22 @@ void CUIFrameLineWnd::DrawElements() const draw_rect(lt, rb, tex_rect.lt, tex_rect.rb, m_texture_color, ts); }; + bool one_shader = false; + if (m_shader[flFirst] == m_shader[flBack] && m_shader[flBack] == m_shader[flSecond]) + one_shader = true; + // first - GEnv.UIRender->StartPrimitive(6, IUIRender::ptTriList, UI().m_currentPointType); + const auto first_tiles = one_shader ? total_tiles + 2 : 1; + GEnv.UIRender->StartPrimitive(6 * first_tiles, IUIRender::ptTriList, UI().m_currentPointType); draw_tile(first_len, m_tex_rect[flFirst], flFirst); - GEnv.UIRender->FlushPrimitive(); + + if (!one_shader) + { + GEnv.UIRender->FlushPrimitive(); + GEnv.UIRender->StartPrimitive(6 * total_tiles, IUIRender::ptTriList, UI().m_currentPointType); + } // back - GEnv.UIRender->StartPrimitive(6 * total_tiles, IUIRender::ptTriList, UI().m_currentPointType); for (u32 i = 0; i < back_tiles; ++i) { draw_tile(back_len, m_tex_rect[flBack], flBack); @@ -221,10 +230,14 @@ void CUIFrameLineWnd::DrawElements() const draw_tile(back_len * back_remainder, remainder_tc, flBack); } - GEnv.UIRender->FlushPrimitive(); + + if (!one_shader) + { + GEnv.UIRender->FlushPrimitive(); + GEnv.UIRender->StartPrimitive(6, IUIRender::ptTriList, UI().m_currentPointType); + } // second - GEnv.UIRender->StartPrimitive(6, IUIRender::ptTriList, UI().m_currentPointType); draw_tile(second_len, m_tex_rect[flSecond], flSecond); GEnv.UIRender->FlushPrimitive(); } From 39dbc0be45cd3ed7c67c0ba766d9dcf72e26bf9a Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 21:13:01 +0300 Subject: [PATCH 058/261] xrUICore/Windows/UIFrameLineWnd.cpp: reapply one shader optimization Original COP code calls StartPrimitive and FlushPrimitive only once. We now do the same again, if all texture fragments all in the same file. --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index 3cc5618e436..a3b03711097 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -199,13 +199,22 @@ void CUIFrameLineWnd::DrawElements() const draw_rect(lt, rb, tex_rect.lt, tex_rect.rb, m_texture_color, ts); }; + bool one_shader = false; + if (m_shader[flFirst] == m_shader[flBack] && m_shader[flBack] == m_shader[flSecond]) + one_shader = true; + // first - GEnv.UIRender->StartPrimitive(6, IUIRender::ptTriList, UI().m_currentPointType); + const auto first_tiles = one_shader ? total_tiles + 2 : 1; + GEnv.UIRender->StartPrimitive(6 * first_tiles, IUIRender::ptTriList, UI().m_currentPointType); draw_tile(first_len, m_tex_rect[flFirst], flFirst); - GEnv.UIRender->FlushPrimitive(); + + if (!one_shader) + { + GEnv.UIRender->FlushPrimitive(); + GEnv.UIRender->StartPrimitive(6 * total_tiles, IUIRender::ptTriList, UI().m_currentPointType); + } // back - GEnv.UIRender->StartPrimitive(6 * total_tiles, IUIRender::ptTriList, UI().m_currentPointType); for (u32 i = 0; i < back_tiles; ++i) { draw_tile(back_len, m_tex_rect[flBack], flBack); @@ -221,10 +230,14 @@ void CUIFrameLineWnd::DrawElements() const draw_tile(back_len * back_remainder, remainder_tc, flBack); } - GEnv.UIRender->FlushPrimitive(); + + if (!one_shader) + { + GEnv.UIRender->FlushPrimitive(); + GEnv.UIRender->StartPrimitive(6, IUIRender::ptTriList, UI().m_currentPointType); + } // second - GEnv.UIRender->StartPrimitive(6, IUIRender::ptTriList, UI().m_currentPointType); draw_tile(second_len, m_tex_rect[flSecond], flSecond); GEnv.UIRender->FlushPrimitive(); } From 9f4264af18a05e62f5c7713c16e323b9d092aea0 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 21:28:18 +0300 Subject: [PATCH 059/261] xrUICore/Windows/UIFrameLineWnd.cpp: remove size match assertions That's not critical, new code should work with that. --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 41 +++++++------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index a3b03711097..2d20009dc14 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -47,41 +47,22 @@ bool CUIFrameLineWnd::InitTextureEx(pcstr texture, pcstr shader, bool fatal /*= failed |= !e_exist; } - const bool B_and_E_are_similar_by_height = fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flSecond].height()); - const bool B_and_Back_are_similar_by_height = fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flBack].height()); - const bool B_and_E_are_similar_by_width = fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flSecond].width()); - const bool B_and_Back_are_similar_by_width = fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flBack].width()); - - if (fatal) +#ifndef MASTER_GOLD + if (bHorizontal) { - if (bHorizontal) - { - R_ASSERT2(B_and_E_are_similar_by_height, texture); - R_ASSERT2(B_and_Back_are_similar_by_height, texture); - } - else - { - R_ASSERT2(B_and_E_are_similar_by_width, texture); - R_ASSERT2(B_and_Back_are_similar_by_width, texture); - } + if (b_exist && e_exist && !fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flSecond].height())) + Msg("~ Textures %s_b and %s_e are not similar by height", texture, texture); + if (b_exist && back_exist && !fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flBack].height())) + Msg("~ Textures %s_b and %s_back are not similar by height", texture, texture); } else { - if (bHorizontal) - { - if (!B_and_E_are_similar_by_height && b_exist && e_exist) - Msg("! Textures %s_b and %s_e are not similar by height", texture, texture); - if (!B_and_Back_are_similar_by_height && b_exist && back_exist) - Msg("! Textures %s_b and %s_back are not similar by height", texture, texture); - } - else - { - if (!B_and_E_are_similar_by_width && b_exist && e_exist) - Msg("! Textures %s_b and %s_e are not similar by width", texture, texture); - if (!B_and_Back_are_similar_by_width && b_exist && back_exist) - Msg("! Textures %s_b and %s_back are not similar by width", texture, texture); - } + if (b_exist && e_exist && !fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flSecond].width())) + Msg("~ Textures %s_b and %s_e are not similar by width", texture, texture); + if (b_exist && back_exist && !fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flBack].width())) + Msg("~ Textures %s_b and %s_back are not similar by width", texture, texture); } +#endif m_bTextureVisible = !failed; return !failed; From bc41defb8d3c1a9a43b066d3fbd85c24dd9ee223 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 21:28:18 +0300 Subject: [PATCH 060/261] xrUICore/Windows/UIFrameLineWnd.cpp: remove size match assertions That's not critical, new code should work with that. --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 41 +++++++------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index a3b03711097..2d20009dc14 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -47,41 +47,22 @@ bool CUIFrameLineWnd::InitTextureEx(pcstr texture, pcstr shader, bool fatal /*= failed |= !e_exist; } - const bool B_and_E_are_similar_by_height = fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flSecond].height()); - const bool B_and_Back_are_similar_by_height = fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flBack].height()); - const bool B_and_E_are_similar_by_width = fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flSecond].width()); - const bool B_and_Back_are_similar_by_width = fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flBack].width()); - - if (fatal) +#ifndef MASTER_GOLD + if (bHorizontal) { - if (bHorizontal) - { - R_ASSERT2(B_and_E_are_similar_by_height, texture); - R_ASSERT2(B_and_Back_are_similar_by_height, texture); - } - else - { - R_ASSERT2(B_and_E_are_similar_by_width, texture); - R_ASSERT2(B_and_Back_are_similar_by_width, texture); - } + if (b_exist && e_exist && !fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flSecond].height())) + Msg("~ Textures %s_b and %s_e are not similar by height", texture, texture); + if (b_exist && back_exist && !fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flBack].height())) + Msg("~ Textures %s_b and %s_back are not similar by height", texture, texture); } else { - if (bHorizontal) - { - if (!B_and_E_are_similar_by_height && b_exist && e_exist) - Msg("! Textures %s_b and %s_e are not similar by height", texture, texture); - if (!B_and_Back_are_similar_by_height && b_exist && back_exist) - Msg("! Textures %s_b and %s_back are not similar by height", texture, texture); - } - else - { - if (!B_and_E_are_similar_by_width && b_exist && e_exist) - Msg("! Textures %s_b and %s_e are not similar by width", texture, texture); - if (!B_and_Back_are_similar_by_width && b_exist && back_exist) - Msg("! Textures %s_b and %s_back are not similar by width", texture, texture); - } + if (b_exist && e_exist && !fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flSecond].width())) + Msg("~ Textures %s_b and %s_e are not similar by width", texture, texture); + if (b_exist && back_exist && !fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flBack].width())) + Msg("~ Textures %s_b and %s_back are not similar by width", texture, texture); } +#endif m_bTextureVisible = !failed; return !failed; From e4a0c585a5051265801ab495d9c3ecc477db937b Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 21:40:56 +0300 Subject: [PATCH 061/261] xrCore/FS.cpp: remove nagging verify File size can be 0 sometimes. --- src/xrCore/FS.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xrCore/FS.cpp b/src/xrCore/FS.cpp index 6a7fd7cd6a1..af923d279d8 100644 --- a/src/xrCore/FS.cpp +++ b/src/xrCore/FS.cpp @@ -115,7 +115,6 @@ bool file_handle_internal(pcstr file_name, size_t& size, int& file_handle) void* FileDownload(pcstr file_name, const int& file_handle, size_t& file_size) { - VERIFY(file_size != 0); void* buffer = xr_malloc(file_size); const auto r_bytes = _read(file_handle, buffer, file_size); From d3170a474130ba4936d688b9e46938796fdebd14 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 21:40:56 +0300 Subject: [PATCH 062/261] xrCore/FS.cpp: remove nagging verify File size can be 0 sometimes. --- src/xrCore/FS.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xrCore/FS.cpp b/src/xrCore/FS.cpp index 6a7fd7cd6a1..af923d279d8 100644 --- a/src/xrCore/FS.cpp +++ b/src/xrCore/FS.cpp @@ -115,7 +115,6 @@ bool file_handle_internal(pcstr file_name, size_t& size, int& file_handle) void* FileDownload(pcstr file_name, const int& file_handle, size_t& file_size) { - VERIFY(file_size != 0); void* buffer = xr_malloc(file_size); const auto r_bytes = _read(file_handle, buffer, file_size); From 9d6fad2dde1663c60e61c61451c19dc2452527cd Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 22:46:32 +0300 Subject: [PATCH 063/261] Generalize xml initialization code for CUIFrameLineWnd --- src/xrUICore/ListBox/UIListBox.cpp | 4 +-- src/xrUICore/ListWnd/UIListWnd.cpp | 4 +-- src/xrUICore/ScrollBar/UIScrollBar.cpp | 8 ++++-- src/xrUICore/Windows/UIFrameLineWnd.cpp | 16 +---------- src/xrUICore/Windows/UIFrameLineWnd.h | 8 ++---- src/xrUICore/Windows/UITextFrameLineWnd.cpp | 20 +------------ src/xrUICore/Windows/UITextFrameLineWnd.h | 16 +---------- src/xrUICore/XML/UIXmlInitBase.cpp | 32 ++++++--------------- src/xrUICore/ui_export_script.cpp | 7 +++-- 9 files changed, 28 insertions(+), 87 deletions(-) diff --git a/src/xrUICore/ListBox/UIListBox.cpp b/src/xrUICore/ListBox/UIListBox.cpp index 5964ded1f9f..453f7904f1f 100644 --- a/src/xrUICore/ListBox/UIListBox.cpp +++ b/src/xrUICore/ListBox/UIListBox.cpp @@ -50,7 +50,7 @@ CUIListBoxItem* CUIListBox::AddTextItem(LPCSTR text) CUIListBoxItem* CUIListBox::AddItem() { CUIListBoxItem* item = xr_new(m_def_item_height); - item->InitFrameLineWnd(Fvector2().set(0, 0), Fvector2().set(GetDesiredChildWidth() - 5, m_def_item_height)); + item->SetWndSize({ GetDesiredChildWidth() - 5, m_def_item_height }); item->GetTextItem()->SetWidth(GetDesiredChildWidth()); item->SetWidth(GetDesiredChildWidth()); @@ -68,7 +68,7 @@ CUIListBoxItem* CUIListBox::AddItem() void CUIListBox::AddExistingItem(CUIListBoxItem* item) { - item->InitFrameLineWnd(Fvector2().set(0, 0), Fvector2().set(GetDesiredChildWidth() - 5, m_def_item_height)); + item->SetWndSize({ GetDesiredChildWidth() - 5, m_def_item_height }); item->SetWidth(GetDesiredChildWidth()); if (m_selection_texture.size()) diff --git a/src/xrUICore/ListWnd/UIListWnd.cpp b/src/xrUICore/ListWnd/UIListWnd.cpp index 147d02d1d1a..1c3a4785478 100644 --- a/src/xrUICore/ListWnd/UIListWnd.cpp +++ b/src/xrUICore/ListWnd/UIListWnd.cpp @@ -651,8 +651,8 @@ void CUIListWnd::create_active_back() return; m_ActiveBackgroundFrame = xr_new("Active background frame"); - m_ActiveBackgroundFrame->InitFrameLineWnd("ui_listline", Fvector2().set(0.0f, 0.0f), - Fvector2().set(GetWidth(), 18.0f)); + m_ActiveBackgroundFrame->SetWndSize({ GetWidth(), 18.0f }); + m_ActiveBackgroundFrame->InitTexture("ui_listline"); } void CUIListWnd::destroy_active_back() diff --git a/src/xrUICore/ScrollBar/UIScrollBar.cpp b/src/xrUICore/ScrollBar/UIScrollBar.cpp index 10956087b89..138e56f3332 100644 --- a/src/xrUICore/ScrollBar/UIScrollBar.cpp +++ b/src/xrUICore/ScrollBar/UIScrollBar.cpp @@ -137,7 +137,9 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, else SetWidth(tempBackground->GetWidth()); - m_FrameBackground->InitFrameLineWnd(GetWndPos(), GetWndSize(), m_bIsHorizontal); + m_FrameBackground->SetWndPos(GetWndPos()); + m_FrameBackground->SetWndSize(GetWndSize()); + m_FrameBackground->SetHorizontal(m_bIsHorizontal); m_FrameBackground->SetShader(tempBackground->GetShader()); m_FrameBackground->SetTextureRect(tempBackground->GetTextureRect(), CUIFrameLineWnd::flBack); @@ -148,7 +150,9 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, if (tempScroll && tempScroll->IsShown()) { - m_ScrollBox->InitFrameLineWnd(tempScroll->GetWndPos(), GetWndSize(), m_bIsHorizontal); + m_ScrollBox->SetWndPos(tempScroll->GetWndPos()); + m_ScrollBox->SetWndSize(GetWndSize()); + m_ScrollBox->SetHorizontal(m_bIsHorizontal); m_ScrollBox->SetShader(tempScroll->GetShader()); m_ScrollBox->SetTextureRect(tempScroll->GetTextureRect(), CUIFrameLineWnd::flBack); diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index 2d20009dc14..67067984342 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -5,23 +5,9 @@ CUIFrameLineWnd::CUIFrameLineWnd(pcstr window_name) : CUIWindow(window_name) {} -bool CUIFrameLineWnd::InitFrameLineWnd(pcstr base_name, Fvector2 pos, Fvector2 size, bool horizontal, bool fatal /*= true*/) -{ - InitFrameLineWnd(pos, size, horizontal); - return InitTexture(base_name, fatal); -} - -void CUIFrameLineWnd::InitFrameLineWnd(Fvector2 pos, Fvector2 size, bool horizontal) -{ - inherited::SetWndPos(pos); - inherited::SetWndSize(size); - - bHorizontal = horizontal; -} - bool CUIFrameLineWnd::InitTexture(pcstr texture, bool fatal /*= true*/) { - return InitTextureEx(texture, "hud" DELIMITER "default", fatal); + return InitTextureEx(texture, "hud\\default", fatal); } bool CUIFrameLineWnd::InitTextureEx(pcstr texture, pcstr shader, bool fatal /*= true*/) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.h b/src/xrUICore/Windows/UIFrameLineWnd.h index 8259b9d9cdf..561aff58a3a 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.h +++ b/src/xrUICore/Windows/UIFrameLineWnd.h @@ -16,8 +16,6 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner CUIFrameLineWnd(pcstr window_name); - bool InitFrameLineWnd(pcstr base_name, Fvector2 pos, Fvector2 size, bool horizontal = true, bool fatal = true); - void InitFrameLineWnd(Fvector2 pos, Fvector2 size, bool horizontal = true); bool InitTexture(pcstr texture, bool fatal = true) override; bool InitTextureEx(pcstr texture, pcstr shader = "hud" DELIMITER "default", bool fatal = true) override; @@ -33,14 +31,12 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner void SetTextureRect(const Frect& r) override { - VERIFY2(false, "This overload is not supposed to be called!!!"); - m_tex_rect[flBack] = r; + // we don't mess with it } const Frect& GetTextureRect() const override { - VERIFY2(false, "This overload is not supposed to be called!!!"); - return m_tex_rect[flBack]; + return m_tex_rect[flFirst]; } void SetTextureColor(u32 cl) override { m_texture_color = cl; } diff --git a/src/xrUICore/Windows/UITextFrameLineWnd.cpp b/src/xrUICore/Windows/UITextFrameLineWnd.cpp index 6b2086f165d..6101ebb22ff 100644 --- a/src/xrUICore/Windows/UITextFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UITextFrameLineWnd.cpp @@ -1,28 +1,10 @@ #include "pch.hpp" #include "UITextFrameLineWnd.h" -#include "XML/UITextureMaster.h" CUITextFrameLineWnd::CUITextFrameLineWnd() : CUIWindow(CUITextFrameLineWnd::GetDebugType()), - bHorizontal(true), m_frameline("Frameline"), m_title("Title") + m_frameline("Frameline"), m_title("Title") { AttachChild(&m_title); AttachChild(&m_frameline); } - -void CUITextFrameLineWnd::Init(pcstr baseTexture, Fvector2 pos, Fvector2 size, bool horizontal) -{ - m_frameline.InitFrameLineWnd(baseTexture, pos, size, horizontal); - - m_title.SetWndPos({ 0.0f, 0.0f }); - if (horizontal) - m_title.SetWndSize({ size.x, 50 }); - else - m_title.SetWndSize({ 50, size.y }); -} - -void CUITextFrameLineWnd::InitTexture(pcstr texture, bool horizontal) -{ - bHorizontal = horizontal; - m_frameline.InitFrameLineWnd(texture, GetWndPos(), GetWndSize(), horizontal); -} diff --git a/src/xrUICore/Windows/UITextFrameLineWnd.h b/src/xrUICore/Windows/UITextFrameLineWnd.h index d0e104d50c0..3064a53ae38 100644 --- a/src/xrUICore/Windows/UITextFrameLineWnd.h +++ b/src/xrUICore/Windows/UITextFrameLineWnd.h @@ -10,21 +10,8 @@ class XRUICORE_API CUITextFrameLineWnd final : public CUIWindow public: CUITextFrameLineWnd(); - void Init(pcstr baseTexture, Fvector2 pos, Fvector2 size, bool horizontal = true); - void InitTexture(pcstr texture, bool horizontal = true); - - virtual void SetOrientation(bool horizontal) - { - m_frameline.SetHorizontal(horizontal); - } - - void SetColor(u32 cl) - { - m_frameline.SetTextureColor(cl); - } - // Also we can display textual caption on the frame - CUIStatic* GetTitleStatic() { return &m_title; }; + CUIStatic* GetTitleStatic() { return &m_title; } void SetText(pcstr text) { m_title.SetText(text); } pcstr GetDebugType() override { return "CUITextFrameLineWnd"; } @@ -32,7 +19,6 @@ class XRUICORE_API CUITextFrameLineWnd final : public CUIWindow protected: friend class CUIXmlInitBase; - bool bHorizontal; CUIFrameLineWnd m_frameline; CUIStatic m_title; }; diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 4fa47441047..644ac8822f2 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -784,32 +784,18 @@ bool CUIXmlInitBase::InitFrameLine(CUIXml& xml_doc, pcstr path, int index, CUIFr return false; } - const int stretch_default = ClearSkyMode || ShadowOfChernobylMode ? 0 : 1; - const int stretch_flag = xml_doc.ReadAttribInt(path, index, "stretch", stretch_default); - pWnd->SetStretchTexture(stretch_flag); - - Fvector2 pos, size; - pos.x = xml_doc.ReadAttribFlt(path, index, "x"); - pos.y = xml_doc.ReadAttribFlt(path, index, "y"); - - InitAlignment(xml_doc, path, index, pos.x, pos.y, pWnd); - - size.x = xml_doc.ReadAttribFlt(path, index, "width"); - size.y = xml_doc.ReadAttribFlt(path, index, "height"); const bool vertical = !!xml_doc.ReadAttribInt(path, index, "vertical"); + pWnd->SetHorizontal(!vertical); - string256 buf; - strconcat(buf, path, ":texture"); - const shared_str base_name = xml_doc.Read(buf, index, nullptr); - - VERIFY(base_name); - - const u32 color = GetColor(xml_doc, buf, index, 0xff); - pWnd->SetTextureColor(color); + bool result = InitWindow(xml_doc, path, index, pWnd, fatal); + result &= InitTexture(xml_doc, path, index, pWnd, fatal); - InitWindow(xml_doc, path, index, pWnd); + // Reapply stretch (previously applied in InitTexture) as a special case + const int stretch_default = ClearSkyMode || ShadowOfChernobylMode ? 0 : 1; + const int stretch_flag = xml_doc.ReadAttribInt(path, index, "stretch", stretch_default); + pWnd->SetStretchTexture(stretch_flag); - return pWnd->InitFrameLineWnd(base_name.c_str(), pos, size, !vertical, fatal); + return result; } bool CUIXmlInitBase::InitTextFrameLine(CUIXml& xml_doc, pcstr path, int index, CUITextFrameLineWnd* pWnd, bool fatal /*= true*/) @@ -931,7 +917,7 @@ bool CUIXmlInitBase::InitTexture(const CUIXml& xml_doc, pcstr path, int index, I const u32 color = GetColor(xml_doc, buf, index, 0xff); pWnd->SetTextureColor(color); - if (rect.width() != 0 && rect.height() != 0) + if (!fis_zero(rect.width()) && !fis_zero(rect.height())) pWnd->SetTextureRect(rect); return result; diff --git a/src/xrUICore/ui_export_script.cpp b/src/xrUICore/ui_export_script.cpp index df6af70db74..90902922988 100644 --- a/src/xrUICore/ui_export_script.cpp +++ b/src/xrUICore/ui_export_script.cpp @@ -391,9 +391,10 @@ void CUIFrameLineWnd::script_register(lua_State* luaState) .def("SetColor", &CUIFrameLineWnd::SetTextureColor) .def("Init", +[](CUIFrameLineWnd* self, cpcstr texture, float x, float y, float width, float height, bool horizontal) { - const Fvector2 pos { x, y }; - const Fvector2 size { width, height }; - self->InitFrameLineWnd(texture, pos, size, horizontal); + self->SetWndPos({ x, y }); + self->SetWndSize({ width, height }); + self->SetHorizontal(horizontal); + self->InitTexture(texture); }), class_("CUIFrameLineWnd") From ebfd1c2f70dac9029ee27b2630c814008f68fd01 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 20 Dec 2025 22:46:32 +0300 Subject: [PATCH 064/261] Generalize xml initialization code for CUIFrameLineWnd --- src/xrUICore/ListBox/UIListBox.cpp | 4 +-- src/xrUICore/ListWnd/UIListWnd.cpp | 4 +-- src/xrUICore/ScrollBar/UIScrollBar.cpp | 8 ++++-- src/xrUICore/Windows/UIFrameLineWnd.cpp | 16 +---------- src/xrUICore/Windows/UIFrameLineWnd.h | 8 ++---- src/xrUICore/Windows/UITextFrameLineWnd.cpp | 20 +------------ src/xrUICore/Windows/UITextFrameLineWnd.h | 16 +---------- src/xrUICore/XML/UIXmlInitBase.cpp | 32 ++++++--------------- src/xrUICore/ui_export_script.cpp | 7 +++-- 9 files changed, 28 insertions(+), 87 deletions(-) diff --git a/src/xrUICore/ListBox/UIListBox.cpp b/src/xrUICore/ListBox/UIListBox.cpp index 5964ded1f9f..453f7904f1f 100644 --- a/src/xrUICore/ListBox/UIListBox.cpp +++ b/src/xrUICore/ListBox/UIListBox.cpp @@ -50,7 +50,7 @@ CUIListBoxItem* CUIListBox::AddTextItem(LPCSTR text) CUIListBoxItem* CUIListBox::AddItem() { CUIListBoxItem* item = xr_new(m_def_item_height); - item->InitFrameLineWnd(Fvector2().set(0, 0), Fvector2().set(GetDesiredChildWidth() - 5, m_def_item_height)); + item->SetWndSize({ GetDesiredChildWidth() - 5, m_def_item_height }); item->GetTextItem()->SetWidth(GetDesiredChildWidth()); item->SetWidth(GetDesiredChildWidth()); @@ -68,7 +68,7 @@ CUIListBoxItem* CUIListBox::AddItem() void CUIListBox::AddExistingItem(CUIListBoxItem* item) { - item->InitFrameLineWnd(Fvector2().set(0, 0), Fvector2().set(GetDesiredChildWidth() - 5, m_def_item_height)); + item->SetWndSize({ GetDesiredChildWidth() - 5, m_def_item_height }); item->SetWidth(GetDesiredChildWidth()); if (m_selection_texture.size()) diff --git a/src/xrUICore/ListWnd/UIListWnd.cpp b/src/xrUICore/ListWnd/UIListWnd.cpp index 147d02d1d1a..1c3a4785478 100644 --- a/src/xrUICore/ListWnd/UIListWnd.cpp +++ b/src/xrUICore/ListWnd/UIListWnd.cpp @@ -651,8 +651,8 @@ void CUIListWnd::create_active_back() return; m_ActiveBackgroundFrame = xr_new("Active background frame"); - m_ActiveBackgroundFrame->InitFrameLineWnd("ui_listline", Fvector2().set(0.0f, 0.0f), - Fvector2().set(GetWidth(), 18.0f)); + m_ActiveBackgroundFrame->SetWndSize({ GetWidth(), 18.0f }); + m_ActiveBackgroundFrame->InitTexture("ui_listline"); } void CUIListWnd::destroy_active_back() diff --git a/src/xrUICore/ScrollBar/UIScrollBar.cpp b/src/xrUICore/ScrollBar/UIScrollBar.cpp index 10956087b89..138e56f3332 100644 --- a/src/xrUICore/ScrollBar/UIScrollBar.cpp +++ b/src/xrUICore/ScrollBar/UIScrollBar.cpp @@ -137,7 +137,9 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, else SetWidth(tempBackground->GetWidth()); - m_FrameBackground->InitFrameLineWnd(GetWndPos(), GetWndSize(), m_bIsHorizontal); + m_FrameBackground->SetWndPos(GetWndPos()); + m_FrameBackground->SetWndSize(GetWndSize()); + m_FrameBackground->SetHorizontal(m_bIsHorizontal); m_FrameBackground->SetShader(tempBackground->GetShader()); m_FrameBackground->SetTextureRect(tempBackground->GetTextureRect(), CUIFrameLineWnd::flBack); @@ -148,7 +150,9 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, if (tempScroll && tempScroll->IsShown()) { - m_ScrollBox->InitFrameLineWnd(tempScroll->GetWndPos(), GetWndSize(), m_bIsHorizontal); + m_ScrollBox->SetWndPos(tempScroll->GetWndPos()); + m_ScrollBox->SetWndSize(GetWndSize()); + m_ScrollBox->SetHorizontal(m_bIsHorizontal); m_ScrollBox->SetShader(tempScroll->GetShader()); m_ScrollBox->SetTextureRect(tempScroll->GetTextureRect(), CUIFrameLineWnd::flBack); diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index 2d20009dc14..67067984342 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -5,23 +5,9 @@ CUIFrameLineWnd::CUIFrameLineWnd(pcstr window_name) : CUIWindow(window_name) {} -bool CUIFrameLineWnd::InitFrameLineWnd(pcstr base_name, Fvector2 pos, Fvector2 size, bool horizontal, bool fatal /*= true*/) -{ - InitFrameLineWnd(pos, size, horizontal); - return InitTexture(base_name, fatal); -} - -void CUIFrameLineWnd::InitFrameLineWnd(Fvector2 pos, Fvector2 size, bool horizontal) -{ - inherited::SetWndPos(pos); - inherited::SetWndSize(size); - - bHorizontal = horizontal; -} - bool CUIFrameLineWnd::InitTexture(pcstr texture, bool fatal /*= true*/) { - return InitTextureEx(texture, "hud" DELIMITER "default", fatal); + return InitTextureEx(texture, "hud\\default", fatal); } bool CUIFrameLineWnd::InitTextureEx(pcstr texture, pcstr shader, bool fatal /*= true*/) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.h b/src/xrUICore/Windows/UIFrameLineWnd.h index 8259b9d9cdf..561aff58a3a 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.h +++ b/src/xrUICore/Windows/UIFrameLineWnd.h @@ -16,8 +16,6 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner CUIFrameLineWnd(pcstr window_name); - bool InitFrameLineWnd(pcstr base_name, Fvector2 pos, Fvector2 size, bool horizontal = true, bool fatal = true); - void InitFrameLineWnd(Fvector2 pos, Fvector2 size, bool horizontal = true); bool InitTexture(pcstr texture, bool fatal = true) override; bool InitTextureEx(pcstr texture, pcstr shader = "hud" DELIMITER "default", bool fatal = true) override; @@ -33,14 +31,12 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner void SetTextureRect(const Frect& r) override { - VERIFY2(false, "This overload is not supposed to be called!!!"); - m_tex_rect[flBack] = r; + // we don't mess with it } const Frect& GetTextureRect() const override { - VERIFY2(false, "This overload is not supposed to be called!!!"); - return m_tex_rect[flBack]; + return m_tex_rect[flFirst]; } void SetTextureColor(u32 cl) override { m_texture_color = cl; } diff --git a/src/xrUICore/Windows/UITextFrameLineWnd.cpp b/src/xrUICore/Windows/UITextFrameLineWnd.cpp index 6b2086f165d..6101ebb22ff 100644 --- a/src/xrUICore/Windows/UITextFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UITextFrameLineWnd.cpp @@ -1,28 +1,10 @@ #include "pch.hpp" #include "UITextFrameLineWnd.h" -#include "XML/UITextureMaster.h" CUITextFrameLineWnd::CUITextFrameLineWnd() : CUIWindow(CUITextFrameLineWnd::GetDebugType()), - bHorizontal(true), m_frameline("Frameline"), m_title("Title") + m_frameline("Frameline"), m_title("Title") { AttachChild(&m_title); AttachChild(&m_frameline); } - -void CUITextFrameLineWnd::Init(pcstr baseTexture, Fvector2 pos, Fvector2 size, bool horizontal) -{ - m_frameline.InitFrameLineWnd(baseTexture, pos, size, horizontal); - - m_title.SetWndPos({ 0.0f, 0.0f }); - if (horizontal) - m_title.SetWndSize({ size.x, 50 }); - else - m_title.SetWndSize({ 50, size.y }); -} - -void CUITextFrameLineWnd::InitTexture(pcstr texture, bool horizontal) -{ - bHorizontal = horizontal; - m_frameline.InitFrameLineWnd(texture, GetWndPos(), GetWndSize(), horizontal); -} diff --git a/src/xrUICore/Windows/UITextFrameLineWnd.h b/src/xrUICore/Windows/UITextFrameLineWnd.h index d0e104d50c0..3064a53ae38 100644 --- a/src/xrUICore/Windows/UITextFrameLineWnd.h +++ b/src/xrUICore/Windows/UITextFrameLineWnd.h @@ -10,21 +10,8 @@ class XRUICORE_API CUITextFrameLineWnd final : public CUIWindow public: CUITextFrameLineWnd(); - void Init(pcstr baseTexture, Fvector2 pos, Fvector2 size, bool horizontal = true); - void InitTexture(pcstr texture, bool horizontal = true); - - virtual void SetOrientation(bool horizontal) - { - m_frameline.SetHorizontal(horizontal); - } - - void SetColor(u32 cl) - { - m_frameline.SetTextureColor(cl); - } - // Also we can display textual caption on the frame - CUIStatic* GetTitleStatic() { return &m_title; }; + CUIStatic* GetTitleStatic() { return &m_title; } void SetText(pcstr text) { m_title.SetText(text); } pcstr GetDebugType() override { return "CUITextFrameLineWnd"; } @@ -32,7 +19,6 @@ class XRUICORE_API CUITextFrameLineWnd final : public CUIWindow protected: friend class CUIXmlInitBase; - bool bHorizontal; CUIFrameLineWnd m_frameline; CUIStatic m_title; }; diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 4fa47441047..644ac8822f2 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -784,32 +784,18 @@ bool CUIXmlInitBase::InitFrameLine(CUIXml& xml_doc, pcstr path, int index, CUIFr return false; } - const int stretch_default = ClearSkyMode || ShadowOfChernobylMode ? 0 : 1; - const int stretch_flag = xml_doc.ReadAttribInt(path, index, "stretch", stretch_default); - pWnd->SetStretchTexture(stretch_flag); - - Fvector2 pos, size; - pos.x = xml_doc.ReadAttribFlt(path, index, "x"); - pos.y = xml_doc.ReadAttribFlt(path, index, "y"); - - InitAlignment(xml_doc, path, index, pos.x, pos.y, pWnd); - - size.x = xml_doc.ReadAttribFlt(path, index, "width"); - size.y = xml_doc.ReadAttribFlt(path, index, "height"); const bool vertical = !!xml_doc.ReadAttribInt(path, index, "vertical"); + pWnd->SetHorizontal(!vertical); - string256 buf; - strconcat(buf, path, ":texture"); - const shared_str base_name = xml_doc.Read(buf, index, nullptr); - - VERIFY(base_name); - - const u32 color = GetColor(xml_doc, buf, index, 0xff); - pWnd->SetTextureColor(color); + bool result = InitWindow(xml_doc, path, index, pWnd, fatal); + result &= InitTexture(xml_doc, path, index, pWnd, fatal); - InitWindow(xml_doc, path, index, pWnd); + // Reapply stretch (previously applied in InitTexture) as a special case + const int stretch_default = ClearSkyMode || ShadowOfChernobylMode ? 0 : 1; + const int stretch_flag = xml_doc.ReadAttribInt(path, index, "stretch", stretch_default); + pWnd->SetStretchTexture(stretch_flag); - return pWnd->InitFrameLineWnd(base_name.c_str(), pos, size, !vertical, fatal); + return result; } bool CUIXmlInitBase::InitTextFrameLine(CUIXml& xml_doc, pcstr path, int index, CUITextFrameLineWnd* pWnd, bool fatal /*= true*/) @@ -931,7 +917,7 @@ bool CUIXmlInitBase::InitTexture(const CUIXml& xml_doc, pcstr path, int index, I const u32 color = GetColor(xml_doc, buf, index, 0xff); pWnd->SetTextureColor(color); - if (rect.width() != 0 && rect.height() != 0) + if (!fis_zero(rect.width()) && !fis_zero(rect.height())) pWnd->SetTextureRect(rect); return result; diff --git a/src/xrUICore/ui_export_script.cpp b/src/xrUICore/ui_export_script.cpp index df6af70db74..90902922988 100644 --- a/src/xrUICore/ui_export_script.cpp +++ b/src/xrUICore/ui_export_script.cpp @@ -391,9 +391,10 @@ void CUIFrameLineWnd::script_register(lua_State* luaState) .def("SetColor", &CUIFrameLineWnd::SetTextureColor) .def("Init", +[](CUIFrameLineWnd* self, cpcstr texture, float x, float y, float width, float height, bool horizontal) { - const Fvector2 pos { x, y }; - const Fvector2 size { width, height }; - self->InitFrameLineWnd(texture, pos, size, horizontal); + self->SetWndPos({ x, y }); + self->SetWndSize({ width, height }); + self->SetHorizontal(horizontal); + self->InitTexture(texture); }), class_("CUIFrameLineWnd") From e4f709a04a7267e9f49cd0006723008b2b2bc23e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sun, 21 Dec 2025 02:21:50 +0300 Subject: [PATCH 065/261] A bit of non functional refactoring of CUIScrollBar and CUIFrameLineWnd --- src/xrUICore/ScrollBar/UIScrollBar.cpp | 46 +++++++----------- src/xrUICore/Windows/UIFrameLineWnd.cpp | 63 +++++++++++++------------ 2 files changed, 50 insertions(+), 59 deletions(-) diff --git a/src/xrUICore/ScrollBar/UIScrollBar.cpp b/src/xrUICore/ScrollBar/UIScrollBar.cpp index 138e56f3332..e6b22ad0549 100644 --- a/src/xrUICore/ScrollBar/UIScrollBar.cpp +++ b/src/xrUICore/ScrollBar/UIScrollBar.cpp @@ -65,8 +65,6 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, CUIXmlInitBase::Init3tButton(xml_doc, _path, 0, m_IncButton); m_IncButton->SetWndPos(Fvector2().set(length - m_IncButton->GetWidth(), 0.0f)); - m_ScrollBox->SetHorizontal(true); - strconcat(_path, profile, ":box"); if (!CUIXmlInitBase::InitFrameLine(xml_doc, _path, 0, m_ScrollBox, false)) { @@ -82,7 +80,7 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, if (!m_FrameBackground->InitTexture(texture, false)) { tempBackground = xr_new("temporary background"); - tempBackground->SetWndRect(GetWndRect()); + tempBackground->SetWidth(GetWidth()); strconcat(_path, profile, ":back"); if (CUIXmlInitBase::InitStatic(xml_doc, _path, 0, tempBackground, false)) tempBackground->Show(true); @@ -102,8 +100,6 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, CUIXmlInitBase::Init3tButton(xml_doc, _path, 0, m_IncButton); m_IncButton->SetWndPos(Fvector2().set(0.0f, length - m_IncButton->GetHeight())); - m_ScrollBox->SetHorizontal(false); - strconcat(_path, profile, ":box_v"); if (!CUIXmlInitBase::InitFrameLine(xml_doc, _path, 0, m_ScrollBox, false)) { @@ -119,7 +115,7 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, if (!m_FrameBackground->InitTexture(texture, false)) { tempBackground = xr_new("temporary background"); - tempBackground->SetWndRect(GetWndRect()); + tempBackground->SetHeight(GetHeight()); strconcat(_path, profile, ":back_v"); if (CUIXmlInitBase::InitStatic(xml_doc, _path, 0, tempBackground, false)) tempBackground->Show(true); @@ -128,7 +124,7 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, m_ScrollWorkArea = _max(0, iFloor(GetHeight() - 2 * height)); } - UpdateScrollBar(); + m_ScrollBox->SetHorizontal(m_bIsHorizontal); if (tempBackground && tempBackground->IsShown()) { @@ -137,10 +133,6 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, else SetWidth(tempBackground->GetWidth()); - m_FrameBackground->SetWndPos(GetWndPos()); - m_FrameBackground->SetWndSize(GetWndSize()); - m_FrameBackground->SetHorizontal(m_bIsHorizontal); - m_FrameBackground->SetShader(tempBackground->GetShader()); m_FrameBackground->SetTextureRect(tempBackground->GetTextureRect(), CUIFrameLineWnd::flBack); m_FrameBackground->SetTextureRect({ 0, 0, 0, 0 }, CUIFrameLineWnd::flFirst); @@ -150,10 +142,6 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, if (tempScroll && tempScroll->IsShown()) { - m_ScrollBox->SetWndPos(tempScroll->GetWndPos()); - m_ScrollBox->SetWndSize(GetWndSize()); - m_ScrollBox->SetHorizontal(m_bIsHorizontal); - m_ScrollBox->SetShader(tempScroll->GetShader()); m_ScrollBox->SetTextureRect(tempScroll->GetTextureRect(), CUIFrameLineWnd::flBack); m_ScrollBox->SetTextureRect({ 0, 0, 0, 0 }, CUIFrameLineWnd::flFirst); @@ -161,6 +149,8 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, m_ScrollBox->SetTextureVisible(true); } + UpdateScrollBar(); + bool result = true; if (tempScroll && !tempScroll->IsShown()) @@ -253,6 +243,12 @@ void CUIScrollBar::UpdateScrollBar() const int pos = PosViewFromScroll(iFloor(m_ScrollBox->GetWidth()), iFloor(GetHeight())); m_ScrollBox->SetWndPos(Fvector2().set(float(pos), m_ScrollBox->GetWndRect().top)); m_IncButton->SetWndPos(Fvector2().set(GetWidth() - m_IncButton->GetWidth(), 0.0f)); + + // set background + const float size = GetWidth() - m_DecButton->GetWidth() - m_IncButton->GetWidth(); + + m_FrameBackground->SetWndSize({ size, GetHeight() }); + m_FrameBackground->SetWndPos({ m_DecButton->GetWidth(), 0.0f }); } else { @@ -266,6 +262,12 @@ void CUIScrollBar::UpdateScrollBar() const int pos = PosViewFromScroll(iFloor(m_ScrollBox->GetHeight()), iFloor(GetWidth())); m_ScrollBox->SetWndPos(Fvector2().set(m_ScrollBox->GetWndRect().left, float(pos))); m_IncButton->SetWndPos(Fvector2().set(0.0f, GetHeight() - m_IncButton->GetHeight())); + + // set background + const float size = GetHeight() - m_IncButton->GetHeight() - m_DecButton->GetHeight(); + + m_FrameBackground->SetWndSize({ GetWidth(), size }); + m_FrameBackground->SetWndPos({ 0.0f, m_DecButton->GetHeight() }); } } } @@ -531,20 +533,6 @@ bool CUIScrollBar::IsRelevant() const void CUIScrollBar::Draw() { - if (m_bIsHorizontal) - { - const float size = GetWidth() - m_DecButton->GetWidth() - m_IncButton->GetWidth(); - - m_FrameBackground->SetWndSize(Fvector2().set(size, GetHeight())); - m_FrameBackground->SetWndPos(Fvector2().set(m_DecButton->GetWidth(), 0.0f)); - } - else - { - const float size = GetHeight() - m_IncButton->GetHeight() - m_DecButton->GetHeight(); - - m_FrameBackground->SetWndSize(Fvector2().set(GetWidth(), size)); - m_FrameBackground->SetWndPos(Fvector2().set(0.0f, m_DecButton->GetHeight())); - } inherited::Draw(); } diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index 67067984342..d231fcf0bb9 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -12,46 +12,46 @@ bool CUIFrameLineWnd::InitTexture(pcstr texture, bool fatal /*= true*/) bool CUIFrameLineWnd::InitTextureEx(pcstr texture, pcstr shader, bool fatal /*= true*/) { - string256 buf; + m_bTextureVisible = false; - const bool back_exist = CUITextureMaster::InitTexture(strconcat(buf, texture, "_back"), shader, m_shader[flBack], m_tex_rect[flBack]); - const bool b_exist = CUITextureMaster::InitTexture(strconcat(buf, texture, "_b"), shader, m_shader[flFirst], m_tex_rect[flFirst]); - const bool e_exist = CUITextureMaster::InitTexture(strconcat(buf, texture, "_e"), shader, m_shader[flSecond], m_tex_rect[flSecond]); + string_path first, second, back; + strconcat(back, texture, "_back"); + strconcat(first, texture, "_b"); + strconcat(second, texture, "_e"); - bool failed = false; + const bool back_exist = CUITextureMaster::InitTexture(back, shader, m_shader[flBack], m_tex_rect[flBack]); + const bool b_exist = CUITextureMaster::InitTexture(first, shader, m_shader[flFirst], m_tex_rect[flFirst]); + const bool e_exist = CUITextureMaster::InitTexture(second, shader, m_shader[flSecond], m_tex_rect[flSecond]); - if (fatal) + if (back_exist && b_exist && e_exist) { - R_ASSERT2(back_exist, texture); - R_ASSERT2(b_exist, texture); - R_ASSERT2(e_exist, texture); - } - /*else*/ // Always set failed flag to be able to play in debug - { - failed |= !back_exist; - failed |= !b_exist; - failed |= !e_exist; - } + m_bTextureVisible = true; #ifndef MASTER_GOLD - if (bHorizontal) - { - if (b_exist && e_exist && !fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flSecond].height())) - Msg("~ Textures %s_b and %s_e are not similar by height", texture, texture); - if (b_exist && back_exist && !fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flBack].height())) - Msg("~ Textures %s_b and %s_back are not similar by height", texture, texture); + if (bHorizontal) + { + if (b_exist && e_exist && !fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flSecond].height())) + Msg("~ Textures %s_b and %s_e are not similar by height", texture, texture); + if (b_exist && back_exist && !fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flBack].height())) + Msg("~ Textures %s_b and %s_back are not similar by height", texture, texture); + } + else + { + if (b_exist && e_exist && !fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flSecond].width())) + Msg("~ Textures %s_b and %s_e are not similar by width", texture, texture); + if (b_exist && back_exist && !fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flBack].width())) + Msg("~ Textures %s_b and %s_back are not similar by width", texture, texture); + } +#endif } - else + else if (fatal) { - if (b_exist && e_exist && !fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flSecond].width())) - Msg("~ Textures %s_b and %s_e are not similar by width", texture, texture); - if (b_exist && back_exist && !fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flBack].width())) - Msg("~ Textures %s_b and %s_back are not similar by width", texture, texture); + R_ASSERT3(back_exist, "Texture needed for CUIFrameLineWnd is missing", back); + R_ASSERT3(b_exist, "Texture needed for CUIFrameLineWnd is missing", first); + R_ASSERT3(e_exist, "Texture needed for CUIFrameLineWnd is missing", second); } -#endif - m_bTextureVisible = !failed; - return !failed; + return m_bTextureVisible; } void CUIFrameLineWnd::Draw() @@ -134,6 +134,9 @@ void CUIFrameLineWnd::DrawElements() const const auto draw_tile = [&](const float length, const Frect tex_rect, const RectSegment segment) { + if (fis_zero(length)) + return; + Fvector2 lt, rb; if (bHorizontal) { From d9c55f6aa62d970f98a08d8dea4eb6a9240d7070 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sun, 21 Dec 2025 02:21:50 +0300 Subject: [PATCH 066/261] A bit of non functional refactoring of CUIScrollBar and CUIFrameLineWnd --- src/xrUICore/ScrollBar/UIScrollBar.cpp | 46 +++++++----------- src/xrUICore/Windows/UIFrameLineWnd.cpp | 63 +++++++++++++------------ 2 files changed, 50 insertions(+), 59 deletions(-) diff --git a/src/xrUICore/ScrollBar/UIScrollBar.cpp b/src/xrUICore/ScrollBar/UIScrollBar.cpp index 138e56f3332..e6b22ad0549 100644 --- a/src/xrUICore/ScrollBar/UIScrollBar.cpp +++ b/src/xrUICore/ScrollBar/UIScrollBar.cpp @@ -65,8 +65,6 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, CUIXmlInitBase::Init3tButton(xml_doc, _path, 0, m_IncButton); m_IncButton->SetWndPos(Fvector2().set(length - m_IncButton->GetWidth(), 0.0f)); - m_ScrollBox->SetHorizontal(true); - strconcat(_path, profile, ":box"); if (!CUIXmlInitBase::InitFrameLine(xml_doc, _path, 0, m_ScrollBox, false)) { @@ -82,7 +80,7 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, if (!m_FrameBackground->InitTexture(texture, false)) { tempBackground = xr_new("temporary background"); - tempBackground->SetWndRect(GetWndRect()); + tempBackground->SetWidth(GetWidth()); strconcat(_path, profile, ":back"); if (CUIXmlInitBase::InitStatic(xml_doc, _path, 0, tempBackground, false)) tempBackground->Show(true); @@ -102,8 +100,6 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, CUIXmlInitBase::Init3tButton(xml_doc, _path, 0, m_IncButton); m_IncButton->SetWndPos(Fvector2().set(0.0f, length - m_IncButton->GetHeight())); - m_ScrollBox->SetHorizontal(false); - strconcat(_path, profile, ":box_v"); if (!CUIXmlInitBase::InitFrameLine(xml_doc, _path, 0, m_ScrollBox, false)) { @@ -119,7 +115,7 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, if (!m_FrameBackground->InitTexture(texture, false)) { tempBackground = xr_new("temporary background"); - tempBackground->SetWndRect(GetWndRect()); + tempBackground->SetHeight(GetHeight()); strconcat(_path, profile, ":back_v"); if (CUIXmlInitBase::InitStatic(xml_doc, _path, 0, tempBackground, false)) tempBackground->Show(true); @@ -128,7 +124,7 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, m_ScrollWorkArea = _max(0, iFloor(GetHeight() - 2 * height)); } - UpdateScrollBar(); + m_ScrollBox->SetHorizontal(m_bIsHorizontal); if (tempBackground && tempBackground->IsShown()) { @@ -137,10 +133,6 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, else SetWidth(tempBackground->GetWidth()); - m_FrameBackground->SetWndPos(GetWndPos()); - m_FrameBackground->SetWndSize(GetWndSize()); - m_FrameBackground->SetHorizontal(m_bIsHorizontal); - m_FrameBackground->SetShader(tempBackground->GetShader()); m_FrameBackground->SetTextureRect(tempBackground->GetTextureRect(), CUIFrameLineWnd::flBack); m_FrameBackground->SetTextureRect({ 0, 0, 0, 0 }, CUIFrameLineWnd::flFirst); @@ -150,10 +142,6 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, if (tempScroll && tempScroll->IsShown()) { - m_ScrollBox->SetWndPos(tempScroll->GetWndPos()); - m_ScrollBox->SetWndSize(GetWndSize()); - m_ScrollBox->SetHorizontal(m_bIsHorizontal); - m_ScrollBox->SetShader(tempScroll->GetShader()); m_ScrollBox->SetTextureRect(tempScroll->GetTextureRect(), CUIFrameLineWnd::flBack); m_ScrollBox->SetTextureRect({ 0, 0, 0, 0 }, CUIFrameLineWnd::flFirst); @@ -161,6 +149,8 @@ bool CUIScrollBar::InitScrollBar(Fvector2 pos, float length, bool bIsHorizontal, m_ScrollBox->SetTextureVisible(true); } + UpdateScrollBar(); + bool result = true; if (tempScroll && !tempScroll->IsShown()) @@ -253,6 +243,12 @@ void CUIScrollBar::UpdateScrollBar() const int pos = PosViewFromScroll(iFloor(m_ScrollBox->GetWidth()), iFloor(GetHeight())); m_ScrollBox->SetWndPos(Fvector2().set(float(pos), m_ScrollBox->GetWndRect().top)); m_IncButton->SetWndPos(Fvector2().set(GetWidth() - m_IncButton->GetWidth(), 0.0f)); + + // set background + const float size = GetWidth() - m_DecButton->GetWidth() - m_IncButton->GetWidth(); + + m_FrameBackground->SetWndSize({ size, GetHeight() }); + m_FrameBackground->SetWndPos({ m_DecButton->GetWidth(), 0.0f }); } else { @@ -266,6 +262,12 @@ void CUIScrollBar::UpdateScrollBar() const int pos = PosViewFromScroll(iFloor(m_ScrollBox->GetHeight()), iFloor(GetWidth())); m_ScrollBox->SetWndPos(Fvector2().set(m_ScrollBox->GetWndRect().left, float(pos))); m_IncButton->SetWndPos(Fvector2().set(0.0f, GetHeight() - m_IncButton->GetHeight())); + + // set background + const float size = GetHeight() - m_IncButton->GetHeight() - m_DecButton->GetHeight(); + + m_FrameBackground->SetWndSize({ GetWidth(), size }); + m_FrameBackground->SetWndPos({ 0.0f, m_DecButton->GetHeight() }); } } } @@ -531,20 +533,6 @@ bool CUIScrollBar::IsRelevant() const void CUIScrollBar::Draw() { - if (m_bIsHorizontal) - { - const float size = GetWidth() - m_DecButton->GetWidth() - m_IncButton->GetWidth(); - - m_FrameBackground->SetWndSize(Fvector2().set(size, GetHeight())); - m_FrameBackground->SetWndPos(Fvector2().set(m_DecButton->GetWidth(), 0.0f)); - } - else - { - const float size = GetHeight() - m_IncButton->GetHeight() - m_DecButton->GetHeight(); - - m_FrameBackground->SetWndSize(Fvector2().set(GetWidth(), size)); - m_FrameBackground->SetWndPos(Fvector2().set(0.0f, m_DecButton->GetHeight())); - } inherited::Draw(); } diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index 67067984342..d231fcf0bb9 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -12,46 +12,46 @@ bool CUIFrameLineWnd::InitTexture(pcstr texture, bool fatal /*= true*/) bool CUIFrameLineWnd::InitTextureEx(pcstr texture, pcstr shader, bool fatal /*= true*/) { - string256 buf; + m_bTextureVisible = false; - const bool back_exist = CUITextureMaster::InitTexture(strconcat(buf, texture, "_back"), shader, m_shader[flBack], m_tex_rect[flBack]); - const bool b_exist = CUITextureMaster::InitTexture(strconcat(buf, texture, "_b"), shader, m_shader[flFirst], m_tex_rect[flFirst]); - const bool e_exist = CUITextureMaster::InitTexture(strconcat(buf, texture, "_e"), shader, m_shader[flSecond], m_tex_rect[flSecond]); + string_path first, second, back; + strconcat(back, texture, "_back"); + strconcat(first, texture, "_b"); + strconcat(second, texture, "_e"); - bool failed = false; + const bool back_exist = CUITextureMaster::InitTexture(back, shader, m_shader[flBack], m_tex_rect[flBack]); + const bool b_exist = CUITextureMaster::InitTexture(first, shader, m_shader[flFirst], m_tex_rect[flFirst]); + const bool e_exist = CUITextureMaster::InitTexture(second, shader, m_shader[flSecond], m_tex_rect[flSecond]); - if (fatal) + if (back_exist && b_exist && e_exist) { - R_ASSERT2(back_exist, texture); - R_ASSERT2(b_exist, texture); - R_ASSERT2(e_exist, texture); - } - /*else*/ // Always set failed flag to be able to play in debug - { - failed |= !back_exist; - failed |= !b_exist; - failed |= !e_exist; - } + m_bTextureVisible = true; #ifndef MASTER_GOLD - if (bHorizontal) - { - if (b_exist && e_exist && !fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flSecond].height())) - Msg("~ Textures %s_b and %s_e are not similar by height", texture, texture); - if (b_exist && back_exist && !fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flBack].height())) - Msg("~ Textures %s_b and %s_back are not similar by height", texture, texture); + if (bHorizontal) + { + if (b_exist && e_exist && !fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flSecond].height())) + Msg("~ Textures %s_b and %s_e are not similar by height", texture, texture); + if (b_exist && back_exist && !fsimilar(m_tex_rect[flFirst].height(), m_tex_rect[flBack].height())) + Msg("~ Textures %s_b and %s_back are not similar by height", texture, texture); + } + else + { + if (b_exist && e_exist && !fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flSecond].width())) + Msg("~ Textures %s_b and %s_e are not similar by width", texture, texture); + if (b_exist && back_exist && !fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flBack].width())) + Msg("~ Textures %s_b and %s_back are not similar by width", texture, texture); + } +#endif } - else + else if (fatal) { - if (b_exist && e_exist && !fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flSecond].width())) - Msg("~ Textures %s_b and %s_e are not similar by width", texture, texture); - if (b_exist && back_exist && !fsimilar(m_tex_rect[flFirst].width(), m_tex_rect[flBack].width())) - Msg("~ Textures %s_b and %s_back are not similar by width", texture, texture); + R_ASSERT3(back_exist, "Texture needed for CUIFrameLineWnd is missing", back); + R_ASSERT3(b_exist, "Texture needed for CUIFrameLineWnd is missing", first); + R_ASSERT3(e_exist, "Texture needed for CUIFrameLineWnd is missing", second); } -#endif - m_bTextureVisible = !failed; - return !failed; + return m_bTextureVisible; } void CUIFrameLineWnd::Draw() @@ -134,6 +134,9 @@ void CUIFrameLineWnd::DrawElements() const const auto draw_tile = [&](const float length, const Frect tex_rect, const RectSegment segment) { + if (fis_zero(length)) + return; + Fvector2 lt, rb; if (bHorizontal) { From 2afdd9599aa58c07f78e065d0e54a4ed9ceb0a28 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sun, 21 Dec 2025 04:34:35 +0300 Subject: [PATCH 067/261] xrGame/ui/ui_af_params: reorder artefact params order to match SOC --- src/xrGame/ui/ui_af_params.cpp | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/xrGame/ui/ui_af_params.cpp b/src/xrGame/ui/ui_af_params.cpp index 24ccbccad70..231db108de8 100644 --- a/src/xrGame/ui/ui_af_params.cpp +++ b/src/xrGame/ui/ui_af_params.cpp @@ -76,14 +76,14 @@ bool CUIArtefactParams::InitFromXml(CUIXml& xml) m_disp_condition = CreateItem(xml, "condition", "ui_inv_af_condition"); //-Alundaio - for (auto [id, section, caption, magnitude, sign_inverse, unit] : af_immunity) - { - m_immunity_item[id] = CreateItem(xml, section, magnitude, sign_inverse, unit, caption); - } for (auto [id, section, caption, magnitude, sign_inverse, unit] : af_restore) { m_restore_item[id] = CreateItem(xml, section, magnitude, sign_inverse, unit, caption); } + for (auto [id, section, caption, magnitude, sign_inverse, unit] : af_immunity) + { + m_immunity_item[id] = CreateItem(xml, section, magnitude, sign_inverse, unit, caption); + } m_additional_weight = CreateItem(xml, "additional_weight", "ui_inv_weight", "ui_inv_outfit_additional_weight"); xml.SetLocalRoot(stored_root); @@ -164,6 +164,19 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) setValue(m_disp_condition, pInvItem.GetCondition()); //-Alundaio + for (auto [id, restore_section, restore_caption, magnitude, sign_inverse, unit] : af_restore) + { + if (!m_restore_item[id]) + continue; + + float val = pSettings->r_float(af_section, restore_section); + if (fis_zero(val)) + continue; + + val *= pInvItem.GetCondition(); + setValue(m_restore_item[id], val); + } + for (auto [id, immunity_section, immunity_caption, magnitude, sign_inverse, unit] : af_immunity) { if (!m_immunity_item[id]) @@ -190,19 +203,6 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) } } - for (auto [id, restore_section, restore_caption, magnitude, sign_inverse, unit] : af_restore) - { - if (!m_restore_item[id]) - continue; - - float val = pSettings->r_float(af_section, restore_section); - if (fis_zero(val)) - continue; - - val *= pInvItem.GetCondition(); - setValue(m_restore_item[id], val); - } - SetHeight(h); } From ea6aaed83549ea9b6e43b9a1e068b07ffdb985fb Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sun, 21 Dec 2025 04:34:35 +0300 Subject: [PATCH 068/261] xrGame/ui/ui_af_params: reorder artefact params order to match SOC --- src/xrGame/ui/ui_af_params.cpp | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/xrGame/ui/ui_af_params.cpp b/src/xrGame/ui/ui_af_params.cpp index 24ccbccad70..231db108de8 100644 --- a/src/xrGame/ui/ui_af_params.cpp +++ b/src/xrGame/ui/ui_af_params.cpp @@ -76,14 +76,14 @@ bool CUIArtefactParams::InitFromXml(CUIXml& xml) m_disp_condition = CreateItem(xml, "condition", "ui_inv_af_condition"); //-Alundaio - for (auto [id, section, caption, magnitude, sign_inverse, unit] : af_immunity) - { - m_immunity_item[id] = CreateItem(xml, section, magnitude, sign_inverse, unit, caption); - } for (auto [id, section, caption, magnitude, sign_inverse, unit] : af_restore) { m_restore_item[id] = CreateItem(xml, section, magnitude, sign_inverse, unit, caption); } + for (auto [id, section, caption, magnitude, sign_inverse, unit] : af_immunity) + { + m_immunity_item[id] = CreateItem(xml, section, magnitude, sign_inverse, unit, caption); + } m_additional_weight = CreateItem(xml, "additional_weight", "ui_inv_weight", "ui_inv_outfit_additional_weight"); xml.SetLocalRoot(stored_root); @@ -164,6 +164,19 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) setValue(m_disp_condition, pInvItem.GetCondition()); //-Alundaio + for (auto [id, restore_section, restore_caption, magnitude, sign_inverse, unit] : af_restore) + { + if (!m_restore_item[id]) + continue; + + float val = pSettings->r_float(af_section, restore_section); + if (fis_zero(val)) + continue; + + val *= pInvItem.GetCondition(); + setValue(m_restore_item[id], val); + } + for (auto [id, immunity_section, immunity_caption, magnitude, sign_inverse, unit] : af_immunity) { if (!m_immunity_item[id]) @@ -190,19 +203,6 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) } } - for (auto [id, restore_section, restore_caption, magnitude, sign_inverse, unit] : af_restore) - { - if (!m_restore_item[id]) - continue; - - float val = pSettings->r_float(af_section, restore_section); - if (fis_zero(val)) - continue; - - val *= pInvItem.GetCondition(); - setValue(m_restore_item[id], val); - } - SetHeight(h); } From e81302b15d1d66193ab4b18add89020dfb55ce80 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 22 Dec 2025 15:57:19 +0300 Subject: [PATCH 069/261] Add support for SOC title in CUIFrameWindow and CUIFrameLineWnd (#392) --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 15 +++++++++++++++ src/xrUICore/Windows/UIFrameLineWnd.h | 7 +++++++ src/xrUICore/Windows/UIFrameWindow.cpp | 16 +++++++++++++++- src/xrUICore/Windows/UIFrameWindow.h | 7 +++++++ src/xrUICore/XML/UIXmlInitBase.cpp | 12 ++++++++++++ src/xrUICore/ui_export_script.cpp | 8 ++++++++ 6 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index d231fcf0bb9..a76db3250f2 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -1,6 +1,10 @@ #include "pch.hpp" + #include "UIFrameLineWnd.h" + +#include "Static/UIStatic.h" #include "XML/UITextureMaster.h" + #include "xrEngine/editor_helper.h" CUIFrameLineWnd::CUIFrameLineWnd(pcstr window_name) : CUIWindow(window_name) {} @@ -212,6 +216,17 @@ void CUIFrameLineWnd::DrawElements() const GEnv.UIRender->FlushPrimitive(); } +CUIStatic* CUIFrameLineWnd::GetTitleText(bool create_on_demand /*= false*/) +{ + if (create_on_demand && !m_title_text) + { + m_title_text = xr_new("title"); + m_title_text->SetAutoDelete(true); + AttachChild(m_title_text); + } + return m_title_text; +} + bool CUIFrameLineWnd::FillDebugTree(const CUIDebugState& debugState) { return CUIWindow::FillDebugTree(debugState); diff --git a/src/xrUICore/Windows/UIFrameLineWnd.h b/src/xrUICore/Windows/UIFrameLineWnd.h index 561aff58a3a..7b29c45c01b 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.h +++ b/src/xrUICore/Windows/UIFrameLineWnd.h @@ -1,6 +1,9 @@ #pragma once + #include "UIWindow.h" +class CUIStatic; + class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner { typedef CUIWindow inherited; @@ -55,6 +58,8 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner bool IsHorizontal() const { return bHorizontal; } void SetHorizontal(bool horiz) { bHorizontal = horiz; } + CUIStatic* GetTitleText(bool create_on_demand = false); + pcstr GetDebugType() override { return "CUIFrameLineWnd"; } bool FillDebugTree(const CUIDebugState& debugState) override; void FillDebugInfo() override; @@ -70,6 +75,8 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner Frect m_tex_rect[flMax]{}; ui_shader m_shader[flMax]{}; + CUIStatic* m_title_text{}; + private: DECLARE_SCRIPT_REGISTER_FUNCTION(CUIWindow); }; diff --git a/src/xrUICore/Windows/UIFrameWindow.cpp b/src/xrUICore/Windows/UIFrameWindow.cpp index b029b9371c1..51f9d9c29e7 100644 --- a/src/xrUICore/Windows/UIFrameWindow.cpp +++ b/src/xrUICore/Windows/UIFrameWindow.cpp @@ -1,7 +1,9 @@ #include "pch.hpp" + #include "UIFrameWindow.h" -#include "XML/UITextureMaster.h" + #include "Static/UIStatic.h" +#include "XML/UITextureMaster.h" void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 clr, Fvector2 const& ts); @@ -165,6 +167,18 @@ void CUIFrameWindow::Draw() inherited::Draw(); } +CUIStatic* CUIFrameWindow::GetTitleText(bool create_on_demand /*= false*/) +{ + if (create_on_demand && !m_title_text) + { + m_title_text = xr_new("title"); + m_title_text->SetAutoDelete(true); + AttachChild(m_title_text); + } + return m_title_text; +} + + void CUIFrameWindow::DrawElements() { GEnv.UIRender->SetShader(*m_shader); diff --git a/src/xrUICore/Windows/UIFrameWindow.h b/src/xrUICore/Windows/UIFrameWindow.h index 1d632f83fc0..2a129973b9c 100644 --- a/src/xrUICore/Windows/UIFrameWindow.h +++ b/src/xrUICore/Windows/UIFrameWindow.h @@ -1,6 +1,9 @@ #pragma once + #include "UIWindow.h" +class CUIStatic; + class XRUICORE_API CUIFrameWindow : public CUIWindow, public ITextureOwner { typedef CUIWindow inherited; @@ -21,6 +24,8 @@ class XRUICORE_API CUIFrameWindow : public CUIWindow, public ITextureOwner virtual bool GetStretchTexture() { return false; }; virtual void Draw(); + CUIStatic* GetTitleText(bool create_on_demand = false); + pcstr GetDebugType() override { return "CUIFrameWindow"; } protected: @@ -44,6 +49,8 @@ class XRUICORE_API CUIFrameWindow : public CUIWindow, public ITextureOwner u32 m_texture_color; bool m_bTextureVisible; + CUIStatic* m_title_text{}; + void DrawElements(); bool get_points(Frect const& r, int i, Fvector2& LTp, Fvector2& RBp, Fvector2& LTt, Fvector2& RBt); void draw_tile_line(Frect rect, int i, bool b_horz, Fvector2 const& ts); diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 644ac8822f2..78de52fb2da 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -94,6 +94,12 @@ bool CUIXmlInitBase::InitFrameWindow(CUIXml& xml_doc, pcstr path, int index, CUI bool result = InitWindow(xml_doc, path, index, pWnd, fatal); result &= InitTexture(xml_doc, path, index, pWnd, fatal); + // Initialize window title (Shadow of Chernobyl) + string256 buf; + strconcat(buf, path, ":title"); + if (xml_doc.NavigateToNode(buf, index)) + InitStatic(xml_doc, buf, index, pWnd->GetTitleText(true), false); + return result; } @@ -795,6 +801,12 @@ bool CUIXmlInitBase::InitFrameLine(CUIXml& xml_doc, pcstr path, int index, CUIFr const int stretch_flag = xml_doc.ReadAttribInt(path, index, "stretch", stretch_default); pWnd->SetStretchTexture(stretch_flag); + // Initialize window title (Shadow of Chernobyl) + string256 buf; + strconcat(buf, path, ":title"); + if (xml_doc.NavigateToNode(buf, index)) + InitStatic(xml_doc, buf, index, pWnd->GetTitleText(true), false); + return result; } diff --git a/src/xrUICore/ui_export_script.cpp b/src/xrUICore/ui_export_script.cpp index 90902922988..919d0d7498e 100644 --- a/src/xrUICore/ui_export_script.cpp +++ b/src/xrUICore/ui_export_script.cpp @@ -354,6 +354,10 @@ void CUIFrameWindow::script_register(lua_State* luaState) .def("SetWidth", &CUIFrameWindow::SetWidth) .def("SetHeight", &CUIFrameWindow::SetHeight) .def("SetColor", &CUIFrameWindow::SetTextureColor) + .def("GetTitleStatic", +[](CUIFrameWindow* self) -> CUIStatic* + { + return self->GetTitleText(true); + }) .def("Init", +[](CUIFrameWindow* self, pcstr texture, float x, float y, float width, float height) { self->SetWndRect({ x, y, width, height }); @@ -389,6 +393,10 @@ void CUIFrameLineWnd::script_register(lua_State* luaState) .def("SetWidth", &CUIFrameLineWnd::SetWidth) .def("SetHeight", &CUIFrameLineWnd::SetHeight) .def("SetColor", &CUIFrameLineWnd::SetTextureColor) + .def("GetTitleStatic", +[](CUIFrameWindow* self) -> CUIStatic* + { + return self->GetTitleText(true); + }) .def("Init", +[](CUIFrameLineWnd* self, cpcstr texture, float x, float y, float width, float height, bool horizontal) { self->SetWndPos({ x, y }); From c4ed98f346c3261c2274c2de9146876e94a97e33 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 22 Dec 2025 15:57:19 +0300 Subject: [PATCH 070/261] Add support for SOC title in CUIFrameWindow and CUIFrameLineWnd (#392) --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 15 +++++++++++++++ src/xrUICore/Windows/UIFrameLineWnd.h | 7 +++++++ src/xrUICore/Windows/UIFrameWindow.cpp | 16 +++++++++++++++- src/xrUICore/Windows/UIFrameWindow.h | 7 +++++++ src/xrUICore/XML/UIXmlInitBase.cpp | 12 ++++++++++++ src/xrUICore/ui_export_script.cpp | 8 ++++++++ 6 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index d231fcf0bb9..a76db3250f2 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -1,6 +1,10 @@ #include "pch.hpp" + #include "UIFrameLineWnd.h" + +#include "Static/UIStatic.h" #include "XML/UITextureMaster.h" + #include "xrEngine/editor_helper.h" CUIFrameLineWnd::CUIFrameLineWnd(pcstr window_name) : CUIWindow(window_name) {} @@ -212,6 +216,17 @@ void CUIFrameLineWnd::DrawElements() const GEnv.UIRender->FlushPrimitive(); } +CUIStatic* CUIFrameLineWnd::GetTitleText(bool create_on_demand /*= false*/) +{ + if (create_on_demand && !m_title_text) + { + m_title_text = xr_new("title"); + m_title_text->SetAutoDelete(true); + AttachChild(m_title_text); + } + return m_title_text; +} + bool CUIFrameLineWnd::FillDebugTree(const CUIDebugState& debugState) { return CUIWindow::FillDebugTree(debugState); diff --git a/src/xrUICore/Windows/UIFrameLineWnd.h b/src/xrUICore/Windows/UIFrameLineWnd.h index 561aff58a3a..7b29c45c01b 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.h +++ b/src/xrUICore/Windows/UIFrameLineWnd.h @@ -1,6 +1,9 @@ #pragma once + #include "UIWindow.h" +class CUIStatic; + class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner { typedef CUIWindow inherited; @@ -55,6 +58,8 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner bool IsHorizontal() const { return bHorizontal; } void SetHorizontal(bool horiz) { bHorizontal = horiz; } + CUIStatic* GetTitleText(bool create_on_demand = false); + pcstr GetDebugType() override { return "CUIFrameLineWnd"; } bool FillDebugTree(const CUIDebugState& debugState) override; void FillDebugInfo() override; @@ -70,6 +75,8 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner Frect m_tex_rect[flMax]{}; ui_shader m_shader[flMax]{}; + CUIStatic* m_title_text{}; + private: DECLARE_SCRIPT_REGISTER_FUNCTION(CUIWindow); }; diff --git a/src/xrUICore/Windows/UIFrameWindow.cpp b/src/xrUICore/Windows/UIFrameWindow.cpp index b029b9371c1..51f9d9c29e7 100644 --- a/src/xrUICore/Windows/UIFrameWindow.cpp +++ b/src/xrUICore/Windows/UIFrameWindow.cpp @@ -1,7 +1,9 @@ #include "pch.hpp" + #include "UIFrameWindow.h" -#include "XML/UITextureMaster.h" + #include "Static/UIStatic.h" +#include "XML/UITextureMaster.h" void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 clr, Fvector2 const& ts); @@ -165,6 +167,18 @@ void CUIFrameWindow::Draw() inherited::Draw(); } +CUIStatic* CUIFrameWindow::GetTitleText(bool create_on_demand /*= false*/) +{ + if (create_on_demand && !m_title_text) + { + m_title_text = xr_new("title"); + m_title_text->SetAutoDelete(true); + AttachChild(m_title_text); + } + return m_title_text; +} + + void CUIFrameWindow::DrawElements() { GEnv.UIRender->SetShader(*m_shader); diff --git a/src/xrUICore/Windows/UIFrameWindow.h b/src/xrUICore/Windows/UIFrameWindow.h index 1d632f83fc0..2a129973b9c 100644 --- a/src/xrUICore/Windows/UIFrameWindow.h +++ b/src/xrUICore/Windows/UIFrameWindow.h @@ -1,6 +1,9 @@ #pragma once + #include "UIWindow.h" +class CUIStatic; + class XRUICORE_API CUIFrameWindow : public CUIWindow, public ITextureOwner { typedef CUIWindow inherited; @@ -21,6 +24,8 @@ class XRUICORE_API CUIFrameWindow : public CUIWindow, public ITextureOwner virtual bool GetStretchTexture() { return false; }; virtual void Draw(); + CUIStatic* GetTitleText(bool create_on_demand = false); + pcstr GetDebugType() override { return "CUIFrameWindow"; } protected: @@ -44,6 +49,8 @@ class XRUICORE_API CUIFrameWindow : public CUIWindow, public ITextureOwner u32 m_texture_color; bool m_bTextureVisible; + CUIStatic* m_title_text{}; + void DrawElements(); bool get_points(Frect const& r, int i, Fvector2& LTp, Fvector2& RBp, Fvector2& LTt, Fvector2& RBt); void draw_tile_line(Frect rect, int i, bool b_horz, Fvector2 const& ts); diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 644ac8822f2..78de52fb2da 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -94,6 +94,12 @@ bool CUIXmlInitBase::InitFrameWindow(CUIXml& xml_doc, pcstr path, int index, CUI bool result = InitWindow(xml_doc, path, index, pWnd, fatal); result &= InitTexture(xml_doc, path, index, pWnd, fatal); + // Initialize window title (Shadow of Chernobyl) + string256 buf; + strconcat(buf, path, ":title"); + if (xml_doc.NavigateToNode(buf, index)) + InitStatic(xml_doc, buf, index, pWnd->GetTitleText(true), false); + return result; } @@ -795,6 +801,12 @@ bool CUIXmlInitBase::InitFrameLine(CUIXml& xml_doc, pcstr path, int index, CUIFr const int stretch_flag = xml_doc.ReadAttribInt(path, index, "stretch", stretch_default); pWnd->SetStretchTexture(stretch_flag); + // Initialize window title (Shadow of Chernobyl) + string256 buf; + strconcat(buf, path, ":title"); + if (xml_doc.NavigateToNode(buf, index)) + InitStatic(xml_doc, buf, index, pWnd->GetTitleText(true), false); + return result; } diff --git a/src/xrUICore/ui_export_script.cpp b/src/xrUICore/ui_export_script.cpp index 90902922988..919d0d7498e 100644 --- a/src/xrUICore/ui_export_script.cpp +++ b/src/xrUICore/ui_export_script.cpp @@ -354,6 +354,10 @@ void CUIFrameWindow::script_register(lua_State* luaState) .def("SetWidth", &CUIFrameWindow::SetWidth) .def("SetHeight", &CUIFrameWindow::SetHeight) .def("SetColor", &CUIFrameWindow::SetTextureColor) + .def("GetTitleStatic", +[](CUIFrameWindow* self) -> CUIStatic* + { + return self->GetTitleText(true); + }) .def("Init", +[](CUIFrameWindow* self, pcstr texture, float x, float y, float width, float height) { self->SetWndRect({ x, y, width, height }); @@ -389,6 +393,10 @@ void CUIFrameLineWnd::script_register(lua_State* luaState) .def("SetWidth", &CUIFrameLineWnd::SetWidth) .def("SetHeight", &CUIFrameLineWnd::SetHeight) .def("SetColor", &CUIFrameLineWnd::SetTextureColor) + .def("GetTitleStatic", +[](CUIFrameWindow* self) -> CUIStatic* + { + return self->GetTitleText(true); + }) .def("Init", +[](CUIFrameLineWnd* self, cpcstr texture, float x, float y, float width, float height, bool horizontal) { self->SetWndPos({ x, y }); From a43f7e4dfd915f397fbc741cde7e272e7e5ec410 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 22 Dec 2025 16:55:42 +0300 Subject: [PATCH 071/261] Export CUIFrameLineWnd::SetOrientation to Lua (#392) --- src/xrUICore/ui_export_script.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/xrUICore/ui_export_script.cpp b/src/xrUICore/ui_export_script.cpp index 919d0d7498e..3ce301d836a 100644 --- a/src/xrUICore/ui_export_script.cpp +++ b/src/xrUICore/ui_export_script.cpp @@ -393,6 +393,7 @@ void CUIFrameLineWnd::script_register(lua_State* luaState) .def("SetWidth", &CUIFrameLineWnd::SetWidth) .def("SetHeight", &CUIFrameLineWnd::SetHeight) .def("SetColor", &CUIFrameLineWnd::SetTextureColor) + .def("SetOrientation", &CUIFrameLineWnd::SetHorizontal) .def("GetTitleStatic", +[](CUIFrameWindow* self) -> CUIStatic* { return self->GetTitleText(true); From c379512d5fe739df974f79a904b00dcde51a3672 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 22 Dec 2025 16:55:42 +0300 Subject: [PATCH 072/261] Export CUIFrameLineWnd::SetOrientation to Lua (#392) --- src/xrUICore/ui_export_script.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/xrUICore/ui_export_script.cpp b/src/xrUICore/ui_export_script.cpp index 919d0d7498e..3ce301d836a 100644 --- a/src/xrUICore/ui_export_script.cpp +++ b/src/xrUICore/ui_export_script.cpp @@ -393,6 +393,7 @@ void CUIFrameLineWnd::script_register(lua_State* luaState) .def("SetWidth", &CUIFrameLineWnd::SetWidth) .def("SetHeight", &CUIFrameLineWnd::SetHeight) .def("SetColor", &CUIFrameLineWnd::SetTextureColor) + .def("SetOrientation", &CUIFrameLineWnd::SetHorizontal) .def("GetTitleStatic", +[](CUIFrameWindow* self) -> CUIStatic* { return self->GetTitleText(true); From 0301e95fffd08cee91c3a8ed1f39dcb93d95388d Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 22 Dec 2025 17:16:29 +0300 Subject: [PATCH 073/261] xrUICore/ui_export_script.cpp: a bit of CUIWindow script export refactoring --- src/xrUICore/Windows/UIWindow.h | 4 --- src/xrUICore/ui_export_script.cpp | 49 ++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/xrUICore/Windows/UIWindow.h b/src/xrUICore/Windows/UIWindow.h index 3fdb39a7d9d..c325e3e4589 100644 --- a/src/xrUICore/Windows/UIWindow.h +++ b/src/xrUICore/Windows/UIWindow.h @@ -293,10 +293,6 @@ class XRUICORE_API CUIWindow : public CUIDebuggable return { abs.x1 + size.x, abs.y1 + size.y }; } - void SetWndRect_script(Frect rect) { SetWndRect(rect); } - void SetWndPos_script(Fvector2 pos) { SetWndPos(pos); } - void SetWndSize_script(Fvector2 size) { SetWndSize(size); } - // прорисовка окна virtual void Draw(); virtual void Draw(float x, float y); diff --git a/src/xrUICore/ui_export_script.cpp b/src/xrUICore/ui_export_script.cpp index 3ce301d836a..e271f0b4333 100644 --- a/src/xrUICore/ui_export_script.cpp +++ b/src/xrUICore/ui_export_script.cpp @@ -209,33 +209,54 @@ void CUIWindow::script_register(lua_State* luaState) .def("Init", +[](CUIWindow* self, float x, float y, float width, float height) { - const Frect rect { x, y, width, height }; + self->SetWndRect({ x, y, width, height }); + }) + .def("Init", +[](CUIWindow* self, Frect rect) + { self->SetWndRect(rect); }) - .def("Init", (void (CUIWindow::*)(Frect))& CUIWindow::SetWndRect_script) - .def("SetWndRect", (void (CUIWindow::*)(Frect)) & CUIWindow::SetWndRect_script) - .def("SetWndRect", +[](CUIWindow* self, float x, float y, float width, float height) + // Rect + .def("GetWndRect", +[](CUIWindow* self) -> Frect + { + return self->GetWndRect(); + }) + .def("SetWndRect", +[](CUIWindow* self, Frect rect) { - const Frect rect { x, y, width, height }; self->SetWndRect(rect); }) + .def("SetWndRect", +[](CUIWindow* self, float x, float y, float width, float height) + { + self->SetWndRect({ x, y, width, height }); + }) - .def("SetWndSize", (void (CUIWindow::*)(Fvector2)) & CUIWindow::SetWndSize_script) - - .def("GetWndPos", +[](CUIWindow* w) -> Fvector2 { return w->GetWndPos(); }) - .def("SetWndPos", (void (CUIWindow::*)(Fvector2)) & CUIWindow::SetWndPos_script) - + // Position + .def("GetWndPos", +[](CUIWindow* self) -> Fvector2 + { + return self->GetWndPos(); + }) + .def("SetWndPos", +[](CUIWindow* self, Fvector2 pos) + { + self->SetWndPos(pos); + }) .def("SetWndPos", +[](CUIWindow* self, float x, float y) { - const Fvector2 pos { x, y }; - self->SetWndPos(pos); + self->SetWndPos({ x, y }); }) - .def("SetWndSize", +[](CUIWindow* self, float width, float height) + + // Size + .def("GetWndSize", +[](CUIWindow* self) -> Fvector2 + { + return self->GetWndSize(); + }) + .def("SetWndSize", +[](CUIWindow* self, Fvector2 size) { - const Fvector2 size { width, height }; self->SetWndSize(size); }) + .def("SetWndSize", +[](CUIWindow* self, float width, float height) + { + self->SetWndSize({ width, height }); + }) .def("GetWidth", &CUIWindow::GetWidth) .def("SetWidth", &CUIWindow::SetWidth) From d74c37a425705c1fbd3ecf1fbbcb13d71a1f4bea Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 22 Dec 2025 17:16:29 +0300 Subject: [PATCH 074/261] xrUICore/ui_export_script.cpp: a bit of CUIWindow script export refactoring --- src/xrUICore/Windows/UIWindow.h | 4 --- src/xrUICore/ui_export_script.cpp | 49 ++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/xrUICore/Windows/UIWindow.h b/src/xrUICore/Windows/UIWindow.h index 3fdb39a7d9d..c325e3e4589 100644 --- a/src/xrUICore/Windows/UIWindow.h +++ b/src/xrUICore/Windows/UIWindow.h @@ -293,10 +293,6 @@ class XRUICORE_API CUIWindow : public CUIDebuggable return { abs.x1 + size.x, abs.y1 + size.y }; } - void SetWndRect_script(Frect rect) { SetWndRect(rect); } - void SetWndPos_script(Fvector2 pos) { SetWndPos(pos); } - void SetWndSize_script(Fvector2 size) { SetWndSize(size); } - // прорисовка окна virtual void Draw(); virtual void Draw(float x, float y); diff --git a/src/xrUICore/ui_export_script.cpp b/src/xrUICore/ui_export_script.cpp index 3ce301d836a..e271f0b4333 100644 --- a/src/xrUICore/ui_export_script.cpp +++ b/src/xrUICore/ui_export_script.cpp @@ -209,33 +209,54 @@ void CUIWindow::script_register(lua_State* luaState) .def("Init", +[](CUIWindow* self, float x, float y, float width, float height) { - const Frect rect { x, y, width, height }; + self->SetWndRect({ x, y, width, height }); + }) + .def("Init", +[](CUIWindow* self, Frect rect) + { self->SetWndRect(rect); }) - .def("Init", (void (CUIWindow::*)(Frect))& CUIWindow::SetWndRect_script) - .def("SetWndRect", (void (CUIWindow::*)(Frect)) & CUIWindow::SetWndRect_script) - .def("SetWndRect", +[](CUIWindow* self, float x, float y, float width, float height) + // Rect + .def("GetWndRect", +[](CUIWindow* self) -> Frect + { + return self->GetWndRect(); + }) + .def("SetWndRect", +[](CUIWindow* self, Frect rect) { - const Frect rect { x, y, width, height }; self->SetWndRect(rect); }) + .def("SetWndRect", +[](CUIWindow* self, float x, float y, float width, float height) + { + self->SetWndRect({ x, y, width, height }); + }) - .def("SetWndSize", (void (CUIWindow::*)(Fvector2)) & CUIWindow::SetWndSize_script) - - .def("GetWndPos", +[](CUIWindow* w) -> Fvector2 { return w->GetWndPos(); }) - .def("SetWndPos", (void (CUIWindow::*)(Fvector2)) & CUIWindow::SetWndPos_script) - + // Position + .def("GetWndPos", +[](CUIWindow* self) -> Fvector2 + { + return self->GetWndPos(); + }) + .def("SetWndPos", +[](CUIWindow* self, Fvector2 pos) + { + self->SetWndPos(pos); + }) .def("SetWndPos", +[](CUIWindow* self, float x, float y) { - const Fvector2 pos { x, y }; - self->SetWndPos(pos); + self->SetWndPos({ x, y }); }) - .def("SetWndSize", +[](CUIWindow* self, float width, float height) + + // Size + .def("GetWndSize", +[](CUIWindow* self) -> Fvector2 + { + return self->GetWndSize(); + }) + .def("SetWndSize", +[](CUIWindow* self, Fvector2 size) { - const Fvector2 size { width, height }; self->SetWndSize(size); }) + .def("SetWndSize", +[](CUIWindow* self, float width, float height) + { + self->SetWndSize({ width, height }); + }) .def("GetWidth", &CUIWindow::GetWidth) .def("SetWidth", &CUIWindow::SetWidth) From d73a4cdeb177e887022d81baf656c2f1fe5cae1e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 01:39:04 +0300 Subject: [PATCH 075/261] xrGame/CustomOutfit.h|cpp: make m_boneProtection an actual instance, not pointer --- src/xrGame/CustomOutfit.cpp | 59 ++++++++++++++++++------------------- src/xrGame/CustomOutfit.h | 5 ++-- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/xrGame/CustomOutfit.cpp b/src/xrGame/CustomOutfit.cpp index caeb9aa3ca6..985122c4773 100644 --- a/src/xrGame/CustomOutfit.cpp +++ b/src/xrGame/CustomOutfit.cpp @@ -19,11 +19,8 @@ CCustomOutfit::CCustomOutfit() m_HitTypeProtection.resize(ALife::eHitTypeMax); for (int i = 0; i < static_cast(ALife::eHitTypeMax); i++) m_HitTypeProtection[i] = 1.0f; - - m_boneProtection = xr_new(); } -CCustomOutfit::~CCustomOutfit() { xr_delete(m_boneProtection); } bool CCustomOutfit::net_Spawn(CSE_Abstract* DC) { if (IsGameTypeSingle()) @@ -73,16 +70,16 @@ void CCustomOutfit::Load(LPCSTR section) if (pSettings->line_exist(section, "hit_fraction_actor")) { - m_boneProtection->m_fHitFrac = pSettings->r_float(section, "hit_fraction_actor"); + m_boneProtection.m_fHitFrac = pSettings->r_float(section, "hit_fraction_actor"); // Since hit_fraction_actor exists both in CS and COP, but fire_wound_protection was removed in COP, // We can use this hacky solution to determine which damage formula to use. // It not robust for mods, because they can have fire_wound_protection in configs, despite that // original COP engine doesn't read it. if (pSettings->line_exist(section, "fire_wound_protection")) - m_boneProtection->m_hitFracType = SBoneProtections::HitFractionActorCS; + m_boneProtection.m_hitFracType = SBoneProtections::HitFractionActorCS; else - m_boneProtection->m_hitFracType = SBoneProtections::HitFractionActorCOP; + m_boneProtection.m_hitFracType = SBoneProtections::HitFractionActorCOP; } if (pSettings->line_exist(section, "nightvision_sect")) @@ -126,7 +123,7 @@ void CCustomOutfit::ReloadBonesProtection() parent = smart_cast(Level().CurrentViewEntity()); if (parent && parent->Visual() && m_BonesProtectionSect.size()) - m_boneProtection->reload(m_BonesProtectionSect, smart_cast(parent->Visual())); + m_boneProtection.reload(m_BonesProtectionSect, smart_cast(parent->Visual())); } void CCustomOutfit::Hit(float hit_power, ALife::EHitType hit_type) @@ -139,7 +136,7 @@ float CCustomOutfit::GetDefHitTypeProtection(ALife::EHitType hit_type) const { const float base = m_HitTypeProtection[hit_type] * GetCondition(); - if (m_boneProtection->m_hitFracType == SBoneProtections::HitFraction) + if (m_boneProtection.m_hitFracType == SBoneProtections::HitFraction) return 1.0f - base; // SOC return base; // CS/COP @@ -148,9 +145,9 @@ float CCustomOutfit::GetDefHitTypeProtection(ALife::EHitType hit_type) const float CCustomOutfit::GetHitTypeProtection(ALife::EHitType hit_type, s16 element) const { const float base = m_HitTypeProtection[hit_type] * GetCondition(); - const float bone = m_boneProtection->getBoneProtection(element); + const float bone = m_boneProtection.getBoneProtection(element); - if (m_boneProtection->m_hitFracType == SBoneProtections::HitFraction) + if (m_boneProtection.m_hitFracType == SBoneProtections::HitFraction) return 1.0f - base * bone; // SOC return base * bone; // CS/COP @@ -158,14 +155,14 @@ float CCustomOutfit::GetHitTypeProtection(ALife::EHitType hit_type, s16 element) float CCustomOutfit::GetBoneArmor(s16 element) const { - return m_boneProtection->getBoneArmor(element); + return m_boneProtection.getBoneArmor(element); } float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, bool& add_wound, ALife::EHitType hit_type) { float NewHitPower = hit_power; - switch (m_boneProtection->m_hitFracType) + switch (m_boneProtection.m_hitFracType) { default: case SBoneProtections::HitFractionActorCOP: @@ -183,11 +180,11 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo if (!IsGameTypeSingle()) { float hit_fraction = (ap - BoneArmor) / ap; - if (hit_fraction < m_boneProtection->m_fHitFrac) - hit_fraction = m_boneProtection->m_fHitFrac; + if (hit_fraction < m_boneProtection.m_fHitFrac) + hit_fraction = m_boneProtection.m_fHitFrac; NewHitPower *= hit_fraction; - NewHitPower *= m_boneProtection->getBoneProtection(element); + NewHitPower *= m_boneProtection.getBoneProtection(element); } VERIFY(NewHitPower >= 0.0f); @@ -195,7 +192,7 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo else { //пуля НЕ пробила бронь - NewHitPower *= m_boneProtection->m_fHitFrac; + NewHitPower *= m_boneProtection.m_fHitFrac; add_wound = false; //раны нет } } @@ -224,7 +221,7 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo { if (hit_type == ALife::eHitTypeFireWound) { - const float BoneArmor = m_boneProtection->getBoneArmor(element) * GetCondition(); + const float BoneArmor = m_boneProtection.getBoneArmor(element) * GetCondition(); if (ap > EPS && ap > BoneArmor) { @@ -232,12 +229,12 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo const float d_ap = ap - BoneArmor; NewHitPower *= (d_ap / ap); - if (NewHitPower < m_boneProtection->m_fHitFrac) - NewHitPower = m_boneProtection->m_fHitFrac; + if (NewHitPower < m_boneProtection.m_fHitFrac) + NewHitPower = m_boneProtection.m_fHitFrac; if (!IsGameTypeSingle()) { - NewHitPower *= m_boneProtection->getBoneProtection(element); + NewHitPower *= m_boneProtection.getBoneProtection(element); } if (NewHitPower < 0.0f) @@ -246,7 +243,7 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo else { //пуля НЕ пробила бронь - NewHitPower *= m_boneProtection->m_fHitFrac; + NewHitPower *= m_boneProtection.m_fHitFrac; add_wound = false; //раны нет } } @@ -274,10 +271,10 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo { if (hit_type == ALife::eHitTypeFireWound) { - const float BoneArmor = m_boneProtection->getBoneArmor(element) * GetCondition() * (1 - ap); + const float BoneArmor = m_boneProtection.getBoneArmor(element) * GetCondition() * (1 - ap); NewHitPower -= BoneArmor; - if (NewHitPower < hit_power * m_boneProtection->m_fHitFrac) - NewHitPower = hit_power * m_boneProtection->m_fHitFrac; + if (NewHitPower < hit_power * m_boneProtection.m_fHitFrac) + NewHitPower = hit_power * m_boneProtection.m_fHitFrac; } else { @@ -288,12 +285,12 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo Hit(hit_power, hit_type); break; } - } // switch (m_boneProtection->m_hitFracType) + } // switch (m_boneProtection.m_hitFracType) return NewHitPower; } -bool CCustomOutfit::BonePassBullet(int boneID) { return m_boneProtection->getBonePassBullet(s16(boneID)); } +bool CCustomOutfit::BonePassBullet(int boneID) { return m_boneProtection.getBonePassBullet(s16(boneID)); } #include "Torch.h" void CCustomOutfit::OnMoveToSlot(const SInvItemPlace& prev) { @@ -392,7 +389,7 @@ float CCustomOutfit::GetPowerLoss() const // Hit fraction and power loss are unrelated, // but it's the only way we can distinguish between SOC/CS and COP. // Sorry. - if (m_boneProtection->m_hitFracType != SBoneProtections::HitFractionActorCOP) + if (m_boneProtection.m_hitFracType != SBoneProtections::HitFractionActorCOP) { if (m_fPowerLoss < 1 && GetCondition() <= 0) { @@ -445,10 +442,10 @@ bool CCustomOutfit::install_upgrade_impl(LPCSTR section, bool test) AddBonesProtection(str); result |= result2; - if (m_boneProtection->m_hitFracType == SBoneProtections::HitFractionActorCS || - m_boneProtection->m_hitFracType == SBoneProtections::HitFractionActorCOP) + if (m_boneProtection.m_hitFracType == SBoneProtections::HitFractionActorCS || + m_boneProtection.m_hitFracType == SBoneProtections::HitFractionActorCOP) { - result |= process_if_exists(section, "hit_fraction_actor", &CInifile::r_float, m_boneProtection->m_fHitFrac, test); + result |= process_if_exists(section, "hit_fraction_actor", &CInifile::r_float, m_boneProtection.m_fHitFrac, test); } result |= process_if_exists(section, "additional_inventory_weight", &CInifile::r_float, m_additional_weight, test); @@ -477,5 +474,5 @@ void CCustomOutfit::AddBonesProtection(LPCSTR bones_section) parent = smart_cast(Level().CurrentViewEntity()); if (parent && parent->Visual() && m_BonesProtectionSect.size()) - m_boneProtection->add(bones_section, smart_cast(parent->Visual())); + m_boneProtection.add(bones_section, smart_cast(parent->Visual())); } diff --git a/src/xrGame/CustomOutfit.h b/src/xrGame/CustomOutfit.h index 882593be31d..834228d3045 100644 --- a/src/xrGame/CustomOutfit.h +++ b/src/xrGame/CustomOutfit.h @@ -2,7 +2,7 @@ #include "inventory_item_object.h" -struct SBoneProtections; +#include "BoneProtections.h" class CCustomOutfit : public CInventoryItemObject { @@ -10,7 +10,6 @@ class CCustomOutfit : public CInventoryItemObject public: CCustomOutfit(); - virtual ~CCustomOutfit(); virtual void Load(LPCSTR section); @@ -39,7 +38,7 @@ class CCustomOutfit : public CInventoryItemObject shared_str m_ActorVisual; shared_str m_full_icon_name; - SBoneProtections* m_boneProtection; + SBoneProtections m_boneProtection; protected: u32 m_ef_equipment_type; From b845f4fb86b899ffdfe70f82716a79208d16d794 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 01:39:04 +0300 Subject: [PATCH 076/261] xrGame/CustomOutfit.h|cpp: make m_boneProtection an actual instance, not pointer --- src/xrGame/CustomOutfit.cpp | 59 ++++++++++++++++++------------------- src/xrGame/CustomOutfit.h | 5 ++-- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/xrGame/CustomOutfit.cpp b/src/xrGame/CustomOutfit.cpp index caeb9aa3ca6..985122c4773 100644 --- a/src/xrGame/CustomOutfit.cpp +++ b/src/xrGame/CustomOutfit.cpp @@ -19,11 +19,8 @@ CCustomOutfit::CCustomOutfit() m_HitTypeProtection.resize(ALife::eHitTypeMax); for (int i = 0; i < static_cast(ALife::eHitTypeMax); i++) m_HitTypeProtection[i] = 1.0f; - - m_boneProtection = xr_new(); } -CCustomOutfit::~CCustomOutfit() { xr_delete(m_boneProtection); } bool CCustomOutfit::net_Spawn(CSE_Abstract* DC) { if (IsGameTypeSingle()) @@ -73,16 +70,16 @@ void CCustomOutfit::Load(LPCSTR section) if (pSettings->line_exist(section, "hit_fraction_actor")) { - m_boneProtection->m_fHitFrac = pSettings->r_float(section, "hit_fraction_actor"); + m_boneProtection.m_fHitFrac = pSettings->r_float(section, "hit_fraction_actor"); // Since hit_fraction_actor exists both in CS and COP, but fire_wound_protection was removed in COP, // We can use this hacky solution to determine which damage formula to use. // It not robust for mods, because they can have fire_wound_protection in configs, despite that // original COP engine doesn't read it. if (pSettings->line_exist(section, "fire_wound_protection")) - m_boneProtection->m_hitFracType = SBoneProtections::HitFractionActorCS; + m_boneProtection.m_hitFracType = SBoneProtections::HitFractionActorCS; else - m_boneProtection->m_hitFracType = SBoneProtections::HitFractionActorCOP; + m_boneProtection.m_hitFracType = SBoneProtections::HitFractionActorCOP; } if (pSettings->line_exist(section, "nightvision_sect")) @@ -126,7 +123,7 @@ void CCustomOutfit::ReloadBonesProtection() parent = smart_cast(Level().CurrentViewEntity()); if (parent && parent->Visual() && m_BonesProtectionSect.size()) - m_boneProtection->reload(m_BonesProtectionSect, smart_cast(parent->Visual())); + m_boneProtection.reload(m_BonesProtectionSect, smart_cast(parent->Visual())); } void CCustomOutfit::Hit(float hit_power, ALife::EHitType hit_type) @@ -139,7 +136,7 @@ float CCustomOutfit::GetDefHitTypeProtection(ALife::EHitType hit_type) const { const float base = m_HitTypeProtection[hit_type] * GetCondition(); - if (m_boneProtection->m_hitFracType == SBoneProtections::HitFraction) + if (m_boneProtection.m_hitFracType == SBoneProtections::HitFraction) return 1.0f - base; // SOC return base; // CS/COP @@ -148,9 +145,9 @@ float CCustomOutfit::GetDefHitTypeProtection(ALife::EHitType hit_type) const float CCustomOutfit::GetHitTypeProtection(ALife::EHitType hit_type, s16 element) const { const float base = m_HitTypeProtection[hit_type] * GetCondition(); - const float bone = m_boneProtection->getBoneProtection(element); + const float bone = m_boneProtection.getBoneProtection(element); - if (m_boneProtection->m_hitFracType == SBoneProtections::HitFraction) + if (m_boneProtection.m_hitFracType == SBoneProtections::HitFraction) return 1.0f - base * bone; // SOC return base * bone; // CS/COP @@ -158,14 +155,14 @@ float CCustomOutfit::GetHitTypeProtection(ALife::EHitType hit_type, s16 element) float CCustomOutfit::GetBoneArmor(s16 element) const { - return m_boneProtection->getBoneArmor(element); + return m_boneProtection.getBoneArmor(element); } float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, bool& add_wound, ALife::EHitType hit_type) { float NewHitPower = hit_power; - switch (m_boneProtection->m_hitFracType) + switch (m_boneProtection.m_hitFracType) { default: case SBoneProtections::HitFractionActorCOP: @@ -183,11 +180,11 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo if (!IsGameTypeSingle()) { float hit_fraction = (ap - BoneArmor) / ap; - if (hit_fraction < m_boneProtection->m_fHitFrac) - hit_fraction = m_boneProtection->m_fHitFrac; + if (hit_fraction < m_boneProtection.m_fHitFrac) + hit_fraction = m_boneProtection.m_fHitFrac; NewHitPower *= hit_fraction; - NewHitPower *= m_boneProtection->getBoneProtection(element); + NewHitPower *= m_boneProtection.getBoneProtection(element); } VERIFY(NewHitPower >= 0.0f); @@ -195,7 +192,7 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo else { //пуля НЕ пробила бронь - NewHitPower *= m_boneProtection->m_fHitFrac; + NewHitPower *= m_boneProtection.m_fHitFrac; add_wound = false; //раны нет } } @@ -224,7 +221,7 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo { if (hit_type == ALife::eHitTypeFireWound) { - const float BoneArmor = m_boneProtection->getBoneArmor(element) * GetCondition(); + const float BoneArmor = m_boneProtection.getBoneArmor(element) * GetCondition(); if (ap > EPS && ap > BoneArmor) { @@ -232,12 +229,12 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo const float d_ap = ap - BoneArmor; NewHitPower *= (d_ap / ap); - if (NewHitPower < m_boneProtection->m_fHitFrac) - NewHitPower = m_boneProtection->m_fHitFrac; + if (NewHitPower < m_boneProtection.m_fHitFrac) + NewHitPower = m_boneProtection.m_fHitFrac; if (!IsGameTypeSingle()) { - NewHitPower *= m_boneProtection->getBoneProtection(element); + NewHitPower *= m_boneProtection.getBoneProtection(element); } if (NewHitPower < 0.0f) @@ -246,7 +243,7 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo else { //пуля НЕ пробила бронь - NewHitPower *= m_boneProtection->m_fHitFrac; + NewHitPower *= m_boneProtection.m_fHitFrac; add_wound = false; //раны нет } } @@ -274,10 +271,10 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo { if (hit_type == ALife::eHitTypeFireWound) { - const float BoneArmor = m_boneProtection->getBoneArmor(element) * GetCondition() * (1 - ap); + const float BoneArmor = m_boneProtection.getBoneArmor(element) * GetCondition() * (1 - ap); NewHitPower -= BoneArmor; - if (NewHitPower < hit_power * m_boneProtection->m_fHitFrac) - NewHitPower = hit_power * m_boneProtection->m_fHitFrac; + if (NewHitPower < hit_power * m_boneProtection.m_fHitFrac) + NewHitPower = hit_power * m_boneProtection.m_fHitFrac; } else { @@ -288,12 +285,12 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo Hit(hit_power, hit_type); break; } - } // switch (m_boneProtection->m_hitFracType) + } // switch (m_boneProtection.m_hitFracType) return NewHitPower; } -bool CCustomOutfit::BonePassBullet(int boneID) { return m_boneProtection->getBonePassBullet(s16(boneID)); } +bool CCustomOutfit::BonePassBullet(int boneID) { return m_boneProtection.getBonePassBullet(s16(boneID)); } #include "Torch.h" void CCustomOutfit::OnMoveToSlot(const SInvItemPlace& prev) { @@ -392,7 +389,7 @@ float CCustomOutfit::GetPowerLoss() const // Hit fraction and power loss are unrelated, // but it's the only way we can distinguish between SOC/CS and COP. // Sorry. - if (m_boneProtection->m_hitFracType != SBoneProtections::HitFractionActorCOP) + if (m_boneProtection.m_hitFracType != SBoneProtections::HitFractionActorCOP) { if (m_fPowerLoss < 1 && GetCondition() <= 0) { @@ -445,10 +442,10 @@ bool CCustomOutfit::install_upgrade_impl(LPCSTR section, bool test) AddBonesProtection(str); result |= result2; - if (m_boneProtection->m_hitFracType == SBoneProtections::HitFractionActorCS || - m_boneProtection->m_hitFracType == SBoneProtections::HitFractionActorCOP) + if (m_boneProtection.m_hitFracType == SBoneProtections::HitFractionActorCS || + m_boneProtection.m_hitFracType == SBoneProtections::HitFractionActorCOP) { - result |= process_if_exists(section, "hit_fraction_actor", &CInifile::r_float, m_boneProtection->m_fHitFrac, test); + result |= process_if_exists(section, "hit_fraction_actor", &CInifile::r_float, m_boneProtection.m_fHitFrac, test); } result |= process_if_exists(section, "additional_inventory_weight", &CInifile::r_float, m_additional_weight, test); @@ -477,5 +474,5 @@ void CCustomOutfit::AddBonesProtection(LPCSTR bones_section) parent = smart_cast(Level().CurrentViewEntity()); if (parent && parent->Visual() && m_BonesProtectionSect.size()) - m_boneProtection->add(bones_section, smart_cast(parent->Visual())); + m_boneProtection.add(bones_section, smart_cast(parent->Visual())); } diff --git a/src/xrGame/CustomOutfit.h b/src/xrGame/CustomOutfit.h index 882593be31d..834228d3045 100644 --- a/src/xrGame/CustomOutfit.h +++ b/src/xrGame/CustomOutfit.h @@ -2,7 +2,7 @@ #include "inventory_item_object.h" -struct SBoneProtections; +#include "BoneProtections.h" class CCustomOutfit : public CInventoryItemObject { @@ -10,7 +10,6 @@ class CCustomOutfit : public CInventoryItemObject public: CCustomOutfit(); - virtual ~CCustomOutfit(); virtual void Load(LPCSTR section); @@ -39,7 +38,7 @@ class CCustomOutfit : public CInventoryItemObject shared_str m_ActorVisual; shared_str m_full_icon_name; - SBoneProtections* m_boneProtection; + SBoneProtections m_boneProtection; protected: u32 m_ef_equipment_type; From 9fac292fd18da2cdd5ee8733c6e99084b27f02f2 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 02:28:32 +0300 Subject: [PATCH 077/261] Make CUIOutfitInfo work in SOC (#392) --- src/xrGame/BoneProtections.h | 3 + src/xrGame/CustomOutfit.cpp | 6 +- src/xrGame/CustomOutfit.h | 2 + src/xrGame/script_game_object3.cpp | 8 +- src/xrGame/ui/UIActorMenu.h | 2 + src/xrGame/ui/UIActorMenuInitialize.cpp | 8 +- src/xrGame/ui/UIActorMenuInventory.cpp | 6 + src/xrGame/ui/UIOutfitInfo.cpp | 188 +++++++++++++++++------- src/xrGame/ui/UIOutfitInfo.h | 13 +- 9 files changed, 165 insertions(+), 71 deletions(-) diff --git a/src/xrGame/BoneProtections.h b/src/xrGame/BoneProtections.h index 46a19adcd5e..8fa61098c63 100644 --- a/src/xrGame/BoneProtections.h +++ b/src/xrGame/BoneProtections.h @@ -13,9 +13,12 @@ struct SBoneProtections HitFraction, // Introduced in CS, externally assigned + // Used in outfits instead of HitFraction HitFractionActorCS, // Introduced in COP + // Used for NPCs to distinguish from hit_frac_monster, + // which is non-relevant to this struct HitFractionNPC, // Hit formula changed in COP, externally assigned diff --git a/src/xrGame/CustomOutfit.cpp b/src/xrGame/CustomOutfit.cpp index 985122c4773..1b4757a44a0 100644 --- a/src/xrGame/CustomOutfit.cpp +++ b/src/xrGame/CustomOutfit.cpp @@ -63,6 +63,8 @@ void CCustomOutfit::Load(LPCSTR section) m_HitTypeProtection[ALife::eHitTypeTelepatic] = pSettings->r_float(section, "telepatic_protection"); m_HitTypeProtection[ALife::eHitTypeChemicalBurn] = pSettings->r_float(section, "chemical_burn_protection"); m_HitTypeProtection[ALife::eHitTypeExplosion] = pSettings->r_float(section, "explosion_protection"); + // fire_wound_protection isn't used in hit calculations code, bone protections are used instead. + // This is used as a virtual value in the UI, and possibly in Lua scripts (which can do some real calculations). m_HitTypeProtection[ALife::eHitTypeFireWound] = pSettings->read_if_exists(section, "fire_wound_protection", 0.0f); m_HitTypeProtection[ALife::eHitTypePhysicStrike] = pSettings->read_if_exists( section, "physic_strike_protection", m_HitTypeProtection[ALife::eHitTypeStrike]); @@ -221,7 +223,7 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo { if (hit_type == ALife::eHitTypeFireWound) { - const float BoneArmor = m_boneProtection.getBoneArmor(element) * GetCondition(); + const float BoneArmor = GetBoneArmor(element) * GetCondition(); if (ap > EPS && ap > BoneArmor) { @@ -271,7 +273,7 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo { if (hit_type == ALife::eHitTypeFireWound) { - const float BoneArmor = m_boneProtection.getBoneArmor(element) * GetCondition() * (1 - ap); + const float BoneArmor = GetBoneArmor(element) * GetCondition() * (1 - ap); NewHitPower -= BoneArmor; if (NewHitPower < hit_power * m_boneProtection.m_fHitFrac) NewHitPower = hit_power * m_boneProtection.m_fHitFrac; diff --git a/src/xrGame/CustomOutfit.h b/src/xrGame/CustomOutfit.h index 834228d3045..1bce3fff360 100644 --- a/src/xrGame/CustomOutfit.h +++ b/src/xrGame/CustomOutfit.h @@ -23,6 +23,8 @@ class CCustomOutfit : public CInventoryItemObject [[nodiscard]] float GetDefHitTypeProtection(ALife::EHitType hit_type) const; [[nodiscard]] float GetBoneArmor(s16 element) const; + [[nodiscard]] auto GetHitFracType() const { return m_boneProtection.m_hitFracType; } + //коэффициент на который домножается потеря силы //если на персонаже надет костюм [[nodiscard]] float GetPowerLoss() const; diff --git a/src/xrGame/script_game_object3.cpp b/src/xrGame/script_game_object3.cpp index 8d55675619b..27482d5032a 100644 --- a/src/xrGame/script_game_object3.cpp +++ b/src/xrGame/script_game_object3.cpp @@ -299,11 +299,15 @@ float CScriptGameObject::GetCurrentOutfitProtection(int hit_type) return (0); } CGameObject* current_equipment = inventoryOwner->GetOutfit(); - CCustomOutfit* o = smart_cast(current_equipment); + const CCustomOutfit* o = smart_cast(current_equipment); if (!o) return 0.0f; - return o->GetDefHitTypeProtection(ALife::EHitType(hit_type)); + const float protection = o->GetDefHitTypeProtection(ALife::EHitType(hit_type)); + + if (o->GetHitFracType() == SBoneProtections::HitFraction) + return 1.0f - protection; // SOC + return protection; } CScriptGameObject* CScriptGameObject::GetFood() const diff --git a/src/xrGame/ui/UIActorMenu.h b/src/xrGame/ui/UIActorMenu.h index f80f063d226..84834054e6e 100644 --- a/src/xrGame/ui/UIActorMenu.h +++ b/src/xrGame/ui/UIActorMenu.h @@ -12,6 +12,7 @@ class CUICellItem; class CUIDragItem; class ui_actor_state_wnd; class CUIItemInfo; +class CUIOutfitInfo; class CUIFrameLineWnd; class CUIStatic; class CUI3tButton; @@ -153,6 +154,7 @@ class CUIActorMenu final : public CUIDialogWnd, public CUIWndCallback CUIItemInfo* m_ItemInfoInventoryMode{}; CUIItemInfo* m_ItemInfoTradeMode{}; CUIItemInfo* m_ItemInfoSearchLootMode{}; + CUIOutfitInfo* m_OutfitInfo{}; // Money CUIStatic* m_ActorMoney{}; diff --git a/src/xrGame/ui/UIActorMenuInitialize.cpp b/src/xrGame/ui/UIActorMenuInitialize.cpp index 3a172064777..27226cb232a 100644 --- a/src/xrGame/ui/UIActorMenuInitialize.cpp +++ b/src/xrGame/ui/UIActorMenuInitialize.cpp @@ -7,6 +7,7 @@ #include "UIDragDropReferenceList.h" #include "UIActorStateInfo.h" #include "UIItemInfo.h" +#include "UIOutfitInfo.h" #include "UITradeBar.h" #include "UIWeightBar.h" #include "xrUICore/Windows/UIFrameLineWnd.h" @@ -321,9 +322,10 @@ void CUIActorMenu::InitializeInventoryMode(CUIXml& uiXml) CUIFrameWindow* personalWnd = UIHelper::CreateFrameWindow(uiXml, "character_frame_window", m_pInventoryWnd); UIHelper::CreateStatic(uiXml, "static_personal", personalWnd); - CUIItemInfo* outfitInfo = xr_new(); - outfitInfo->SetAutoDelete(true); - m_pInventoryWnd->AttachChild(outfitInfo); + m_OutfitInfo = xr_new(); + m_OutfitInfo->SetAutoDelete(true); + m_pInventoryWnd->AttachChild(m_OutfitInfo); + m_OutfitInfo->InitFromXml(uiXml); std::tuple inventory_lists[] = { diff --git a/src/xrGame/ui/UIActorMenuInventory.cpp b/src/xrGame/ui/UIActorMenuInventory.cpp index 0d1ec74e450..30d76be535c 100644 --- a/src/xrGame/ui/UIActorMenuInventory.cpp +++ b/src/xrGame/ui/UIActorMenuInventory.cpp @@ -11,6 +11,7 @@ #include "UIDragDropReferenceList.h" #include "UICellCustomItems.h" #include "UIItemInfo.h" +#include "UIOutfitInfo.h" #include "xrUICore/Windows/UIFrameLineWnd.h" #include "xrUICore/PropertiesBox/UIPropertiesBox.h" #include "xrUICore/ListBox/UIListBoxItem.h" @@ -1547,6 +1548,11 @@ void CUIActorMenu::UpdateOutfit() m_pLists[eInventoryHelmetList]->SetCellsCapacity(m_pLists[eInventoryHelmetList]->MaxCellsCapacity()); } + if (m_OutfitInfo) + { + m_OutfitInfo->UpdateInfo(outfit, nullptr, true); + } + if (ShadowOfChernobylMode) { m_pLists[eInventoryBeltList]->ResetCellsCapacity(); diff --git a/src/xrGame/ui/UIOutfitInfo.cpp b/src/xrGame/ui/UIOutfitInfo.cpp index 356d377e672..2736a327e50 100644 --- a/src/xrGame/ui/UIOutfitInfo.cpp +++ b/src/xrGame/ui/UIOutfitInfo.cpp @@ -24,10 +24,9 @@ constexpr std::tuple immunity_names[] = { ALife::eHitTypeFireWound, "fire_wound_immunity", "ui_inv_outfit_fire_wound_protection" }, }; -CUIOutfitImmunity::CUIOutfitImmunity() - : CUIWindow("CUIOutfitImmunity"), m_name("Name"), m_value("Value") +CUIOutfitImmunity::CUIOutfitImmunity(pcstr immunity_name) + : CUIStatic(immunity_name), m_value("static_value") { - AttachChild(&m_name); AttachChild(&m_progress); AttachChild(&m_value); m_magnitude = 1.0f; @@ -35,145 +34,220 @@ CUIOutfitImmunity::CUIOutfitImmunity() bool CUIOutfitImmunity::InitFromXml(CUIXml& xml_doc, pcstr base_str, pcstr immunity, pcstr immunity_text) { - CUIXmlInit::InitWindow(xml_doc, base_str, 0, this); + float default_magnitude = 1.0f; string256 buf; - - strconcat(sizeof(buf), buf, base_str, ":", immunity); - if (!CUIXmlInit::InitWindow(xml_doc, buf, 0, this, false)) + strconcat(buf, base_str, ":", immunity); + if (!CUIXmlInit::InitStatic(xml_doc, buf, 0, this, false)) return false; - CUIXmlInit::InitStatic(xml_doc, buf, 0, &m_name); - m_name.TextItemControl()->SetTextST(immunity_text); + SetTextST(immunity_text); - strconcat(sizeof(buf), buf, base_str, ":", immunity, ":progress_immunity"); - m_progress.InitFromXml(xml_doc, buf); + strconcat(buf, base_str, ":", immunity, ":progress_immunity"); + if (xml_doc.NavigateToNode(buf, 0)) + { + m_progress.InitFromXml(xml_doc, buf); + m_progress.Show(true); + } + else + { + m_progress.Show(false); + } - strconcat(sizeof(buf), buf, base_str, ":", immunity, ":static_value"); + strconcat(buf, base_str, ":", immunity, ":static_value"); if (xml_doc.NavigateToNode(buf, 0) && !CallOfPripyatMode) { CUIXmlInit::InitStatic(xml_doc, buf, 0, &m_value); m_value.Show(true); } + else if (!m_progress.IsShown()) // SOC + { + strconcat(buf, base_str, ":", immunity); + CUIXmlInit::InitText(xml_doc, buf, 0, m_value.TextItemControl()); + + const auto font = GetFont(); + float text_len = font->SizeOf_(GetText()); + float space_len = font->SizeOf_(" "); + UI().ClientToScreenScaledWidth(text_len); + UI().ClientToScreenScaledWidth(space_len); + + auto rect = GetWndRect(); + rect.x1 += TextItemControl()->m_TextOffset.x + text_len + space_len; + m_value.SetWndRect(rect); + + m_value.Show(true); + default_magnitude = 100.0f; + } else { m_value.Show(false); } - m_magnitude = xml_doc.ReadAttribFlt(buf, 0, "magnitude", 1.0f); + m_magnitude = xml_doc.ReadAttribFlt(buf, 0, "magnitude", default_magnitude); return true; } void CUIOutfitImmunity::SetProgressValue(float cur, float comp) { + string128 buf; + cur *= m_magnitude; comp *= m_magnitude; - m_progress.SetTwoPos(cur, comp); - string32 buf; - // xr_sprintf( buf, sizeof(buf), "%d %%", (int)cur ); - xr_sprintf(buf, sizeof(buf), "%.0f", cur); + + if (m_progress.IsShown()) + { + m_progress.SetTwoPos(cur, comp); + xr_sprintf(buf, "%.0f", cur); + } + else // SOC + { + xr_sprintf(buf, "%s %+3.0f%%", cur > 0.0f ? "%c[green]" : "%c[red]", cur); + } + m_value.SetText(buf); } // =========================================================================================== void CUIOutfitInfo::InitFromXml(CUIXml& xml_doc) { - const LPCSTR base_str = "outfit_info"; + constexpr pcstr base_str = "outfit_info"; CUIXmlInit::InitWindow(xml_doc, base_str, 0, this); string128 buf; - strconcat(sizeof(buf), buf, base_str, ":caption"); - m_caption = UIHelper::CreateStatic(xml_doc, buf, this, false); - - strconcat(sizeof(buf), buf, base_str, ":", "prop_line"); - m_Prop_line = UIHelper::CreateStatic(xml_doc, buf, this, false); + strconcat(buf, base_str, ":caption"); + const auto caption = UIHelper::CreateStatic(xml_doc, buf, this, false); - Fvector2 pos{}; + strconcat(buf, base_str, ":prop_line"); + const auto prop_line = UIHelper::CreateStatic(xml_doc, buf, this, false); - if (m_Prop_line) - pos.set(0.0f, m_Prop_line->GetWndPos().y + m_Prop_line->GetWndSize().y); - else if (m_caption) - pos.set(0.0f, m_caption->GetWndSize().y); + if (prop_line) + m_start_pos = { 0.0f, prop_line->GetWndPos().y + prop_line->GetWndSize().y }; + else if (caption) + m_start_pos = { 0.0f, caption->GetWndSize().y }; + else // SOC + { + strconcat(buf, base_str, ":scroll_view"); + if (const auto node = xml_doc.NavigateToNode(buf)) + { + const auto x = xml_doc.ReadAttribFlt(node, "x", 0.0f); + const auto y = xml_doc.ReadAttribFlt(node, "y", 0.0f); + m_start_pos = { x, y }; + } + } for (const auto [hit_type, immunity, immunity_text] : immunity_names) { - auto item = xr_new(); - if (!item->InitFromXml(xml_doc, base_str, immunity, immunity_text)) + auto item = xr_new(immunity); + + strconcat(buf, "static_", immunity); // SOC + if (item->InitFromXml(xml_doc, base_str, immunity, immunity_text) || + item->InitFromXml(xml_doc, base_str, buf, immunity_text)) { - xr_delete(item); + item->SetAutoDelete(true); + AttachChild(item); + m_items[hit_type] = item; continue; } - item->SetAutoDelete(true); - AttachChild(item); + xr_delete(item); + } + + AdjustElements(); +} + +void CUIOutfitInfo::AdjustElements() +{ + auto pos = m_start_pos; + + for (const auto [hit_type, _, __] : immunity_names) + { + auto item = m_items[hit_type]; + if (!item || !item->IsShown()) + continue; item->SetWndPos(pos); pos.y += item->GetWndSize().y; - m_items[hit_type] = item; } - pos.x = GetWndSize().x; SetWndSize(pos); } -void CUIOutfitInfo::UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit) +void CUIOutfitInfo::UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit /*= nullptr*/, bool hide_if_zero_immunity /*= false*/) { const CActor* actor = smart_cast(Level().CurrentViewEntity()); if (!actor || !cur_outfit) { + if (hide_if_zero_immunity) + { + for (auto& [hit_type, item] : m_items) + { + if (item) + item->Show(false); + } + } return; } + const bool is_cs_cop = cur_outfit->GetHitFracType() != SBoneProtections::HitFraction; + for (auto& [hit_type, item] : m_items) { if (!item) continue; - if (hit_type == ALife::eHitTypeFireWound) + if (hit_type == ALife::eHitTypeFireWound && is_cs_cop) continue; - const float max_power = actor->conditions().GetZoneMaxPower(hit_type); - - float cur = cur_outfit->GetDefHitTypeProtection(hit_type); - cur /= max_power; // = 0..1 - float slot = cur; + float cur = cur_outfit->GetDefHitTypeProtection(hit_type); + float slot = slot_outfit ? slot_outfit->GetDefHitTypeProtection(hit_type) : cur; - if (slot_outfit) + if (is_cs_cop) { - slot = slot_outfit->GetDefHitTypeProtection(hit_type); + const float max_power = actor->conditions().GetZoneMaxPower(hit_type); + cur /= max_power; // = 0..1 slot /= max_power; // = 0..1 } - item->SetProgressValue(cur, slot); + else // SOC + { + cur = 1.0f - cur; + slot = 1.0f - slot; + } + + const bool hide = hide_if_zero_immunity && fis_zero(cur) && fis_zero(slot); + item->Show(!hide); + + if (!hide) + item->SetProgressValue(cur, slot); } - if (const auto& fireWoundItem = m_items[ALife::eHitTypeFireWound]) + if (const auto& fireWoundItem = m_items[ALife::eHitTypeFireWound]; + fireWoundItem && is_cs_cop) { IKinematics* ikv = smart_cast(actor->Visual()); VERIFY(ikv); u16 spine_bone = ikv->LL_BoneID("bip01_spine"); float cur = cur_outfit->GetBoneArmor(spine_bone) * cur_outfit->GetCondition(); - // if(!cur_outfit->bIsHelmetAvaliable) - //{ - // spine_bone = ikv->LL_BoneID("bip01_head"); - // cur += cur_outfit->GetBoneArmor(spine_bone); - //} + float slot = cur; if (slot_outfit) { spine_bone = ikv->LL_BoneID("bip01_spine"); slot = slot_outfit->GetBoneArmor(spine_bone) * slot_outfit->GetCondition(); - // if(!slot_outfit->bIsHelmetAvaliable) - //{ - // spine_bone = ikv->LL_BoneID("bip01_head"); - // slot += slot_outfit->GetBoneArmor(spine_bone); - //} + } const float max_power = actor->conditions().GetMaxFireWoundProtection(); cur /= max_power; slot /= max_power; - fireWoundItem->SetProgressValue(cur, slot); + + const bool hide = hide_if_zero_immunity && fis_zero(cur) && fis_zero(slot); + fireWoundItem->Show(!hide); + + if (!hide) + fireWoundItem->SetProgressValue(cur, slot); } + + AdjustElements(); } void CUIOutfitInfo::UpdateInfo(CHelmet* cur_helmet, CHelmet* slot_helmet) diff --git a/src/xrGame/ui/UIOutfitInfo.h b/src/xrGame/ui/UIOutfitInfo.h index 72a632a6a3f..29b79a07de6 100644 --- a/src/xrGame/ui/UIOutfitInfo.h +++ b/src/xrGame/ui/UIOutfitInfo.h @@ -9,10 +9,10 @@ class CUIStatic; class CUIDoubleProgressBar; class CUIXml; -class CUIOutfitImmunity final : public CUIWindow +class CUIOutfitImmunity final : public CUIStatic { public: - CUIOutfitImmunity(); + CUIOutfitImmunity(pcstr immunity_name); bool InitFromXml(CUIXml& xml_doc, pcstr base_str, pcstr immunity, pcstr immunity_text); void SetProgressValue(float cur, float comp); @@ -20,11 +20,9 @@ class CUIOutfitImmunity final : public CUIWindow pcstr GetDebugType() override { return "CUIOutfitImmunity"; } protected: - CUIStatic m_name; // texture + name CUIDoubleProgressBar m_progress; CUIStatic m_value; // 100% float m_magnitude; - }; // class CUIOutfitImmunity // ------------------------------------------------------------------------------------- @@ -35,13 +33,14 @@ class CUIOutfitInfo final : public CUIWindow CUIOutfitInfo() : CUIWindow("CUIOutfitInfo") {} void InitFromXml(CUIXml& xml_doc); - void UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit = nullptr); + void UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit = nullptr, bool hide_if_zero_immunity = false); void UpdateInfo(CHelmet* cur_helmet, CHelmet* slot_helmet = nullptr); + void AdjustElements(); + pcstr GetDebugType() override { return "CUIOutfitInfo"; } protected: - CUIStatic* m_caption{}; - CUIStatic* m_Prop_line{}; xr_unordered_map m_items; + Fvector2 m_start_pos{}; }; // class CUIOutfitInfo From f2f9607fea44807b318c901100a2932361c09047 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 02:28:32 +0300 Subject: [PATCH 078/261] Make CUIOutfitInfo work in SOC (#392) --- src/xrGame/BoneProtections.h | 3 + src/xrGame/CustomOutfit.cpp | 6 +- src/xrGame/CustomOutfit.h | 2 + src/xrGame/script_game_object3.cpp | 8 +- src/xrGame/ui/UIActorMenu.h | 2 + src/xrGame/ui/UIActorMenuInitialize.cpp | 8 +- src/xrGame/ui/UIActorMenuInventory.cpp | 6 + src/xrGame/ui/UIOutfitInfo.cpp | 188 +++++++++++++++++------- src/xrGame/ui/UIOutfitInfo.h | 13 +- 9 files changed, 165 insertions(+), 71 deletions(-) diff --git a/src/xrGame/BoneProtections.h b/src/xrGame/BoneProtections.h index 46a19adcd5e..8fa61098c63 100644 --- a/src/xrGame/BoneProtections.h +++ b/src/xrGame/BoneProtections.h @@ -13,9 +13,12 @@ struct SBoneProtections HitFraction, // Introduced in CS, externally assigned + // Used in outfits instead of HitFraction HitFractionActorCS, // Introduced in COP + // Used for NPCs to distinguish from hit_frac_monster, + // which is non-relevant to this struct HitFractionNPC, // Hit formula changed in COP, externally assigned diff --git a/src/xrGame/CustomOutfit.cpp b/src/xrGame/CustomOutfit.cpp index 985122c4773..1b4757a44a0 100644 --- a/src/xrGame/CustomOutfit.cpp +++ b/src/xrGame/CustomOutfit.cpp @@ -63,6 +63,8 @@ void CCustomOutfit::Load(LPCSTR section) m_HitTypeProtection[ALife::eHitTypeTelepatic] = pSettings->r_float(section, "telepatic_protection"); m_HitTypeProtection[ALife::eHitTypeChemicalBurn] = pSettings->r_float(section, "chemical_burn_protection"); m_HitTypeProtection[ALife::eHitTypeExplosion] = pSettings->r_float(section, "explosion_protection"); + // fire_wound_protection isn't used in hit calculations code, bone protections are used instead. + // This is used as a virtual value in the UI, and possibly in Lua scripts (which can do some real calculations). m_HitTypeProtection[ALife::eHitTypeFireWound] = pSettings->read_if_exists(section, "fire_wound_protection", 0.0f); m_HitTypeProtection[ALife::eHitTypePhysicStrike] = pSettings->read_if_exists( section, "physic_strike_protection", m_HitTypeProtection[ALife::eHitTypeStrike]); @@ -221,7 +223,7 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo { if (hit_type == ALife::eHitTypeFireWound) { - const float BoneArmor = m_boneProtection.getBoneArmor(element) * GetCondition(); + const float BoneArmor = GetBoneArmor(element) * GetCondition(); if (ap > EPS && ap > BoneArmor) { @@ -271,7 +273,7 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo { if (hit_type == ALife::eHitTypeFireWound) { - const float BoneArmor = m_boneProtection.getBoneArmor(element) * GetCondition() * (1 - ap); + const float BoneArmor = GetBoneArmor(element) * GetCondition() * (1 - ap); NewHitPower -= BoneArmor; if (NewHitPower < hit_power * m_boneProtection.m_fHitFrac) NewHitPower = hit_power * m_boneProtection.m_fHitFrac; diff --git a/src/xrGame/CustomOutfit.h b/src/xrGame/CustomOutfit.h index 834228d3045..1bce3fff360 100644 --- a/src/xrGame/CustomOutfit.h +++ b/src/xrGame/CustomOutfit.h @@ -23,6 +23,8 @@ class CCustomOutfit : public CInventoryItemObject [[nodiscard]] float GetDefHitTypeProtection(ALife::EHitType hit_type) const; [[nodiscard]] float GetBoneArmor(s16 element) const; + [[nodiscard]] auto GetHitFracType() const { return m_boneProtection.m_hitFracType; } + //коэффициент на который домножается потеря силы //если на персонаже надет костюм [[nodiscard]] float GetPowerLoss() const; diff --git a/src/xrGame/script_game_object3.cpp b/src/xrGame/script_game_object3.cpp index 8d55675619b..27482d5032a 100644 --- a/src/xrGame/script_game_object3.cpp +++ b/src/xrGame/script_game_object3.cpp @@ -299,11 +299,15 @@ float CScriptGameObject::GetCurrentOutfitProtection(int hit_type) return (0); } CGameObject* current_equipment = inventoryOwner->GetOutfit(); - CCustomOutfit* o = smart_cast(current_equipment); + const CCustomOutfit* o = smart_cast(current_equipment); if (!o) return 0.0f; - return o->GetDefHitTypeProtection(ALife::EHitType(hit_type)); + const float protection = o->GetDefHitTypeProtection(ALife::EHitType(hit_type)); + + if (o->GetHitFracType() == SBoneProtections::HitFraction) + return 1.0f - protection; // SOC + return protection; } CScriptGameObject* CScriptGameObject::GetFood() const diff --git a/src/xrGame/ui/UIActorMenu.h b/src/xrGame/ui/UIActorMenu.h index f80f063d226..84834054e6e 100644 --- a/src/xrGame/ui/UIActorMenu.h +++ b/src/xrGame/ui/UIActorMenu.h @@ -12,6 +12,7 @@ class CUICellItem; class CUIDragItem; class ui_actor_state_wnd; class CUIItemInfo; +class CUIOutfitInfo; class CUIFrameLineWnd; class CUIStatic; class CUI3tButton; @@ -153,6 +154,7 @@ class CUIActorMenu final : public CUIDialogWnd, public CUIWndCallback CUIItemInfo* m_ItemInfoInventoryMode{}; CUIItemInfo* m_ItemInfoTradeMode{}; CUIItemInfo* m_ItemInfoSearchLootMode{}; + CUIOutfitInfo* m_OutfitInfo{}; // Money CUIStatic* m_ActorMoney{}; diff --git a/src/xrGame/ui/UIActorMenuInitialize.cpp b/src/xrGame/ui/UIActorMenuInitialize.cpp index 3a172064777..27226cb232a 100644 --- a/src/xrGame/ui/UIActorMenuInitialize.cpp +++ b/src/xrGame/ui/UIActorMenuInitialize.cpp @@ -7,6 +7,7 @@ #include "UIDragDropReferenceList.h" #include "UIActorStateInfo.h" #include "UIItemInfo.h" +#include "UIOutfitInfo.h" #include "UITradeBar.h" #include "UIWeightBar.h" #include "xrUICore/Windows/UIFrameLineWnd.h" @@ -321,9 +322,10 @@ void CUIActorMenu::InitializeInventoryMode(CUIXml& uiXml) CUIFrameWindow* personalWnd = UIHelper::CreateFrameWindow(uiXml, "character_frame_window", m_pInventoryWnd); UIHelper::CreateStatic(uiXml, "static_personal", personalWnd); - CUIItemInfo* outfitInfo = xr_new(); - outfitInfo->SetAutoDelete(true); - m_pInventoryWnd->AttachChild(outfitInfo); + m_OutfitInfo = xr_new(); + m_OutfitInfo->SetAutoDelete(true); + m_pInventoryWnd->AttachChild(m_OutfitInfo); + m_OutfitInfo->InitFromXml(uiXml); std::tuple inventory_lists[] = { diff --git a/src/xrGame/ui/UIActorMenuInventory.cpp b/src/xrGame/ui/UIActorMenuInventory.cpp index 0d1ec74e450..30d76be535c 100644 --- a/src/xrGame/ui/UIActorMenuInventory.cpp +++ b/src/xrGame/ui/UIActorMenuInventory.cpp @@ -11,6 +11,7 @@ #include "UIDragDropReferenceList.h" #include "UICellCustomItems.h" #include "UIItemInfo.h" +#include "UIOutfitInfo.h" #include "xrUICore/Windows/UIFrameLineWnd.h" #include "xrUICore/PropertiesBox/UIPropertiesBox.h" #include "xrUICore/ListBox/UIListBoxItem.h" @@ -1547,6 +1548,11 @@ void CUIActorMenu::UpdateOutfit() m_pLists[eInventoryHelmetList]->SetCellsCapacity(m_pLists[eInventoryHelmetList]->MaxCellsCapacity()); } + if (m_OutfitInfo) + { + m_OutfitInfo->UpdateInfo(outfit, nullptr, true); + } + if (ShadowOfChernobylMode) { m_pLists[eInventoryBeltList]->ResetCellsCapacity(); diff --git a/src/xrGame/ui/UIOutfitInfo.cpp b/src/xrGame/ui/UIOutfitInfo.cpp index 356d377e672..2736a327e50 100644 --- a/src/xrGame/ui/UIOutfitInfo.cpp +++ b/src/xrGame/ui/UIOutfitInfo.cpp @@ -24,10 +24,9 @@ constexpr std::tuple immunity_names[] = { ALife::eHitTypeFireWound, "fire_wound_immunity", "ui_inv_outfit_fire_wound_protection" }, }; -CUIOutfitImmunity::CUIOutfitImmunity() - : CUIWindow("CUIOutfitImmunity"), m_name("Name"), m_value("Value") +CUIOutfitImmunity::CUIOutfitImmunity(pcstr immunity_name) + : CUIStatic(immunity_name), m_value("static_value") { - AttachChild(&m_name); AttachChild(&m_progress); AttachChild(&m_value); m_magnitude = 1.0f; @@ -35,145 +34,220 @@ CUIOutfitImmunity::CUIOutfitImmunity() bool CUIOutfitImmunity::InitFromXml(CUIXml& xml_doc, pcstr base_str, pcstr immunity, pcstr immunity_text) { - CUIXmlInit::InitWindow(xml_doc, base_str, 0, this); + float default_magnitude = 1.0f; string256 buf; - - strconcat(sizeof(buf), buf, base_str, ":", immunity); - if (!CUIXmlInit::InitWindow(xml_doc, buf, 0, this, false)) + strconcat(buf, base_str, ":", immunity); + if (!CUIXmlInit::InitStatic(xml_doc, buf, 0, this, false)) return false; - CUIXmlInit::InitStatic(xml_doc, buf, 0, &m_name); - m_name.TextItemControl()->SetTextST(immunity_text); + SetTextST(immunity_text); - strconcat(sizeof(buf), buf, base_str, ":", immunity, ":progress_immunity"); - m_progress.InitFromXml(xml_doc, buf); + strconcat(buf, base_str, ":", immunity, ":progress_immunity"); + if (xml_doc.NavigateToNode(buf, 0)) + { + m_progress.InitFromXml(xml_doc, buf); + m_progress.Show(true); + } + else + { + m_progress.Show(false); + } - strconcat(sizeof(buf), buf, base_str, ":", immunity, ":static_value"); + strconcat(buf, base_str, ":", immunity, ":static_value"); if (xml_doc.NavigateToNode(buf, 0) && !CallOfPripyatMode) { CUIXmlInit::InitStatic(xml_doc, buf, 0, &m_value); m_value.Show(true); } + else if (!m_progress.IsShown()) // SOC + { + strconcat(buf, base_str, ":", immunity); + CUIXmlInit::InitText(xml_doc, buf, 0, m_value.TextItemControl()); + + const auto font = GetFont(); + float text_len = font->SizeOf_(GetText()); + float space_len = font->SizeOf_(" "); + UI().ClientToScreenScaledWidth(text_len); + UI().ClientToScreenScaledWidth(space_len); + + auto rect = GetWndRect(); + rect.x1 += TextItemControl()->m_TextOffset.x + text_len + space_len; + m_value.SetWndRect(rect); + + m_value.Show(true); + default_magnitude = 100.0f; + } else { m_value.Show(false); } - m_magnitude = xml_doc.ReadAttribFlt(buf, 0, "magnitude", 1.0f); + m_magnitude = xml_doc.ReadAttribFlt(buf, 0, "magnitude", default_magnitude); return true; } void CUIOutfitImmunity::SetProgressValue(float cur, float comp) { + string128 buf; + cur *= m_magnitude; comp *= m_magnitude; - m_progress.SetTwoPos(cur, comp); - string32 buf; - // xr_sprintf( buf, sizeof(buf), "%d %%", (int)cur ); - xr_sprintf(buf, sizeof(buf), "%.0f", cur); + + if (m_progress.IsShown()) + { + m_progress.SetTwoPos(cur, comp); + xr_sprintf(buf, "%.0f", cur); + } + else // SOC + { + xr_sprintf(buf, "%s %+3.0f%%", cur > 0.0f ? "%c[green]" : "%c[red]", cur); + } + m_value.SetText(buf); } // =========================================================================================== void CUIOutfitInfo::InitFromXml(CUIXml& xml_doc) { - const LPCSTR base_str = "outfit_info"; + constexpr pcstr base_str = "outfit_info"; CUIXmlInit::InitWindow(xml_doc, base_str, 0, this); string128 buf; - strconcat(sizeof(buf), buf, base_str, ":caption"); - m_caption = UIHelper::CreateStatic(xml_doc, buf, this, false); - - strconcat(sizeof(buf), buf, base_str, ":", "prop_line"); - m_Prop_line = UIHelper::CreateStatic(xml_doc, buf, this, false); + strconcat(buf, base_str, ":caption"); + const auto caption = UIHelper::CreateStatic(xml_doc, buf, this, false); - Fvector2 pos{}; + strconcat(buf, base_str, ":prop_line"); + const auto prop_line = UIHelper::CreateStatic(xml_doc, buf, this, false); - if (m_Prop_line) - pos.set(0.0f, m_Prop_line->GetWndPos().y + m_Prop_line->GetWndSize().y); - else if (m_caption) - pos.set(0.0f, m_caption->GetWndSize().y); + if (prop_line) + m_start_pos = { 0.0f, prop_line->GetWndPos().y + prop_line->GetWndSize().y }; + else if (caption) + m_start_pos = { 0.0f, caption->GetWndSize().y }; + else // SOC + { + strconcat(buf, base_str, ":scroll_view"); + if (const auto node = xml_doc.NavigateToNode(buf)) + { + const auto x = xml_doc.ReadAttribFlt(node, "x", 0.0f); + const auto y = xml_doc.ReadAttribFlt(node, "y", 0.0f); + m_start_pos = { x, y }; + } + } for (const auto [hit_type, immunity, immunity_text] : immunity_names) { - auto item = xr_new(); - if (!item->InitFromXml(xml_doc, base_str, immunity, immunity_text)) + auto item = xr_new(immunity); + + strconcat(buf, "static_", immunity); // SOC + if (item->InitFromXml(xml_doc, base_str, immunity, immunity_text) || + item->InitFromXml(xml_doc, base_str, buf, immunity_text)) { - xr_delete(item); + item->SetAutoDelete(true); + AttachChild(item); + m_items[hit_type] = item; continue; } - item->SetAutoDelete(true); - AttachChild(item); + xr_delete(item); + } + + AdjustElements(); +} + +void CUIOutfitInfo::AdjustElements() +{ + auto pos = m_start_pos; + + for (const auto [hit_type, _, __] : immunity_names) + { + auto item = m_items[hit_type]; + if (!item || !item->IsShown()) + continue; item->SetWndPos(pos); pos.y += item->GetWndSize().y; - m_items[hit_type] = item; } - pos.x = GetWndSize().x; SetWndSize(pos); } -void CUIOutfitInfo::UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit) +void CUIOutfitInfo::UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit /*= nullptr*/, bool hide_if_zero_immunity /*= false*/) { const CActor* actor = smart_cast(Level().CurrentViewEntity()); if (!actor || !cur_outfit) { + if (hide_if_zero_immunity) + { + for (auto& [hit_type, item] : m_items) + { + if (item) + item->Show(false); + } + } return; } + const bool is_cs_cop = cur_outfit->GetHitFracType() != SBoneProtections::HitFraction; + for (auto& [hit_type, item] : m_items) { if (!item) continue; - if (hit_type == ALife::eHitTypeFireWound) + if (hit_type == ALife::eHitTypeFireWound && is_cs_cop) continue; - const float max_power = actor->conditions().GetZoneMaxPower(hit_type); - - float cur = cur_outfit->GetDefHitTypeProtection(hit_type); - cur /= max_power; // = 0..1 - float slot = cur; + float cur = cur_outfit->GetDefHitTypeProtection(hit_type); + float slot = slot_outfit ? slot_outfit->GetDefHitTypeProtection(hit_type) : cur; - if (slot_outfit) + if (is_cs_cop) { - slot = slot_outfit->GetDefHitTypeProtection(hit_type); + const float max_power = actor->conditions().GetZoneMaxPower(hit_type); + cur /= max_power; // = 0..1 slot /= max_power; // = 0..1 } - item->SetProgressValue(cur, slot); + else // SOC + { + cur = 1.0f - cur; + slot = 1.0f - slot; + } + + const bool hide = hide_if_zero_immunity && fis_zero(cur) && fis_zero(slot); + item->Show(!hide); + + if (!hide) + item->SetProgressValue(cur, slot); } - if (const auto& fireWoundItem = m_items[ALife::eHitTypeFireWound]) + if (const auto& fireWoundItem = m_items[ALife::eHitTypeFireWound]; + fireWoundItem && is_cs_cop) { IKinematics* ikv = smart_cast(actor->Visual()); VERIFY(ikv); u16 spine_bone = ikv->LL_BoneID("bip01_spine"); float cur = cur_outfit->GetBoneArmor(spine_bone) * cur_outfit->GetCondition(); - // if(!cur_outfit->bIsHelmetAvaliable) - //{ - // spine_bone = ikv->LL_BoneID("bip01_head"); - // cur += cur_outfit->GetBoneArmor(spine_bone); - //} + float slot = cur; if (slot_outfit) { spine_bone = ikv->LL_BoneID("bip01_spine"); slot = slot_outfit->GetBoneArmor(spine_bone) * slot_outfit->GetCondition(); - // if(!slot_outfit->bIsHelmetAvaliable) - //{ - // spine_bone = ikv->LL_BoneID("bip01_head"); - // slot += slot_outfit->GetBoneArmor(spine_bone); - //} + } const float max_power = actor->conditions().GetMaxFireWoundProtection(); cur /= max_power; slot /= max_power; - fireWoundItem->SetProgressValue(cur, slot); + + const bool hide = hide_if_zero_immunity && fis_zero(cur) && fis_zero(slot); + fireWoundItem->Show(!hide); + + if (!hide) + fireWoundItem->SetProgressValue(cur, slot); } + + AdjustElements(); } void CUIOutfitInfo::UpdateInfo(CHelmet* cur_helmet, CHelmet* slot_helmet) diff --git a/src/xrGame/ui/UIOutfitInfo.h b/src/xrGame/ui/UIOutfitInfo.h index 72a632a6a3f..29b79a07de6 100644 --- a/src/xrGame/ui/UIOutfitInfo.h +++ b/src/xrGame/ui/UIOutfitInfo.h @@ -9,10 +9,10 @@ class CUIStatic; class CUIDoubleProgressBar; class CUIXml; -class CUIOutfitImmunity final : public CUIWindow +class CUIOutfitImmunity final : public CUIStatic { public: - CUIOutfitImmunity(); + CUIOutfitImmunity(pcstr immunity_name); bool InitFromXml(CUIXml& xml_doc, pcstr base_str, pcstr immunity, pcstr immunity_text); void SetProgressValue(float cur, float comp); @@ -20,11 +20,9 @@ class CUIOutfitImmunity final : public CUIWindow pcstr GetDebugType() override { return "CUIOutfitImmunity"; } protected: - CUIStatic m_name; // texture + name CUIDoubleProgressBar m_progress; CUIStatic m_value; // 100% float m_magnitude; - }; // class CUIOutfitImmunity // ------------------------------------------------------------------------------------- @@ -35,13 +33,14 @@ class CUIOutfitInfo final : public CUIWindow CUIOutfitInfo() : CUIWindow("CUIOutfitInfo") {} void InitFromXml(CUIXml& xml_doc); - void UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit = nullptr); + void UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit = nullptr, bool hide_if_zero_immunity = false); void UpdateInfo(CHelmet* cur_helmet, CHelmet* slot_helmet = nullptr); + void AdjustElements(); + pcstr GetDebugType() override { return "CUIOutfitInfo"; } protected: - CUIStatic* m_caption{}; - CUIStatic* m_Prop_line{}; xr_unordered_map m_items; + Fvector2 m_start_pos{}; }; // class CUIOutfitInfo From a8f6db41361f833e837f096c10a47a21079ba7db Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 02:39:02 +0300 Subject: [PATCH 079/261] Get rid of nagging warning about missing ed/ed_not_existing_texture.dds That's a stub texture, which doesn't exist in SOC, so this warning really nags me when I work on SOC. --- src/Layers/xrRenderDX11/dx11Texture.cpp | 3 ++- src/Layers/xrRenderGL/glTexture.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Layers/xrRenderDX11/dx11Texture.cpp b/src/Layers/xrRenderDX11/dx11Texture.cpp index 299cff63412..7a11a082fad 100644 --- a/src/Layers/xrRenderDX11/dx11Texture.cpp +++ b/src/Layers/xrRenderDX11/dx11Texture.cpp @@ -131,7 +131,8 @@ ID3DBaseTexture* CRender::texture_load(LPCSTR fRName, u32& ret_msize) if (!exist) { Msg("! Can't find texture '%s'", fname); - R_ASSERT1_CURE(FS.exist(fn, "$game_textures$", "ed\\ed_not_existing_texture", ".dds"), return nullptr); + if (!FS.exist(fn, "$game_textures$", "ed\\ed_not_existing_texture", ".dds")) + return nullptr; } } } diff --git a/src/Layers/xrRenderGL/glTexture.cpp b/src/Layers/xrRenderGL/glTexture.cpp index 47898f20e34..c2ca771c0f7 100644 --- a/src/Layers/xrRenderGL/glTexture.cpp +++ b/src/Layers/xrRenderGL/glTexture.cpp @@ -95,7 +95,8 @@ GLuint CRender::texture_load(LPCSTR fRName, u32& ret_msize, GLenum& ret_desc) if (!exist) { Msg("! Can't find texture '%s'", fname); - R_ASSERT1_CURE(FS.exist(fn, "$game_textures$", "ed\\ed_not_existing_texture", ".dds"), return 0); + if (!FS.exist(fn, "$game_textures$", "ed\\ed_not_existing_texture", ".dds")) + return 0; } } } From 786e2f0bfd2ffcb38d325bcf34cc0520623d243c Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 02:39:02 +0300 Subject: [PATCH 080/261] Get rid of nagging warning about missing ed/ed_not_existing_texture.dds That's a stub texture, which doesn't exist in SOC, so this warning really nags me when I work on SOC. --- src/Layers/xrRenderDX11/dx11Texture.cpp | 3 ++- src/Layers/xrRenderGL/glTexture.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Layers/xrRenderDX11/dx11Texture.cpp b/src/Layers/xrRenderDX11/dx11Texture.cpp index 299cff63412..7a11a082fad 100644 --- a/src/Layers/xrRenderDX11/dx11Texture.cpp +++ b/src/Layers/xrRenderDX11/dx11Texture.cpp @@ -131,7 +131,8 @@ ID3DBaseTexture* CRender::texture_load(LPCSTR fRName, u32& ret_msize) if (!exist) { Msg("! Can't find texture '%s'", fname); - R_ASSERT1_CURE(FS.exist(fn, "$game_textures$", "ed\\ed_not_existing_texture", ".dds"), return nullptr); + if (!FS.exist(fn, "$game_textures$", "ed\\ed_not_existing_texture", ".dds")) + return nullptr; } } } diff --git a/src/Layers/xrRenderGL/glTexture.cpp b/src/Layers/xrRenderGL/glTexture.cpp index 47898f20e34..c2ca771c0f7 100644 --- a/src/Layers/xrRenderGL/glTexture.cpp +++ b/src/Layers/xrRenderGL/glTexture.cpp @@ -95,7 +95,8 @@ GLuint CRender::texture_load(LPCSTR fRName, u32& ret_msize, GLenum& ret_desc) if (!exist) { Msg("! Can't find texture '%s'", fname); - R_ASSERT1_CURE(FS.exist(fn, "$game_textures$", "ed\\ed_not_existing_texture", ".dds"), return 0); + if (!FS.exist(fn, "$game_textures$", "ed\\ed_not_existing_texture", ".dds")) + return 0; } } } From 71ffa3262d5a1d9a8d4741cdd52311d1d849f6ac Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 10:51:59 +0300 Subject: [PATCH 081/261] Provide better overloads of CStringTable::translate --- src/xrEngine/StringTable/StringTable.cpp | 17 ++++++++++++----- src/xrEngine/StringTable/StringTable.h | 10 +++++++++- src/xrGame/ui/UIActorMenuInventory.cpp | 2 +- src/xrGame/ui/UIItemInfo.cpp | 6 ++++-- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/xrEngine/StringTable/StringTable.cpp b/src/xrEngine/StringTable/StringTable.cpp index 6f5466777ef..963d897e616 100644 --- a/src/xrEngine/StringTable/StringTable.cpp +++ b/src/xrEngine/StringTable/StringTable.cpp @@ -307,6 +307,14 @@ STRING_VALUE CStringTable::translate(const STRING_ID& str_id) const return str_id; } +STRING_VALUE CStringTable::translate(const STRING_ID& str_id, const STRING_ID& str_id2) const +{ + STRING_VALUE out = str_id; + if (!translate(str_id, out)) + translate(str_id2, out); + return out; +} + bool CStringTable::translate(const STRING_ID& str_id, STRING_VALUE& out) const { if (!pData) @@ -320,11 +328,10 @@ bool CStringTable::translate(const STRING_ID& str_id, STRING_VALUE& out) const return false; } -pcstr CStringTable::translate(const STRING_ID& str_id, pcstr default_value) const +bool CStringTable::has_translation(const STRING_ID& str_id) const { - STRING_VALUE out; - if (translate(str_id, out)) - return out.c_str(); + if (!pData) + return false; - return default_value; + return pData->m_StringTable.find(str_id) != pData->m_StringTable.end(); } diff --git a/src/xrEngine/StringTable/StringTable.h b/src/xrEngine/StringTable/StringTable.h index 5eda57009a7..d096711e4cd 100644 --- a/src/xrEngine/StringTable/StringTable.h +++ b/src/xrEngine/StringTable/StringTable.h @@ -29,9 +29,17 @@ class ENGINE_API CStringTable final void Init(); static void Destroy(); + [[nodiscard]] STRING_VALUE translate(const STRING_ID& str_id) const; + + [[nodiscard]] + STRING_VALUE translate(const STRING_ID& str_id, const STRING_ID& str_id2) const; // returns str_id if no translation found + bool translate(const STRING_ID& str_id, STRING_VALUE& out) const; - pcstr translate(const STRING_ID& str_id, pcstr default_value) const; + + [[nodiscard]] + bool has_translation(const STRING_ID& str_id) const; + void rescan(); void ReloadLanguage(); diff --git a/src/xrGame/ui/UIActorMenuInventory.cpp b/src/xrGame/ui/UIActorMenuInventory.cpp index 30d76be535c..e059e90a343 100644 --- a/src/xrGame/ui/UIActorMenuInventory.cpp +++ b/src/xrGame/ui/UIActorMenuInventory.cpp @@ -1009,7 +1009,7 @@ void CUIActorMenu::PropertiesBoxForSlots(PIItem item, bool& b_show) { if (!pHelmet) { - const bool has_translation = StringTable().translate("st_unequip", nullptr); + const bool has_translation = StringTable().has_translation("st_unequip"); if (m_currMenuMode == mmDeadBodySearch || !has_translation) m_UIPropertiesBox->AddItem("st_move_to_bag", nullptr, INVENTORY_TO_BAG_ACTION); else diff --git a/src/xrGame/ui/UIItemInfo.cpp b/src/xrGame/ui/UIItemInfo.cpp index 3834ce3f1ad..9f7bd546be1 100644 --- a/src/xrGame/ui/UIItemInfo.cpp +++ b/src/xrGame/ui/UIItemInfo.cpp @@ -186,9 +186,11 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, } if (UIWeight) { - LPCSTR kg_str = StringTable().translate("st_kg").c_str(); float weight = pInvItem->Weight(); + shared_str kg_str = "kg"; + StringTable().translate("st_kg", kg_str); + if (!weight) { if (/*CWeaponAmmo* ammo =*/ smart_cast(pInvItem)) @@ -203,7 +205,7 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, } } - xr_sprintf(str, "%3.2f %s", weight, kg_str); + xr_sprintf(str, "%3.2f %s", weight, kg_str.c_str()); UIWeight->SetText(str); pos.x = UIWeight->GetWndPos().x; From 32729882d90aea8b8ac3789d7c9a4dd3826d35e3 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 10:51:59 +0300 Subject: [PATCH 082/261] Provide better overloads of CStringTable::translate --- src/xrEngine/StringTable/StringTable.cpp | 17 ++++++++++++----- src/xrEngine/StringTable/StringTable.h | 10 +++++++++- src/xrGame/ui/UIActorMenuInventory.cpp | 2 +- src/xrGame/ui/UIItemInfo.cpp | 6 ++++-- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/xrEngine/StringTable/StringTable.cpp b/src/xrEngine/StringTable/StringTable.cpp index 6f5466777ef..963d897e616 100644 --- a/src/xrEngine/StringTable/StringTable.cpp +++ b/src/xrEngine/StringTable/StringTable.cpp @@ -307,6 +307,14 @@ STRING_VALUE CStringTable::translate(const STRING_ID& str_id) const return str_id; } +STRING_VALUE CStringTable::translate(const STRING_ID& str_id, const STRING_ID& str_id2) const +{ + STRING_VALUE out = str_id; + if (!translate(str_id, out)) + translate(str_id2, out); + return out; +} + bool CStringTable::translate(const STRING_ID& str_id, STRING_VALUE& out) const { if (!pData) @@ -320,11 +328,10 @@ bool CStringTable::translate(const STRING_ID& str_id, STRING_VALUE& out) const return false; } -pcstr CStringTable::translate(const STRING_ID& str_id, pcstr default_value) const +bool CStringTable::has_translation(const STRING_ID& str_id) const { - STRING_VALUE out; - if (translate(str_id, out)) - return out.c_str(); + if (!pData) + return false; - return default_value; + return pData->m_StringTable.find(str_id) != pData->m_StringTable.end(); } diff --git a/src/xrEngine/StringTable/StringTable.h b/src/xrEngine/StringTable/StringTable.h index 5eda57009a7..d096711e4cd 100644 --- a/src/xrEngine/StringTable/StringTable.h +++ b/src/xrEngine/StringTable/StringTable.h @@ -29,9 +29,17 @@ class ENGINE_API CStringTable final void Init(); static void Destroy(); + [[nodiscard]] STRING_VALUE translate(const STRING_ID& str_id) const; + + [[nodiscard]] + STRING_VALUE translate(const STRING_ID& str_id, const STRING_ID& str_id2) const; // returns str_id if no translation found + bool translate(const STRING_ID& str_id, STRING_VALUE& out) const; - pcstr translate(const STRING_ID& str_id, pcstr default_value) const; + + [[nodiscard]] + bool has_translation(const STRING_ID& str_id) const; + void rescan(); void ReloadLanguage(); diff --git a/src/xrGame/ui/UIActorMenuInventory.cpp b/src/xrGame/ui/UIActorMenuInventory.cpp index 30d76be535c..e059e90a343 100644 --- a/src/xrGame/ui/UIActorMenuInventory.cpp +++ b/src/xrGame/ui/UIActorMenuInventory.cpp @@ -1009,7 +1009,7 @@ void CUIActorMenu::PropertiesBoxForSlots(PIItem item, bool& b_show) { if (!pHelmet) { - const bool has_translation = StringTable().translate("st_unequip", nullptr); + const bool has_translation = StringTable().has_translation("st_unequip"); if (m_currMenuMode == mmDeadBodySearch || !has_translation) m_UIPropertiesBox->AddItem("st_move_to_bag", nullptr, INVENTORY_TO_BAG_ACTION); else diff --git a/src/xrGame/ui/UIItemInfo.cpp b/src/xrGame/ui/UIItemInfo.cpp index 3834ce3f1ad..9f7bd546be1 100644 --- a/src/xrGame/ui/UIItemInfo.cpp +++ b/src/xrGame/ui/UIItemInfo.cpp @@ -186,9 +186,11 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, } if (UIWeight) { - LPCSTR kg_str = StringTable().translate("st_kg").c_str(); float weight = pInvItem->Weight(); + shared_str kg_str = "kg"; + StringTable().translate("st_kg", kg_str); + if (!weight) { if (/*CWeaponAmmo* ammo =*/ smart_cast(pInvItem)) @@ -203,7 +205,7 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, } } - xr_sprintf(str, "%3.2f %s", weight, kg_str); + xr_sprintf(str, "%3.2f %s", weight, kg_str.c_str()); UIWeight->SetText(str); pos.x = UIWeight->GetWndPos().x; From 556c531bfe1813b358c0fd21901447dc73518199 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 12:29:46 +0300 Subject: [PATCH 083/261] xrUICore/XML/UIXmlInitBase.h|cpp: ability to get registered color by name --- src/xrUICore/XML/UIXmlInitBase.cpp | 10 ++++++++++ src/xrUICore/XML/UIXmlInitBase.h | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 78de52fb2da..adaa7c6f32f 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -1356,3 +1356,13 @@ u32 CUIXmlInitBase::GetColor(const CUIXml& xml_doc, pcstr path, int index, u32 d return color_argb(a, r, g, b); } } + +bool CUIXmlInitBase::GetColor(pcstr color_name, u32& color) +{ + const auto it = GetColorDefs()->find(color_name); + if (it == GetColorDefs()->end()) + return false; + + color = it->second; + return true; +} diff --git a/src/xrUICore/XML/UIXmlInitBase.h b/src/xrUICore/XML/UIXmlInitBase.h index ffb1bd062f4..cffb5e43122 100644 --- a/src/xrUICore/XML/UIXmlInitBase.h +++ b/src/xrUICore/XML/UIXmlInitBase.h @@ -67,7 +67,9 @@ class XRUICORE_API CUIXmlInitBase static bool InitComboBox(CUIXml& xml_doc, pcstr path, int index, CUIComboBox* pWnd); static bool InitTrackBar(CUIXml& xml_doc, pcstr path, int index, CUITrackBar* pWnd, bool fatal = true); static Frect GetFRect(const CUIXml& xml_doc, pcstr path, int index); + static u32 GetColor(const CUIXml& xml_doc, pcstr path, int index, u32 def_clr); + static bool GetColor(pcstr color_name, u32& color); static bool InitAlignment(const CUIXml& xml_doc, const char* path, int index, float& x, float& y, CUIWindow* pWnd); From f5048dd28398388a5a4a2147220979a1bf942435 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 12:29:46 +0300 Subject: [PATCH 084/261] xrUICore/XML/UIXmlInitBase.h|cpp: ability to get registered color by name --- src/xrUICore/XML/UIXmlInitBase.cpp | 10 ++++++++++ src/xrUICore/XML/UIXmlInitBase.h | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 78de52fb2da..adaa7c6f32f 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -1356,3 +1356,13 @@ u32 CUIXmlInitBase::GetColor(const CUIXml& xml_doc, pcstr path, int index, u32 d return color_argb(a, r, g, b); } } + +bool CUIXmlInitBase::GetColor(pcstr color_name, u32& color) +{ + const auto it = GetColorDefs()->find(color_name); + if (it == GetColorDefs()->end()) + return false; + + color = it->second; + return true; +} diff --git a/src/xrUICore/XML/UIXmlInitBase.h b/src/xrUICore/XML/UIXmlInitBase.h index ffb1bd062f4..cffb5e43122 100644 --- a/src/xrUICore/XML/UIXmlInitBase.h +++ b/src/xrUICore/XML/UIXmlInitBase.h @@ -67,7 +67,9 @@ class XRUICORE_API CUIXmlInitBase static bool InitComboBox(CUIXml& xml_doc, pcstr path, int index, CUIComboBox* pWnd); static bool InitTrackBar(CUIXml& xml_doc, pcstr path, int index, CUITrackBar* pWnd, bool fatal = true); static Frect GetFRect(const CUIXml& xml_doc, pcstr path, int index); + static u32 GetColor(const CUIXml& xml_doc, pcstr path, int index, u32 def_clr); + static bool GetColor(pcstr color_name, u32& color); static bool InitAlignment(const CUIXml& xml_doc, const char* path, int index, float& x, float& y, CUIWindow* pWnd); From 2a273dd3e6d9e727e53aeb279446c67d80ba47a1 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 12:47:05 +0300 Subject: [PATCH 085/261] CUIXmlInitBase::GetColor can now accept full RGBA colors as default Warning: behaviour change! --- src/xrGame/ui/UICharacterInfo.cpp | 6 +-- src/xrGame/ui/UIInventoryUpgradeWnd_add.cpp | 2 +- src/xrGame/ui/UIMapInfo.cpp | 2 +- src/xrGame/ui/UIRankingWnd.cpp | 2 +- src/xrUICore/XML/UIXmlInitBase.cpp | 49 ++++++++++----------- 5 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/xrGame/ui/UICharacterInfo.cpp b/src/xrGame/ui/UICharacterInfo.cpp index 5c242e8e1a8..7e36c5f2536 100644 --- a/src/xrGame/ui/UICharacterInfo.cpp +++ b/src/xrGame/ui/UICharacterInfo.cpp @@ -46,11 +46,7 @@ void CUICharacterInfo::InitCharacterInfo(Fvector2 pos, Fvector2 size, CUIXml* xm m_original_color = m_icons[eIcon] ? m_icons[eIcon]->GetTextureColor() : color_xrgb(255, 255, 255); - m_deadbody_color = color_argb(160, 160, 160, 160); - if (xml_doc->NavigateToNode("icon:deadbody", 0)) - { - m_deadbody_color = CUIXmlInit::GetColor(*xml_doc, "icon:deadbody", 0, m_deadbody_color); - } + m_deadbody_color = CUIXmlInit::GetColor(*xml_doc, "icon:deadbody", 0, color_argb(160, 160, 160, 160)); Init_IconInfoItem(*xml_doc, "icon_over", eIconOver); diff --git a/src/xrGame/ui/UIInventoryUpgradeWnd_add.cpp b/src/xrGame/ui/UIInventoryUpgradeWnd_add.cpp index da1a54c8c8d..d7947752a16 100644 --- a/src/xrGame/ui/UIInventoryUpgradeWnd_add.cpp +++ b/src/xrGame/ui/UIInventoryUpgradeWnd_add.cpp @@ -27,7 +27,7 @@ void CUIInventoryUpgradeWnd::LoadCellsBacks(CUIXml& uiXml) LPCSTR type = uiXml.Read("type", 0, ""); LPCSTR txr = uiXml.Read("back_texture", 0, NULL); LPCSTR txr2 = uiXml.Read("point_texture", 0, NULL); - u32 color = CUIXmlInit::GetColor(uiXml, "item_color", 0, 0); + u32 color = CUIXmlInit::GetColor(uiXml, "item_color", 0, color_rgba(0, 0, 0, 255)); LoadCellStates(type, txr, txr2, color); uiXml.SetLocalRoot(node); diff --git a/src/xrGame/ui/UIMapInfo.cpp b/src/xrGame/ui/UIMapInfo.cpp index efd43d0c094..981659a3612 100644 --- a/src/xrGame/ui/UIMapInfo.cpp +++ b/src/xrGame/ui/UIMapInfo.cpp @@ -81,7 +81,7 @@ void CUIMapInfo::InitMap(LPCSTR map_name, LPCSTR map_ver) u32 txt_color; CGameFont* txt_font; CUIXmlInit::InitFont(xml_doc, "header", 0, header_color, txt_font); - txt_color = CUIXmlInit::GetColor(xml_doc, "txt:text", 0, 0x00); + txt_color = CUIXmlInit::GetColor(xml_doc, "txt:text", 0, color_rgba(0, 0, 0, 255)); string64 txt_color_tag; xr_sprintf(txt_color_tag, "%s[%u,%u,%u,%u]", "%c", color_get_A(txt_color), color_get_R(txt_color), color_get_G(txt_color), color_get_B(txt_color)); diff --git a/src/xrGame/ui/UIRankingWnd.cpp b/src/xrGame/ui/UIRankingWnd.cpp index 0ed64c40a7d..b695454a49d 100644 --- a/src/xrGame/ui/UIRankingWnd.cpp +++ b/src/xrGame/ui/UIRankingWnd.cpp @@ -143,7 +143,7 @@ bool CUIRankingWnd::Init() xml.SetLocalRoot(node); auto stat_count = xml.GetNodesNum(node, "stat"); - const u32 value_color = CUIXmlInit::GetColor(xml, "value", 0, 0xFFffffff); + const u32 value_color = CUIXmlInit::GetColor(xml, "value", 0, 0xFFFFFFFF); for (size_t i = 0; i < stat_count; ++i) { diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index adaa7c6f32f..065162a8c2c 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -223,28 +223,28 @@ bool CUIXmlInitBase::InitCheck(CUIXml& xml_doc, pcstr path, int index, CUICheckB strconcat(buf, path, ":text_color:e"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Enabled); } strconcat(buf, path, ":text_color:d"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Disabled); } strconcat(buf, path, ":text_color:t"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Touched); } strconcat(buf, path, ":text_color:h"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Highlighted); } @@ -266,13 +266,13 @@ bool CUIXmlInitBase::InitSpin(CUIXml& xml_doc, pcstr path, int index, CUICustomS strconcat(foo, path, ":text_color:e"); if (xml_doc.NavigateToNode(foo, index)) { - color = GetColor(xml_doc, foo, index, 0x00); + color = GetColor(xml_doc, foo, index, color_rgba(0, 0, 0, 255)); pWnd->SetTextColor(color); } strconcat(foo, path, ":text_color:d"); if (xml_doc.NavigateToNode(foo, index)) { - color = GetColor(xml_doc, foo, index, 0x00); + color = GetColor(xml_doc, foo, index, color_rgba(0, 0, 0, 255)); pWnd->SetTextColorD(color); } @@ -354,28 +354,28 @@ bool CUIXmlInitBase::Init3tButton(CUIXml& xml_doc, pcstr path, int index, CUI3tB strconcat(buf, path, ":text_color:e"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Enabled); } strconcat(buf, path, ":text_color:d"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Disabled); } strconcat(buf, path, ":text_color:t"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Touched); } strconcat(buf, path, ":text_color:h"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Highlighted); } @@ -521,17 +521,17 @@ bool CUIXmlInitBase::InitProgressBar(CUIXml& xml_doc, pcstr path, int index, CUI { pWnd->m_bUseColor = true; - pWnd->m_minColor = GetColor(xml_doc, buf, index, 0xff); + pWnd->m_minColor = GetColor(xml_doc, buf, index, 0xFFFFFFFF); strconcat(buf, path, ":middle_color"); if (xml_doc.NavigateToNode(buf, 0)) { - pWnd->m_middleColor = GetColor(xml_doc, buf, index, 0xff); + pWnd->m_middleColor = GetColor(xml_doc, buf, index, 0xFFFFFFFF); pWnd->m_bUseMiddleColor = true; } strconcat(buf, path, ":max_color"); - pWnd->m_maxColor = GetColor(xml_doc, buf, index, 0xff); + pWnd->m_maxColor = GetColor(xml_doc, buf, index, 0xFFFFFFFF); } return true; @@ -670,7 +670,7 @@ void CUIXmlInitBase::InitAutoFrameLineGroup(CUIXml& xml_doc, pcstr path, int ind bool CUIXmlInitBase::InitFont(const CUIXml& xml_doc, pcstr path, int index, u32& color, CGameFont*& pFnt) { - color = GetColor(xml_doc, path, index, 0xff); + color = GetColor(xml_doc, path, index, 0xFFFFFFFF); cpcstr font_name = xml_doc.ReadAttrib(path, index, "font", nullptr); if (!font_name) @@ -830,7 +830,7 @@ bool CUIXmlInitBase::InitCustomEdit(CUIXml& xml_doc, pcstr path, int index, CUIC strconcat(foo, path, ":text_color:e"); if (xml_doc.NavigateToNode(foo, index)) { - const u32 color = GetColor(xml_doc, foo, index, 0x00); + const u32 color = GetColor(xml_doc, foo, index, color_rgba(0, 0, 0, 255)); pWnd->TextItemControl()->SetTextColor(color); } @@ -926,7 +926,7 @@ bool CUIXmlInitBase::InitTexture(const CUIXml& xml_doc, pcstr path, int index, I const bool stretch_flag = xml_doc.ReadAttribInt(path, index, "stretch") ? true : false; pWnd->SetStretchTexture(stretch_flag); - const u32 color = GetColor(xml_doc, buf, index, 0xff); + const u32 color = GetColor(xml_doc, buf, index, 0xFFFFFFFF); pWnd->SetTextureColor(color); if (!fis_zero(rect.width()) && !fis_zero(rect.height())) @@ -1325,14 +1325,14 @@ bool CUIXmlInitBase::InitComboBox(CUIXml& xml_doc, pcstr path, int index, CUICom strconcat(_path, path, ":text_color:e"); if (xml_doc.NavigateToNode(_path, index)) { - color = GetColor(xml_doc, _path, index, 0x00); + color = GetColor(xml_doc, _path, index, color_rgba(0, 0, 0, 255)); pWnd->SetTextColor(color); } strconcat(_path, path, ":text_color:d"); if (xml_doc.NavigateToNode(_path, index)) { - color = GetColor(xml_doc, _path, index, 0x00); + color = GetColor(xml_doc, _path, index, color_rgba(0, 0, 0, 255)); pWnd->SetTextColorD(color); } @@ -1347,14 +1347,11 @@ u32 CUIXmlInitBase::GetColor(const CUIXml& xml_doc, pcstr path, int index, u32 d VERIFY(GetColorDefs()->find(clr_def) != GetColorDefs()->end()); return (*m_pColorDefs)[clr_def]; } - else - { - const int r = xml_doc.ReadAttribInt(path, index, "r", def_clr); - const int g = xml_doc.ReadAttribInt(path, index, "g", def_clr); - const int b = xml_doc.ReadAttribInt(path, index, "b", def_clr); - const int a = xml_doc.ReadAttribInt(path, index, "a", 0xff); - return color_argb(a, r, g, b); - } + const int r = xml_doc.ReadAttribInt(path, index, "r", color_get_R(def_clr)); + const int g = xml_doc.ReadAttribInt(path, index, "g", color_get_G(def_clr)); + const int b = xml_doc.ReadAttribInt(path, index, "b", color_get_B(def_clr)); + const int a = xml_doc.ReadAttribInt(path, index, "a", color_get_A(def_clr)); + return color_argb(a, r, g, b); } bool CUIXmlInitBase::GetColor(pcstr color_name, u32& color) From c628054e0116b5f9eec479c32fbc35aa393b64d8 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 12:47:05 +0300 Subject: [PATCH 086/261] CUIXmlInitBase::GetColor can now accept full RGBA colors as default Warning: behaviour change! --- src/xrGame/ui/UICharacterInfo.cpp | 6 +-- src/xrGame/ui/UIInventoryUpgradeWnd_add.cpp | 2 +- src/xrGame/ui/UIMapInfo.cpp | 2 +- src/xrGame/ui/UIRankingWnd.cpp | 2 +- src/xrUICore/XML/UIXmlInitBase.cpp | 49 ++++++++++----------- 5 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/xrGame/ui/UICharacterInfo.cpp b/src/xrGame/ui/UICharacterInfo.cpp index 5c242e8e1a8..7e36c5f2536 100644 --- a/src/xrGame/ui/UICharacterInfo.cpp +++ b/src/xrGame/ui/UICharacterInfo.cpp @@ -46,11 +46,7 @@ void CUICharacterInfo::InitCharacterInfo(Fvector2 pos, Fvector2 size, CUIXml* xm m_original_color = m_icons[eIcon] ? m_icons[eIcon]->GetTextureColor() : color_xrgb(255, 255, 255); - m_deadbody_color = color_argb(160, 160, 160, 160); - if (xml_doc->NavigateToNode("icon:deadbody", 0)) - { - m_deadbody_color = CUIXmlInit::GetColor(*xml_doc, "icon:deadbody", 0, m_deadbody_color); - } + m_deadbody_color = CUIXmlInit::GetColor(*xml_doc, "icon:deadbody", 0, color_argb(160, 160, 160, 160)); Init_IconInfoItem(*xml_doc, "icon_over", eIconOver); diff --git a/src/xrGame/ui/UIInventoryUpgradeWnd_add.cpp b/src/xrGame/ui/UIInventoryUpgradeWnd_add.cpp index da1a54c8c8d..d7947752a16 100644 --- a/src/xrGame/ui/UIInventoryUpgradeWnd_add.cpp +++ b/src/xrGame/ui/UIInventoryUpgradeWnd_add.cpp @@ -27,7 +27,7 @@ void CUIInventoryUpgradeWnd::LoadCellsBacks(CUIXml& uiXml) LPCSTR type = uiXml.Read("type", 0, ""); LPCSTR txr = uiXml.Read("back_texture", 0, NULL); LPCSTR txr2 = uiXml.Read("point_texture", 0, NULL); - u32 color = CUIXmlInit::GetColor(uiXml, "item_color", 0, 0); + u32 color = CUIXmlInit::GetColor(uiXml, "item_color", 0, color_rgba(0, 0, 0, 255)); LoadCellStates(type, txr, txr2, color); uiXml.SetLocalRoot(node); diff --git a/src/xrGame/ui/UIMapInfo.cpp b/src/xrGame/ui/UIMapInfo.cpp index efd43d0c094..981659a3612 100644 --- a/src/xrGame/ui/UIMapInfo.cpp +++ b/src/xrGame/ui/UIMapInfo.cpp @@ -81,7 +81,7 @@ void CUIMapInfo::InitMap(LPCSTR map_name, LPCSTR map_ver) u32 txt_color; CGameFont* txt_font; CUIXmlInit::InitFont(xml_doc, "header", 0, header_color, txt_font); - txt_color = CUIXmlInit::GetColor(xml_doc, "txt:text", 0, 0x00); + txt_color = CUIXmlInit::GetColor(xml_doc, "txt:text", 0, color_rgba(0, 0, 0, 255)); string64 txt_color_tag; xr_sprintf(txt_color_tag, "%s[%u,%u,%u,%u]", "%c", color_get_A(txt_color), color_get_R(txt_color), color_get_G(txt_color), color_get_B(txt_color)); diff --git a/src/xrGame/ui/UIRankingWnd.cpp b/src/xrGame/ui/UIRankingWnd.cpp index 0ed64c40a7d..b695454a49d 100644 --- a/src/xrGame/ui/UIRankingWnd.cpp +++ b/src/xrGame/ui/UIRankingWnd.cpp @@ -143,7 +143,7 @@ bool CUIRankingWnd::Init() xml.SetLocalRoot(node); auto stat_count = xml.GetNodesNum(node, "stat"); - const u32 value_color = CUIXmlInit::GetColor(xml, "value", 0, 0xFFffffff); + const u32 value_color = CUIXmlInit::GetColor(xml, "value", 0, 0xFFFFFFFF); for (size_t i = 0; i < stat_count; ++i) { diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index adaa7c6f32f..065162a8c2c 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -223,28 +223,28 @@ bool CUIXmlInitBase::InitCheck(CUIXml& xml_doc, pcstr path, int index, CUICheckB strconcat(buf, path, ":text_color:e"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Enabled); } strconcat(buf, path, ":text_color:d"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Disabled); } strconcat(buf, path, ":text_color:t"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Touched); } strconcat(buf, path, ":text_color:h"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Highlighted); } @@ -266,13 +266,13 @@ bool CUIXmlInitBase::InitSpin(CUIXml& xml_doc, pcstr path, int index, CUICustomS strconcat(foo, path, ":text_color:e"); if (xml_doc.NavigateToNode(foo, index)) { - color = GetColor(xml_doc, foo, index, 0x00); + color = GetColor(xml_doc, foo, index, color_rgba(0, 0, 0, 255)); pWnd->SetTextColor(color); } strconcat(foo, path, ":text_color:d"); if (xml_doc.NavigateToNode(foo, index)) { - color = GetColor(xml_doc, foo, index, 0x00); + color = GetColor(xml_doc, foo, index, color_rgba(0, 0, 0, 255)); pWnd->SetTextColorD(color); } @@ -354,28 +354,28 @@ bool CUIXmlInitBase::Init3tButton(CUIXml& xml_doc, pcstr path, int index, CUI3tB strconcat(buf, path, ":text_color:e"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Enabled); } strconcat(buf, path, ":text_color:d"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Disabled); } strconcat(buf, path, ":text_color:t"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Touched); } strconcat(buf, path, ":text_color:h"); if (xml_doc.NavigateToNode(buf, index)) { - color = GetColor(xml_doc, buf, index, 0x00); + color = GetColor(xml_doc, buf, index, color_rgba(0, 0, 0, 255)); pWnd->SetStateTextColor(color, S_Highlighted); } @@ -521,17 +521,17 @@ bool CUIXmlInitBase::InitProgressBar(CUIXml& xml_doc, pcstr path, int index, CUI { pWnd->m_bUseColor = true; - pWnd->m_minColor = GetColor(xml_doc, buf, index, 0xff); + pWnd->m_minColor = GetColor(xml_doc, buf, index, 0xFFFFFFFF); strconcat(buf, path, ":middle_color"); if (xml_doc.NavigateToNode(buf, 0)) { - pWnd->m_middleColor = GetColor(xml_doc, buf, index, 0xff); + pWnd->m_middleColor = GetColor(xml_doc, buf, index, 0xFFFFFFFF); pWnd->m_bUseMiddleColor = true; } strconcat(buf, path, ":max_color"); - pWnd->m_maxColor = GetColor(xml_doc, buf, index, 0xff); + pWnd->m_maxColor = GetColor(xml_doc, buf, index, 0xFFFFFFFF); } return true; @@ -670,7 +670,7 @@ void CUIXmlInitBase::InitAutoFrameLineGroup(CUIXml& xml_doc, pcstr path, int ind bool CUIXmlInitBase::InitFont(const CUIXml& xml_doc, pcstr path, int index, u32& color, CGameFont*& pFnt) { - color = GetColor(xml_doc, path, index, 0xff); + color = GetColor(xml_doc, path, index, 0xFFFFFFFF); cpcstr font_name = xml_doc.ReadAttrib(path, index, "font", nullptr); if (!font_name) @@ -830,7 +830,7 @@ bool CUIXmlInitBase::InitCustomEdit(CUIXml& xml_doc, pcstr path, int index, CUIC strconcat(foo, path, ":text_color:e"); if (xml_doc.NavigateToNode(foo, index)) { - const u32 color = GetColor(xml_doc, foo, index, 0x00); + const u32 color = GetColor(xml_doc, foo, index, color_rgba(0, 0, 0, 255)); pWnd->TextItemControl()->SetTextColor(color); } @@ -926,7 +926,7 @@ bool CUIXmlInitBase::InitTexture(const CUIXml& xml_doc, pcstr path, int index, I const bool stretch_flag = xml_doc.ReadAttribInt(path, index, "stretch") ? true : false; pWnd->SetStretchTexture(stretch_flag); - const u32 color = GetColor(xml_doc, buf, index, 0xff); + const u32 color = GetColor(xml_doc, buf, index, 0xFFFFFFFF); pWnd->SetTextureColor(color); if (!fis_zero(rect.width()) && !fis_zero(rect.height())) @@ -1325,14 +1325,14 @@ bool CUIXmlInitBase::InitComboBox(CUIXml& xml_doc, pcstr path, int index, CUICom strconcat(_path, path, ":text_color:e"); if (xml_doc.NavigateToNode(_path, index)) { - color = GetColor(xml_doc, _path, index, 0x00); + color = GetColor(xml_doc, _path, index, color_rgba(0, 0, 0, 255)); pWnd->SetTextColor(color); } strconcat(_path, path, ":text_color:d"); if (xml_doc.NavigateToNode(_path, index)) { - color = GetColor(xml_doc, _path, index, 0x00); + color = GetColor(xml_doc, _path, index, color_rgba(0, 0, 0, 255)); pWnd->SetTextColorD(color); } @@ -1347,14 +1347,11 @@ u32 CUIXmlInitBase::GetColor(const CUIXml& xml_doc, pcstr path, int index, u32 d VERIFY(GetColorDefs()->find(clr_def) != GetColorDefs()->end()); return (*m_pColorDefs)[clr_def]; } - else - { - const int r = xml_doc.ReadAttribInt(path, index, "r", def_clr); - const int g = xml_doc.ReadAttribInt(path, index, "g", def_clr); - const int b = xml_doc.ReadAttribInt(path, index, "b", def_clr); - const int a = xml_doc.ReadAttribInt(path, index, "a", 0xff); - return color_argb(a, r, g, b); - } + const int r = xml_doc.ReadAttribInt(path, index, "r", color_get_R(def_clr)); + const int g = xml_doc.ReadAttribInt(path, index, "g", color_get_G(def_clr)); + const int b = xml_doc.ReadAttribInt(path, index, "b", color_get_B(def_clr)); + const int a = xml_doc.ReadAttribInt(path, index, "a", color_get_A(def_clr)); + return color_argb(a, r, g, b); } bool CUIXmlInitBase::GetColor(pcstr color_name, u32& color) From daef8331f9d7e175858ad479fff06a12dff1a532 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 14:05:49 +0300 Subject: [PATCH 087/261] Make CUIArtefactParams work in SOC (#392) Added Clear Sky color too (#382) Now works with all three games. --- src/xrGame/ui/ui_af_params.cpp | 294 ++++++++++++++++----------------- src/xrGame/ui/ui_af_params.h | 45 ++--- 2 files changed, 161 insertions(+), 178 deletions(-) diff --git a/src/xrGame/ui/ui_af_params.cpp b/src/xrGame/ui/ui_af_params.cpp index 231db108de8..27b9bb1ea8f 100644 --- a/src/xrGame/ui/ui_af_params.cpp +++ b/src/xrGame/ui/ui_af_params.cpp @@ -8,8 +8,15 @@ #include "UIXmlInit.h" #include "UIHelper.h" -u32 const red_clr = color_argb(255, 210, 50, 50); -u32 const green_clr = color_argb(255, 170, 170, 170); +constexpr pcstr af_params = "af_params"; + +constexpr u32 green_clr_cop = color_argb(255, 170, 170, 170); +constexpr u32 red_clr_cop = color_argb(255, 210, 50, 50); + +constexpr u32 green_clr_cs = color_argb(250, 50, 210, 50); +constexpr u32 red_clr_cs = color_argb(250, 210, 50, 50); + +CUIArtefactParams::CUIArtefactParams() : CUIWindow(af_params) {} CUIArtefactParams::~CUIArtefactParams() { @@ -21,114 +28,89 @@ CUIArtefactParams::~CUIArtefactParams() constexpr std::tuple af_immunity[] = { - //{ ALife::infl_, "section", "caption", magnitude, sign_inverse, "unit" } - { ALife::infl_rad, "radiation_immunity", "ui_inv_outfit_radiation_protection", 1.0f, false, "%" }, - { ALife::infl_fire, "burn_immunity", "ui_inv_outfit_burn_protection", 1.0f, false, "%" }, - { ALife::infl_acid, "chemical_burn_immunity", "ui_inv_outfit_chemical_burn_protection", 1.0f, false, "%" }, - { ALife::infl_psi, "telepatic_immunity", "ui_inv_outfit_telepatic_protection", 1.0f, false, "%" }, - { ALife::infl_electra, "shock_immunity", "ui_inv_outfit_shock_protection", 1.0f, false, "%" }, - { 5, "strike_immunity", "ui_inv_outfit_strike_protection", 1.0f, false, "%" }, - { 6, "wound_immunity", "ui_inv_outfit_wound_protection", 1.0f, false, "%" }, - { 7, "explosion_immunity", "ui_inv_outfit_explosion_protection", 1.0f, false, "%" }, - { 8, "fire_wound_immunity", "ui_inv_outfit_fire_wound_protection", 1.0f, false, "%" }, + //{ ALife::infl_, "section", "caption", magnitude, sign_inverse, "unit" } + { ALife::infl_rad, "radiation_immunity", "ui_inv_outfit_radiation_protection", 100.0f, false, "%" }, + { ALife::infl_fire, "burn_immunity", "ui_inv_outfit_burn_protection", 100.0f, false, "%" }, + { ALife::infl_acid, "chemical_burn_immunity", "ui_inv_outfit_chemical_burn_protection", 100.0f, false, "%" }, + { ALife::infl_psi, "telepatic_immunity", "ui_inv_outfit_telepatic_protection", 100.0f, false, "%" }, + { ALife::infl_electra, "shock_immunity", "ui_inv_outfit_shock_protection", 100.0f, false, "%" }, + { 5, "strike_immunity", "ui_inv_outfit_strike_protection", 100.0f, false, "%" }, + { 6, "wound_immunity", "ui_inv_outfit_wound_protection", 100.0f, false, "%" }, + { 7, "explosion_immunity", "ui_inv_outfit_explosion_protection", 100.0f, false, "%" }, + { 8, "fire_wound_immunity", "ui_inv_outfit_fire_wound_protection", 100.0f, false, "%" }, }; -constexpr std::tuple af_restore[] = +constexpr std::tuple af_restore[] = { - //{ ALife::EConditionRestoreType, "section", "caption", magnitude, sign_inverse, "unit" } - { ALife::eHealthRestoreSpeed, "health_restore_speed", "ui_inv_health", 1.0f, false, "%" }, - { ALife::eSatietyRestoreSpeed, "satiety_restore_speed", "ui_inv_satiety", 1.0f, false, "%" }, - { ALife::ePowerRestoreSpeed, "power_restore_speed", "ui_inv_power", 1.0f, false, nullptr }, - { ALife::eBleedingRestoreSpeed, "bleeding_restore_speed", "ui_inv_bleeding", -1.0f, true, "%" }, - { ALife::eRadiationRestoreSpeed, "radiation_restore_speed", "ui_inv_radiation", 1.0f, true, nullptr }, + //{ ALife::EConditionRestoreType, "section", "actor_condition", "caption", magnitude, sign_inverse, "unit" } + { ALife::eHealthRestoreSpeed, "health_restore_speed", "satiety_health_v", "ui_inv_health", 100.0f, false, "%" }, + { ALife::eSatietyRestoreSpeed, "satiety_restore_speed", "satiety_v", "ui_inv_satiety", 100.0f, false, "%" }, + { ALife::ePowerRestoreSpeed, "power_restore_speed", "satiety_power_v", "ui_inv_power", 1.0f, false, nullptr }, + { ALife::eBleedingRestoreSpeed, "bleeding_restore_speed", "wound_incarnation_v", "ui_inv_bleeding", -100.0f, true, "%" }, + { ALife::eRadiationRestoreSpeed, "radiation_restore_speed", "radiation_v", "ui_inv_radiation", 1.0f, true, nullptr }, }; static_assert(std::size(af_restore) == ALife::eRestoreTypeMax, "All restore types should be listed in the tuple above."); -/* -LPCSTR af_actor_param_names[]= -{ - "satiety_health_v", - "radiation_v", - "satiety_v", - "satiety_power_v", - "wound_incarnation_v", -}; -*/ - -constexpr pcstr af_params = "af_params"; - bool CUIArtefactParams::InitFromXml(CUIXml& xml) { XML_NODE stored_root = xml.GetLocalRoot(); XML_NODE base_node = xml.NavigateToNode(af_params, 0); if (!base_node) - { return false; - } + CUIXmlInit::InitWindow(xml, af_params, 0, this); xml.SetLocalRoot(base_node); if ((m_Prop_line = UIHelper::CreateStatic(xml, "prop_line", this, false))) m_Prop_line->SetAutoDelete(false); + const u32 positive_color = m_Prop_line ? green_clr_cop : green_clr_cs; + const u32 negative_color = m_Prop_line ? red_clr_cop : red_clr_cs; + + const auto create_item = [&](pcstr section, pcstr caption, + float magnitude = 1.0f, bool is_sign_inverse = false, pcstr unit = "") + { + UIArtefactParamItem* item = xr_new(section); + + string256 buf; + strconcat(buf, "static_", section); + + if (item->Init(xml, section, caption, positive_color, negative_color, magnitude, is_sign_inverse, unit) || + item->Init(xml, buf, caption, positive_color, negative_color, magnitude, is_sign_inverse, unit)) + { + item->SetAutoDelete(false); + return item; + } + xr_delete(item); + return item; + }; + //Alundaio: Show AF Condition - m_disp_condition = CreateItem(xml, "condition", "ui_inv_af_condition"); + m_disp_condition = create_item("condition", "ui_inv_af_condition"); //-Alundaio - for (auto [id, section, caption, magnitude, sign_inverse, unit] : af_restore) + for (auto [id, section, actor_condition, caption_id, magnitude, sign_inverse, unit] : af_restore) { - m_restore_item[id] = CreateItem(xml, section, magnitude, sign_inverse, unit, caption); + const auto caption = StringTable().translate(caption_id).c_str(); + m_restore_item[id] = create_item(section, caption, magnitude, sign_inverse, unit); } - for (auto [id, section, caption, magnitude, sign_inverse, unit] : af_immunity) + for (auto [id, section, caption_id, magnitude, sign_inverse, unit] : af_immunity) { - m_immunity_item[id] = CreateItem(xml, section, magnitude, sign_inverse, unit, caption); + const auto caption = StringTable().translate(caption_id).c_str(); + m_immunity_item[id] = create_item(section, caption, magnitude, sign_inverse, unit); } - m_additional_weight = CreateItem(xml, "additional_weight", "ui_inv_weight", "ui_inv_outfit_additional_weight"); + + const auto weight = StringTable().translate("ui_inv_weight", "ui_inv_outfit_additional_weight"); + m_additional_weight = create_item("additional_weight", weight.c_str()); xml.SetLocalRoot(stored_root); return true; } -UIArtefactParamItem* CUIArtefactParams::CreateItem(CUIXml& uiXml, pcstr section, - float magnitude, bool isSignInverse, const shared_str& unit, - const shared_str& translationId, const shared_str& translationId2 /*= nullptr*/) -{ - UIArtefactParamItem* item = xr_new(); - - switch (item->Init(uiXml, section)) - { - case UIArtefactParamItem::InitResult::Failed: - xr_delete(item); - return nullptr; - case UIArtefactParamItem::InitResult::Plain: - item->SetDefaultValuesPlain(magnitude, isSignInverse, unit); - break; - } - - // use either translationId or translationId2 - // but set translationId if both unavailable - shared_str name = StringTable().translate(translationId); - shared_str name2 = translationId2 != nullptr ? StringTable().translate(translationId2) : nullptr; - - if (name != translationId && name2 != translationId2) - item->SetCaption(name2.c_str()); - else - item->SetCaption(name.c_str()); - - item->SetAutoDelete(false); - return item; -} - -UIArtefactParamItem* CUIArtefactParams::CreateItem(CUIXml& uiXml, pcstr section, - const shared_str& translationId, const shared_str& translationId2 /*= nullptr*/) -{ - return CreateItem(uiXml, section, 1.0f, false, nullptr, translationId, translationId2); -} - -bool CUIArtefactParams::Check(const shared_str& af_section) +bool CUIArtefactParams::Check(const shared_str& af_section) const { - return !!pSettings->line_exist(af_section, "af_actor_properties"); + return pSettings->line_exist(af_section, "af_actor_properties"); } void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) @@ -141,7 +123,9 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) if (!actor) return; - const shared_str& af_section = pInvItem.object().cNameSect(); + const auto& af_section = pInvItem.object().cNameSect().c_str(); + const auto& actor_sect = actor->cNameSect().c_str(); + const auto& condition_sect = pSettings->read_if_exists(actor_sect, "condition_sect", actor_sect); float h = 0.0f; if (m_Prop_line) @@ -164,7 +148,9 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) setValue(m_disp_condition, pInvItem.GetCondition()); //-Alundaio - for (auto [id, restore_section, restore_caption, magnitude, sign_inverse, unit] : af_restore) + const bool is_cs_cop = GMLib.GetLibraryVersion() > GAMEMTL_VERSION_SOC; + + for (auto [id, restore_section, actor_condition, restore_caption, magnitude, sign_inverse, unit] : af_restore) { if (!m_restore_item[id]) continue; @@ -173,7 +159,12 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) if (fis_zero(val)) continue; - val *= pInvItem.GetCondition(); + val = val * pInvItem.GetCondition(); + if (!is_cs_cop) + { + const float max_val = pSettings->r_float(condition_sect, actor_condition); + val /= max_val; + } setValue(m_restore_item[id], val); } @@ -184,12 +175,22 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) shared_str const& hit_absorbation_sect = pSettings->r_string(af_section, "hit_absorbation_sect"); float val = pSettings->r_float(hit_absorbation_sect, immunity_section); - if (fis_zero(val)) - continue; - val *= pInvItem.GetCondition(); - const float max_val = actor->conditions().GetZoneMaxPower(static_cast(id)); - val /= max_val; + if (is_cs_cop) + { + if (fis_zero(val)) + continue; + const float max_val = actor->conditions().GetZoneMaxPower(static_cast(id)); + val = val * pInvItem.GetCondition() / max_val; + } + else // SOC + { + if (fsimilar(val, 1.0f)) + continue; + val = 1.0f - val * pInvItem.GetCondition(); + + } + setValue(m_immunity_item[id], val); } @@ -208,70 +209,72 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) /// ---------------------------------------------------------------- -UIArtefactParamItem::UIArtefactParamItem() - : CUIStatic("Artefact Param Item"), - m_magnitude(1.0f), m_sign_inverse(false), m_unit_str(""), - m_texture_minus(""), m_texture_plus("") {} +UIArtefactParamItem::UIArtefactParamItem(pcstr param_name) + : CUIStatic(param_name), m_positive_color(green_clr_cop), m_negative_color(red_clr_cop) +{ + AttachChild(&m_value); +} -UIArtefactParamItem::InitResult UIArtefactParamItem::Init(CUIXml& xml, pcstr section) +bool UIArtefactParamItem::Init(CUIXml& xml, pcstr section, pcstr caption, + u32 positive_color, u32 negative_color, + float magnitude /*= 1.0f*/, bool is_sign_inverse /*= false*/, pcstr unit /*= ""*/) { if (!CUIXmlInit::InitStatic(xml, section, 0, this, false)) - return InitPlain(xml, section); + return false; const XML_NODE base_node = xml.GetLocalRoot(); xml.SetLocalRoot(xml.NavigateToNode(section)); - m_caption = UIHelper::CreateStatic(xml, "caption", this); - m_value = UIHelper::CreateStatic(xml, "value", this); - m_magnitude = xml.ReadAttribFlt("value", 0, "magnitude", 1.0f); - m_sign_inverse = (xml.ReadAttribInt("value", 0, "sign_inverse", 0) == 1); + m_caption = UIHelper::CreateStatic(xml, "caption", this, false); + if (m_caption) + m_caption->SetText(caption); + else + SetText(caption); - LPCSTR unit_str = xml.ReadAttrib("value", 0, "unit_str", ""); - m_unit_str._set(StringTable().translate(unit_str)); + m_positive_color = positive_color; + m_negative_color = negative_color; - LPCSTR texture_minus = xml.Read("texture_minus", 0, ""); - if (texture_minus && xr_strlen(texture_minus)) + if (!CUIXmlInit::InitStatic(xml, "value", 0, &m_value, m_caption)) { - m_texture_minus._set(texture_minus); - - LPCSTR texture_plus = xml.Read("caption:texture", 0, ""); - m_texture_plus._set(texture_plus); - VERIFY(m_texture_plus.size()); + CUIXmlInit::InitText(xml, section, 0, m_value.TextItemControl()); + + const auto font = GetFont(); + float text_len = font->SizeOf_(GetText()); + float space_len = font->SizeOf_(" "); + UI().ClientToScreenScaledWidth(text_len); + UI().ClientToScreenScaledWidth(space_len); + + auto rect = GetWndRect(); + rect.x1 += TextItemControl()->m_TextOffset.x + text_len + space_len; + m_value.SetWndRect(rect); + + m_magnitude = magnitude; + m_sign_inverse = is_sign_inverse; + m_unit_str = unit; + CUIXmlInit::GetColor("green", m_positive_color); + CUIXmlInit::GetColor("red", m_negative_color); } - xml.SetLocalRoot(base_node); - return InitResult::Normal; -} -UIArtefactParamItem::InitResult UIArtefactParamItem::InitPlain(CUIXml& xml, pcstr section) -{ - string256 buf; - strconcat(sizeof(buf), buf, af_params, ":static_", section); - if (!CUIXmlInit::InitStatic(xml, buf, 0, this, false)) - return InitResult::Failed; - - m_caption = xr_new("Caption"); - m_caption->SetAutoDelete(true); - AttachChild(m_caption); - m_caption->Show(false); // hack - - m_value = xr_new("Value"); - m_value->SetAutoDelete(true); - AttachChild(m_value); - m_value->Show(false); // hack - - return InitResult::Plain; -} + m_magnitude = xml.ReadAttribFlt("value", 0, "magnitude", m_magnitude); + m_sign_inverse = xml.ReadAttribInt("value", 0, "sign_inverse", m_sign_inverse) == 1; -void UIArtefactParamItem::SetDefaultValuesPlain(float magnitude, bool isSignInverse, const shared_str& unit) -{ - m_magnitude = magnitude; - m_sign_inverse = isSignInverse; - m_unit_str = unit; -} + if (cpcstr unit_str = xml.ReadAttrib("value", 0, "unit_str", m_unit_str.c_str())) + m_unit_str = StringTable().translate(unit_str); -void UIArtefactParamItem::SetCaption(LPCSTR name) -{ - m_caption->SetText(name); + m_positive_color = CUIXmlInit::GetColor(xml, "value:positive_color", 0, m_positive_color); + m_negative_color = CUIXmlInit::GetColor(xml, "value:negative_color", 0, m_negative_color); + + pcstr texture_minus = xml.Read("texture_minus", 0, nullptr); + if (texture_minus && texture_minus[0]) + { + m_texture_minus = texture_minus; + + pcstr texture_plus = xml.Read("caption:texture", 0, nullptr); + m_texture_plus = texture_plus; + VERIFY(m_texture_plus.size()); + } + xml.SetLocalRoot(base_node); + return true; } void UIArtefactParamItem::SetValue(float value) @@ -289,14 +292,14 @@ void UIArtefactParamItem::SetValue(float value) { STRCONCAT(str, buf); } - m_value->SetText(str); + m_value.SetText(str); - bool positive = (value >= 0.0f); - positive = (m_sign_inverse) ? !positive : positive; - u32 color = (positive) ? green_clr : red_clr; - m_value->SetTextColor(color); + bool positive = value >= 0.0f; + positive = m_sign_inverse ? !positive : positive; + u32 color = positive ? m_positive_color : m_negative_color; + m_value.SetTextColor(color); - if (m_texture_minus.size()) + if (m_texture_minus.size() && m_caption) { if (positive) { @@ -307,11 +310,4 @@ void UIArtefactParamItem::SetValue(float value) m_caption->InitTexture(m_texture_minus.c_str()); } } - - // hack - if (!m_caption->IsShown() && !m_value->IsShown()) - { - xr_sprintf(buf, "%s %s", m_caption->GetText(), m_value->GetText()); - SetText(buf); - } } diff --git a/src/xrGame/ui/ui_af_params.h b/src/xrGame/ui/ui_af_params.h index 3c89b2f8899..f205a8f544a 100644 --- a/src/xrGame/ui/ui_af_params.h +++ b/src/xrGame/ui/ui_af_params.h @@ -1,5 +1,7 @@ #pragma once + #include "xrUICore/Windows/UIWindow.h" + #include "xrServerEntities/alife_space.h" class CInventoryItem; @@ -10,23 +12,17 @@ class UIArtefactParamItem; class CUIArtefactParams final : public CUIWindow { public: - CUIArtefactParams() : CUIWindow("Artefact Params") {} + CUIArtefactParams(); ~CUIArtefactParams() override; + bool InitFromXml(CUIXml& xml); - bool Check(const shared_str& af_section); + bool Check(const shared_str& af_section) const; void SetInfo(const CInventoryItem& pInvItem); - pcstr GetDebugType() override { return "CUIArtefactParams"; } - -protected: - UIArtefactParamItem* CreateItem(CUIXml& uiXml, pcstr section, - const shared_str& translationId, const shared_str& translationId2 = nullptr); - UIArtefactParamItem* CreateItem(CUIXml& uiXml, pcstr section, - float magnitude, bool isSignInverse, const shared_str& unit, - const shared_str& translationId, const shared_str& translationId2 = nullptr); + pcstr GetDebugType() override { return "CUIArtefactParams"; } protected: - UIArtefactParamItem* m_disp_condition; //Alundaio: Show AF Condition + UIArtefactParamItem* m_disp_condition{}; //Alundaio: Show AF Condition UIArtefactParamItem* m_immunity_item[9]{}; UIArtefactParamItem* m_restore_item[ALife::eRestoreTypeMax]{}; UIArtefactParamItem* m_additional_weight{}; @@ -40,33 +36,24 @@ class CUIArtefactParams final : public CUIWindow class UIArtefactParamItem final : public CUIStatic { public: - UIArtefactParamItem(); - - enum class InitResult - { - Failed, - Normal, - Plain - }; + UIArtefactParamItem(pcstr param_name); - InitResult Init(CUIXml& xml, pcstr section); + bool Init(CUIXml& xml, pcstr section, pcstr caption, + u32 positive_color, u32 negative_color, + float magnitude = 1.0f, bool is_sign_inverse = false, pcstr unit = ""); - void SetDefaultValuesPlain(float magnitude, bool isSignInverse, const shared_str& unit); - void SetCaption(LPCSTR name); void SetValue(float value); pcstr GetDebugType() override { return "UIArtefactParamItem"; } -protected: - InitResult InitPlain(CUIXml& xml, pcstr section); - private: + CUIStatic m_value{ "value" }; CUIStatic* m_caption{}; - CUIStatic* m_value{}; - float m_magnitude; - bool m_sign_inverse; + float m_magnitude{ 1.0f }; + bool m_sign_inverse{ false }; shared_str m_unit_str; shared_str m_texture_minus; shared_str m_texture_plus; - + u32 m_positive_color; + u32 m_negative_color; }; // class UIArtefactParamItem From 110ce4f4d003762efdb5c61d56e84b46b50dd990 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 23 Dec 2025 14:05:49 +0300 Subject: [PATCH 088/261] Make CUIArtefactParams work in SOC (#392) Added Clear Sky color too (#382) Now works with all three games. --- src/xrGame/ui/ui_af_params.cpp | 294 ++++++++++++++++----------------- src/xrGame/ui/ui_af_params.h | 45 ++--- 2 files changed, 161 insertions(+), 178 deletions(-) diff --git a/src/xrGame/ui/ui_af_params.cpp b/src/xrGame/ui/ui_af_params.cpp index 231db108de8..27b9bb1ea8f 100644 --- a/src/xrGame/ui/ui_af_params.cpp +++ b/src/xrGame/ui/ui_af_params.cpp @@ -8,8 +8,15 @@ #include "UIXmlInit.h" #include "UIHelper.h" -u32 const red_clr = color_argb(255, 210, 50, 50); -u32 const green_clr = color_argb(255, 170, 170, 170); +constexpr pcstr af_params = "af_params"; + +constexpr u32 green_clr_cop = color_argb(255, 170, 170, 170); +constexpr u32 red_clr_cop = color_argb(255, 210, 50, 50); + +constexpr u32 green_clr_cs = color_argb(250, 50, 210, 50); +constexpr u32 red_clr_cs = color_argb(250, 210, 50, 50); + +CUIArtefactParams::CUIArtefactParams() : CUIWindow(af_params) {} CUIArtefactParams::~CUIArtefactParams() { @@ -21,114 +28,89 @@ CUIArtefactParams::~CUIArtefactParams() constexpr std::tuple af_immunity[] = { - //{ ALife::infl_, "section", "caption", magnitude, sign_inverse, "unit" } - { ALife::infl_rad, "radiation_immunity", "ui_inv_outfit_radiation_protection", 1.0f, false, "%" }, - { ALife::infl_fire, "burn_immunity", "ui_inv_outfit_burn_protection", 1.0f, false, "%" }, - { ALife::infl_acid, "chemical_burn_immunity", "ui_inv_outfit_chemical_burn_protection", 1.0f, false, "%" }, - { ALife::infl_psi, "telepatic_immunity", "ui_inv_outfit_telepatic_protection", 1.0f, false, "%" }, - { ALife::infl_electra, "shock_immunity", "ui_inv_outfit_shock_protection", 1.0f, false, "%" }, - { 5, "strike_immunity", "ui_inv_outfit_strike_protection", 1.0f, false, "%" }, - { 6, "wound_immunity", "ui_inv_outfit_wound_protection", 1.0f, false, "%" }, - { 7, "explosion_immunity", "ui_inv_outfit_explosion_protection", 1.0f, false, "%" }, - { 8, "fire_wound_immunity", "ui_inv_outfit_fire_wound_protection", 1.0f, false, "%" }, + //{ ALife::infl_, "section", "caption", magnitude, sign_inverse, "unit" } + { ALife::infl_rad, "radiation_immunity", "ui_inv_outfit_radiation_protection", 100.0f, false, "%" }, + { ALife::infl_fire, "burn_immunity", "ui_inv_outfit_burn_protection", 100.0f, false, "%" }, + { ALife::infl_acid, "chemical_burn_immunity", "ui_inv_outfit_chemical_burn_protection", 100.0f, false, "%" }, + { ALife::infl_psi, "telepatic_immunity", "ui_inv_outfit_telepatic_protection", 100.0f, false, "%" }, + { ALife::infl_electra, "shock_immunity", "ui_inv_outfit_shock_protection", 100.0f, false, "%" }, + { 5, "strike_immunity", "ui_inv_outfit_strike_protection", 100.0f, false, "%" }, + { 6, "wound_immunity", "ui_inv_outfit_wound_protection", 100.0f, false, "%" }, + { 7, "explosion_immunity", "ui_inv_outfit_explosion_protection", 100.0f, false, "%" }, + { 8, "fire_wound_immunity", "ui_inv_outfit_fire_wound_protection", 100.0f, false, "%" }, }; -constexpr std::tuple af_restore[] = +constexpr std::tuple af_restore[] = { - //{ ALife::EConditionRestoreType, "section", "caption", magnitude, sign_inverse, "unit" } - { ALife::eHealthRestoreSpeed, "health_restore_speed", "ui_inv_health", 1.0f, false, "%" }, - { ALife::eSatietyRestoreSpeed, "satiety_restore_speed", "ui_inv_satiety", 1.0f, false, "%" }, - { ALife::ePowerRestoreSpeed, "power_restore_speed", "ui_inv_power", 1.0f, false, nullptr }, - { ALife::eBleedingRestoreSpeed, "bleeding_restore_speed", "ui_inv_bleeding", -1.0f, true, "%" }, - { ALife::eRadiationRestoreSpeed, "radiation_restore_speed", "ui_inv_radiation", 1.0f, true, nullptr }, + //{ ALife::EConditionRestoreType, "section", "actor_condition", "caption", magnitude, sign_inverse, "unit" } + { ALife::eHealthRestoreSpeed, "health_restore_speed", "satiety_health_v", "ui_inv_health", 100.0f, false, "%" }, + { ALife::eSatietyRestoreSpeed, "satiety_restore_speed", "satiety_v", "ui_inv_satiety", 100.0f, false, "%" }, + { ALife::ePowerRestoreSpeed, "power_restore_speed", "satiety_power_v", "ui_inv_power", 1.0f, false, nullptr }, + { ALife::eBleedingRestoreSpeed, "bleeding_restore_speed", "wound_incarnation_v", "ui_inv_bleeding", -100.0f, true, "%" }, + { ALife::eRadiationRestoreSpeed, "radiation_restore_speed", "radiation_v", "ui_inv_radiation", 1.0f, true, nullptr }, }; static_assert(std::size(af_restore) == ALife::eRestoreTypeMax, "All restore types should be listed in the tuple above."); -/* -LPCSTR af_actor_param_names[]= -{ - "satiety_health_v", - "radiation_v", - "satiety_v", - "satiety_power_v", - "wound_incarnation_v", -}; -*/ - -constexpr pcstr af_params = "af_params"; - bool CUIArtefactParams::InitFromXml(CUIXml& xml) { XML_NODE stored_root = xml.GetLocalRoot(); XML_NODE base_node = xml.NavigateToNode(af_params, 0); if (!base_node) - { return false; - } + CUIXmlInit::InitWindow(xml, af_params, 0, this); xml.SetLocalRoot(base_node); if ((m_Prop_line = UIHelper::CreateStatic(xml, "prop_line", this, false))) m_Prop_line->SetAutoDelete(false); + const u32 positive_color = m_Prop_line ? green_clr_cop : green_clr_cs; + const u32 negative_color = m_Prop_line ? red_clr_cop : red_clr_cs; + + const auto create_item = [&](pcstr section, pcstr caption, + float magnitude = 1.0f, bool is_sign_inverse = false, pcstr unit = "") + { + UIArtefactParamItem* item = xr_new(section); + + string256 buf; + strconcat(buf, "static_", section); + + if (item->Init(xml, section, caption, positive_color, negative_color, magnitude, is_sign_inverse, unit) || + item->Init(xml, buf, caption, positive_color, negative_color, magnitude, is_sign_inverse, unit)) + { + item->SetAutoDelete(false); + return item; + } + xr_delete(item); + return item; + }; + //Alundaio: Show AF Condition - m_disp_condition = CreateItem(xml, "condition", "ui_inv_af_condition"); + m_disp_condition = create_item("condition", "ui_inv_af_condition"); //-Alundaio - for (auto [id, section, caption, magnitude, sign_inverse, unit] : af_restore) + for (auto [id, section, actor_condition, caption_id, magnitude, sign_inverse, unit] : af_restore) { - m_restore_item[id] = CreateItem(xml, section, magnitude, sign_inverse, unit, caption); + const auto caption = StringTable().translate(caption_id).c_str(); + m_restore_item[id] = create_item(section, caption, magnitude, sign_inverse, unit); } - for (auto [id, section, caption, magnitude, sign_inverse, unit] : af_immunity) + for (auto [id, section, caption_id, magnitude, sign_inverse, unit] : af_immunity) { - m_immunity_item[id] = CreateItem(xml, section, magnitude, sign_inverse, unit, caption); + const auto caption = StringTable().translate(caption_id).c_str(); + m_immunity_item[id] = create_item(section, caption, magnitude, sign_inverse, unit); } - m_additional_weight = CreateItem(xml, "additional_weight", "ui_inv_weight", "ui_inv_outfit_additional_weight"); + + const auto weight = StringTable().translate("ui_inv_weight", "ui_inv_outfit_additional_weight"); + m_additional_weight = create_item("additional_weight", weight.c_str()); xml.SetLocalRoot(stored_root); return true; } -UIArtefactParamItem* CUIArtefactParams::CreateItem(CUIXml& uiXml, pcstr section, - float magnitude, bool isSignInverse, const shared_str& unit, - const shared_str& translationId, const shared_str& translationId2 /*= nullptr*/) -{ - UIArtefactParamItem* item = xr_new(); - - switch (item->Init(uiXml, section)) - { - case UIArtefactParamItem::InitResult::Failed: - xr_delete(item); - return nullptr; - case UIArtefactParamItem::InitResult::Plain: - item->SetDefaultValuesPlain(magnitude, isSignInverse, unit); - break; - } - - // use either translationId or translationId2 - // but set translationId if both unavailable - shared_str name = StringTable().translate(translationId); - shared_str name2 = translationId2 != nullptr ? StringTable().translate(translationId2) : nullptr; - - if (name != translationId && name2 != translationId2) - item->SetCaption(name2.c_str()); - else - item->SetCaption(name.c_str()); - - item->SetAutoDelete(false); - return item; -} - -UIArtefactParamItem* CUIArtefactParams::CreateItem(CUIXml& uiXml, pcstr section, - const shared_str& translationId, const shared_str& translationId2 /*= nullptr*/) -{ - return CreateItem(uiXml, section, 1.0f, false, nullptr, translationId, translationId2); -} - -bool CUIArtefactParams::Check(const shared_str& af_section) +bool CUIArtefactParams::Check(const shared_str& af_section) const { - return !!pSettings->line_exist(af_section, "af_actor_properties"); + return pSettings->line_exist(af_section, "af_actor_properties"); } void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) @@ -141,7 +123,9 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) if (!actor) return; - const shared_str& af_section = pInvItem.object().cNameSect(); + const auto& af_section = pInvItem.object().cNameSect().c_str(); + const auto& actor_sect = actor->cNameSect().c_str(); + const auto& condition_sect = pSettings->read_if_exists(actor_sect, "condition_sect", actor_sect); float h = 0.0f; if (m_Prop_line) @@ -164,7 +148,9 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) setValue(m_disp_condition, pInvItem.GetCondition()); //-Alundaio - for (auto [id, restore_section, restore_caption, magnitude, sign_inverse, unit] : af_restore) + const bool is_cs_cop = GMLib.GetLibraryVersion() > GAMEMTL_VERSION_SOC; + + for (auto [id, restore_section, actor_condition, restore_caption, magnitude, sign_inverse, unit] : af_restore) { if (!m_restore_item[id]) continue; @@ -173,7 +159,12 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) if (fis_zero(val)) continue; - val *= pInvItem.GetCondition(); + val = val * pInvItem.GetCondition(); + if (!is_cs_cop) + { + const float max_val = pSettings->r_float(condition_sect, actor_condition); + val /= max_val; + } setValue(m_restore_item[id], val); } @@ -184,12 +175,22 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) shared_str const& hit_absorbation_sect = pSettings->r_string(af_section, "hit_absorbation_sect"); float val = pSettings->r_float(hit_absorbation_sect, immunity_section); - if (fis_zero(val)) - continue; - val *= pInvItem.GetCondition(); - const float max_val = actor->conditions().GetZoneMaxPower(static_cast(id)); - val /= max_val; + if (is_cs_cop) + { + if (fis_zero(val)) + continue; + const float max_val = actor->conditions().GetZoneMaxPower(static_cast(id)); + val = val * pInvItem.GetCondition() / max_val; + } + else // SOC + { + if (fsimilar(val, 1.0f)) + continue; + val = 1.0f - val * pInvItem.GetCondition(); + + } + setValue(m_immunity_item[id], val); } @@ -208,70 +209,72 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) /// ---------------------------------------------------------------- -UIArtefactParamItem::UIArtefactParamItem() - : CUIStatic("Artefact Param Item"), - m_magnitude(1.0f), m_sign_inverse(false), m_unit_str(""), - m_texture_minus(""), m_texture_plus("") {} +UIArtefactParamItem::UIArtefactParamItem(pcstr param_name) + : CUIStatic(param_name), m_positive_color(green_clr_cop), m_negative_color(red_clr_cop) +{ + AttachChild(&m_value); +} -UIArtefactParamItem::InitResult UIArtefactParamItem::Init(CUIXml& xml, pcstr section) +bool UIArtefactParamItem::Init(CUIXml& xml, pcstr section, pcstr caption, + u32 positive_color, u32 negative_color, + float magnitude /*= 1.0f*/, bool is_sign_inverse /*= false*/, pcstr unit /*= ""*/) { if (!CUIXmlInit::InitStatic(xml, section, 0, this, false)) - return InitPlain(xml, section); + return false; const XML_NODE base_node = xml.GetLocalRoot(); xml.SetLocalRoot(xml.NavigateToNode(section)); - m_caption = UIHelper::CreateStatic(xml, "caption", this); - m_value = UIHelper::CreateStatic(xml, "value", this); - m_magnitude = xml.ReadAttribFlt("value", 0, "magnitude", 1.0f); - m_sign_inverse = (xml.ReadAttribInt("value", 0, "sign_inverse", 0) == 1); + m_caption = UIHelper::CreateStatic(xml, "caption", this, false); + if (m_caption) + m_caption->SetText(caption); + else + SetText(caption); - LPCSTR unit_str = xml.ReadAttrib("value", 0, "unit_str", ""); - m_unit_str._set(StringTable().translate(unit_str)); + m_positive_color = positive_color; + m_negative_color = negative_color; - LPCSTR texture_minus = xml.Read("texture_minus", 0, ""); - if (texture_minus && xr_strlen(texture_minus)) + if (!CUIXmlInit::InitStatic(xml, "value", 0, &m_value, m_caption)) { - m_texture_minus._set(texture_minus); - - LPCSTR texture_plus = xml.Read("caption:texture", 0, ""); - m_texture_plus._set(texture_plus); - VERIFY(m_texture_plus.size()); + CUIXmlInit::InitText(xml, section, 0, m_value.TextItemControl()); + + const auto font = GetFont(); + float text_len = font->SizeOf_(GetText()); + float space_len = font->SizeOf_(" "); + UI().ClientToScreenScaledWidth(text_len); + UI().ClientToScreenScaledWidth(space_len); + + auto rect = GetWndRect(); + rect.x1 += TextItemControl()->m_TextOffset.x + text_len + space_len; + m_value.SetWndRect(rect); + + m_magnitude = magnitude; + m_sign_inverse = is_sign_inverse; + m_unit_str = unit; + CUIXmlInit::GetColor("green", m_positive_color); + CUIXmlInit::GetColor("red", m_negative_color); } - xml.SetLocalRoot(base_node); - return InitResult::Normal; -} -UIArtefactParamItem::InitResult UIArtefactParamItem::InitPlain(CUIXml& xml, pcstr section) -{ - string256 buf; - strconcat(sizeof(buf), buf, af_params, ":static_", section); - if (!CUIXmlInit::InitStatic(xml, buf, 0, this, false)) - return InitResult::Failed; - - m_caption = xr_new("Caption"); - m_caption->SetAutoDelete(true); - AttachChild(m_caption); - m_caption->Show(false); // hack - - m_value = xr_new("Value"); - m_value->SetAutoDelete(true); - AttachChild(m_value); - m_value->Show(false); // hack - - return InitResult::Plain; -} + m_magnitude = xml.ReadAttribFlt("value", 0, "magnitude", m_magnitude); + m_sign_inverse = xml.ReadAttribInt("value", 0, "sign_inverse", m_sign_inverse) == 1; -void UIArtefactParamItem::SetDefaultValuesPlain(float magnitude, bool isSignInverse, const shared_str& unit) -{ - m_magnitude = magnitude; - m_sign_inverse = isSignInverse; - m_unit_str = unit; -} + if (cpcstr unit_str = xml.ReadAttrib("value", 0, "unit_str", m_unit_str.c_str())) + m_unit_str = StringTable().translate(unit_str); -void UIArtefactParamItem::SetCaption(LPCSTR name) -{ - m_caption->SetText(name); + m_positive_color = CUIXmlInit::GetColor(xml, "value:positive_color", 0, m_positive_color); + m_negative_color = CUIXmlInit::GetColor(xml, "value:negative_color", 0, m_negative_color); + + pcstr texture_minus = xml.Read("texture_minus", 0, nullptr); + if (texture_minus && texture_minus[0]) + { + m_texture_minus = texture_minus; + + pcstr texture_plus = xml.Read("caption:texture", 0, nullptr); + m_texture_plus = texture_plus; + VERIFY(m_texture_plus.size()); + } + xml.SetLocalRoot(base_node); + return true; } void UIArtefactParamItem::SetValue(float value) @@ -289,14 +292,14 @@ void UIArtefactParamItem::SetValue(float value) { STRCONCAT(str, buf); } - m_value->SetText(str); + m_value.SetText(str); - bool positive = (value >= 0.0f); - positive = (m_sign_inverse) ? !positive : positive; - u32 color = (positive) ? green_clr : red_clr; - m_value->SetTextColor(color); + bool positive = value >= 0.0f; + positive = m_sign_inverse ? !positive : positive; + u32 color = positive ? m_positive_color : m_negative_color; + m_value.SetTextColor(color); - if (m_texture_minus.size()) + if (m_texture_minus.size() && m_caption) { if (positive) { @@ -307,11 +310,4 @@ void UIArtefactParamItem::SetValue(float value) m_caption->InitTexture(m_texture_minus.c_str()); } } - - // hack - if (!m_caption->IsShown() && !m_value->IsShown()) - { - xr_sprintf(buf, "%s %s", m_caption->GetText(), m_value->GetText()); - SetText(buf); - } } diff --git a/src/xrGame/ui/ui_af_params.h b/src/xrGame/ui/ui_af_params.h index 3c89b2f8899..f205a8f544a 100644 --- a/src/xrGame/ui/ui_af_params.h +++ b/src/xrGame/ui/ui_af_params.h @@ -1,5 +1,7 @@ #pragma once + #include "xrUICore/Windows/UIWindow.h" + #include "xrServerEntities/alife_space.h" class CInventoryItem; @@ -10,23 +12,17 @@ class UIArtefactParamItem; class CUIArtefactParams final : public CUIWindow { public: - CUIArtefactParams() : CUIWindow("Artefact Params") {} + CUIArtefactParams(); ~CUIArtefactParams() override; + bool InitFromXml(CUIXml& xml); - bool Check(const shared_str& af_section); + bool Check(const shared_str& af_section) const; void SetInfo(const CInventoryItem& pInvItem); - pcstr GetDebugType() override { return "CUIArtefactParams"; } - -protected: - UIArtefactParamItem* CreateItem(CUIXml& uiXml, pcstr section, - const shared_str& translationId, const shared_str& translationId2 = nullptr); - UIArtefactParamItem* CreateItem(CUIXml& uiXml, pcstr section, - float magnitude, bool isSignInverse, const shared_str& unit, - const shared_str& translationId, const shared_str& translationId2 = nullptr); + pcstr GetDebugType() override { return "CUIArtefactParams"; } protected: - UIArtefactParamItem* m_disp_condition; //Alundaio: Show AF Condition + UIArtefactParamItem* m_disp_condition{}; //Alundaio: Show AF Condition UIArtefactParamItem* m_immunity_item[9]{}; UIArtefactParamItem* m_restore_item[ALife::eRestoreTypeMax]{}; UIArtefactParamItem* m_additional_weight{}; @@ -40,33 +36,24 @@ class CUIArtefactParams final : public CUIWindow class UIArtefactParamItem final : public CUIStatic { public: - UIArtefactParamItem(); - - enum class InitResult - { - Failed, - Normal, - Plain - }; + UIArtefactParamItem(pcstr param_name); - InitResult Init(CUIXml& xml, pcstr section); + bool Init(CUIXml& xml, pcstr section, pcstr caption, + u32 positive_color, u32 negative_color, + float magnitude = 1.0f, bool is_sign_inverse = false, pcstr unit = ""); - void SetDefaultValuesPlain(float magnitude, bool isSignInverse, const shared_str& unit); - void SetCaption(LPCSTR name); void SetValue(float value); pcstr GetDebugType() override { return "UIArtefactParamItem"; } -protected: - InitResult InitPlain(CUIXml& xml, pcstr section); - private: + CUIStatic m_value{ "value" }; CUIStatic* m_caption{}; - CUIStatic* m_value{}; - float m_magnitude; - bool m_sign_inverse; + float m_magnitude{ 1.0f }; + bool m_sign_inverse{ false }; shared_str m_unit_str; shared_str m_texture_minus; shared_str m_texture_plus; - + u32 m_positive_color; + u32 m_negative_color; }; // class UIArtefactParamItem From b33b8638fc7940d12d36469f376a652cceda8697 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:02:34 +0300 Subject: [PATCH 089/261] build(deps): bump cross-platform-actions/action from 0.30.0 to 0.32.0 (#2017) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cibuild.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index eed0a2d658e..de48121a37b 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -204,7 +204,7 @@ jobs: submodules: recursive - name: Setup ${{ matrix.platform.name }} and packages - uses: cross-platform-actions/action@v0.30.0 + uses: cross-platform-actions/action@v0.32.0 with: operating_system: ${{ matrix.platform.os }} architecture: ${{ matrix.platform.arch }} @@ -217,7 +217,7 @@ jobs: run: ${{ matrix.platform.install-cmd }} - name: Run CMake - uses: cross-platform-actions/action@v0.30.0 + uses: cross-platform-actions/action@v0.32.0 with: operating_system: ${{ matrix.platform.os }} architecture: ${{ matrix.platform.arch }} @@ -230,7 +230,7 @@ jobs: run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.Configuration }} -DCMAKE_UNITY_BUILD=ON --log-level VERBOSE - name: Run CMake Build - uses: cross-platform-actions/action@v0.30.0 + uses: cross-platform-actions/action@v0.32.0 with: operating_system: ${{ matrix.platform.os }} architecture: ${{ matrix.platform.arch }} From 2b13929b603ff792f0bcc769271aa4dfaf3c8978 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:02:34 +0300 Subject: [PATCH 090/261] build(deps): bump cross-platform-actions/action from 0.30.0 to 0.32.0 (#2017) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cibuild.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index eed0a2d658e..de48121a37b 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -204,7 +204,7 @@ jobs: submodules: recursive - name: Setup ${{ matrix.platform.name }} and packages - uses: cross-platform-actions/action@v0.30.0 + uses: cross-platform-actions/action@v0.32.0 with: operating_system: ${{ matrix.platform.os }} architecture: ${{ matrix.platform.arch }} @@ -217,7 +217,7 @@ jobs: run: ${{ matrix.platform.install-cmd }} - name: Run CMake - uses: cross-platform-actions/action@v0.30.0 + uses: cross-platform-actions/action@v0.32.0 with: operating_system: ${{ matrix.platform.os }} architecture: ${{ matrix.platform.arch }} @@ -230,7 +230,7 @@ jobs: run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.Configuration }} -DCMAKE_UNITY_BUILD=ON --log-level VERBOSE - name: Run CMake Build - uses: cross-platform-actions/action@v0.30.0 + uses: cross-platform-actions/action@v0.32.0 with: operating_system: ${{ matrix.platform.os }} architecture: ${{ matrix.platform.arch }} From 6a44e8dc0d50758328a25011997cbe0ebcb455dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:03:12 +0300 Subject: [PATCH 091/261] build(deps): bump Externals/imgui from `9ca7ea0` to `0a47833` (#2015) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 9ca7ea00c80..0a4783309d8 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 9ca7ea00c80c1cb95b31cb3efa6f88cea33edf2a +Subproject commit 0a4783309d85bf9a6a646cf21ed2f7bb526ddf2a From 6d280f90072729f0c1ba071bfb50ba38c2e9cada Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:03:12 +0300 Subject: [PATCH 092/261] build(deps): bump Externals/imgui from `9ca7ea0` to `0a47833` (#2015) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 9ca7ea00c80..0a4783309d8 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 9ca7ea00c80c1cb95b31cb3efa6f88cea33edf2a +Subproject commit 0a4783309d85bf9a6a646cf21ed2f7bb526ddf2a From 496245677cd60ce6929edbf7111ba4db46a15e15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:03:33 +0300 Subject: [PATCH 093/261] build(deps): bump Externals/sse2neon from `d352d24` to `ade5552` (#2016) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2neon b/Externals/sse2neon index d352d244aa0..ade5552a328 160000 --- a/Externals/sse2neon +++ b/Externals/sse2neon @@ -1 +1 @@ -Subproject commit d352d244aa0ecd1aabe5d20ffbb4b86fec74cded +Subproject commit ade5552a32852422e4f34f0beaa51790ef9f4171 From 17c038a12733104e74ba1b23c17000f80c68d01c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:03:33 +0300 Subject: [PATCH 094/261] build(deps): bump Externals/sse2neon from `d352d24` to `ade5552` (#2016) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2neon b/Externals/sse2neon index d352d244aa0..ade5552a328 160000 --- a/Externals/sse2neon +++ b/Externals/sse2neon @@ -1 +1 @@ -Subproject commit d352d244aa0ecd1aabe5d20ffbb4b86fec74cded +Subproject commit ade5552a32852422e4f34f0beaa51790ef9f4171 From 19a9474663b5d3e0ab27efa6973cd4c08e15a211 Mon Sep 17 00:00:00 2001 From: Ramil Sattarov <51679282+r-a-sattarov@users.noreply.github.com> Date: Tue, 23 Dec 2025 22:47:13 +0300 Subject: [PATCH 095/261] Fixed configuration on CMake < 3.24 (#2018) Co-authored-by: Sultan Luchezarniy --- cmake/PreProjectInit.cmake | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/cmake/PreProjectInit.cmake b/cmake/PreProjectInit.cmake index b950c238274..d05660dd190 100644 --- a/cmake/PreProjectInit.cmake +++ b/cmake/PreProjectInit.cmake @@ -2,10 +2,23 @@ include_guard() message(VERBOSE "CMAKE_VERSION: ${CMAKE_VERSION}") -cmake_policy(SET CMP0138 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0138.html -cmake_policy(SET CMP0149 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0149.html -cmake_policy(SET CMP0162 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0162.html -cmake_policy(SET CMP0164 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0164.html +cmake_policy(SET CMP0129 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0129.html + +if (POLICY CMP0138) + cmake_policy(SET CMP0138 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0138.html +endif() + +if (POLICY CMP0149) + cmake_policy(SET CMP0149 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0149.html +endif() + +if (POLICY CMP0162) + cmake_policy(SET CMP0162 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0162.html +endif() + +if (POLICY CMP0164) + cmake_policy(SET CMP0164 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0164.html +endif() include(XRay.Configurations) From a2fdcf1dd8db19e4828cebbc9d7e13e4e44a474a Mon Sep 17 00:00:00 2001 From: Ramil Sattarov <51679282+r-a-sattarov@users.noreply.github.com> Date: Tue, 23 Dec 2025 22:47:13 +0300 Subject: [PATCH 096/261] Fixed configuration on CMake < 3.24 (#2018) Co-authored-by: Sultan Luchezarniy --- cmake/PreProjectInit.cmake | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/cmake/PreProjectInit.cmake b/cmake/PreProjectInit.cmake index b950c238274..d05660dd190 100644 --- a/cmake/PreProjectInit.cmake +++ b/cmake/PreProjectInit.cmake @@ -2,10 +2,23 @@ include_guard() message(VERBOSE "CMAKE_VERSION: ${CMAKE_VERSION}") -cmake_policy(SET CMP0138 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0138.html -cmake_policy(SET CMP0149 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0149.html -cmake_policy(SET CMP0162 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0162.html -cmake_policy(SET CMP0164 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0164.html +cmake_policy(SET CMP0129 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0129.html + +if (POLICY CMP0138) + cmake_policy(SET CMP0138 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0138.html +endif() + +if (POLICY CMP0149) + cmake_policy(SET CMP0149 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0149.html +endif() + +if (POLICY CMP0162) + cmake_policy(SET CMP0162 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0162.html +endif() + +if (POLICY CMP0164) + cmake_policy(SET CMP0164 NEW) # https://cmake.org/cmake/help/latest/policy/CMP0164.html +endif() include(XRay.Configurations) From f77e27ced140adf7f893becf7d46c64c913748a1 Mon Sep 17 00:00:00 2001 From: Igor Anpilogov Date: Wed, 24 Dec 2025 19:15:56 +0100 Subject: [PATCH 097/261] xr_ini: add wildcard ltx imports (#2005) --- src/xrCore/xr_ini.cpp | 47 +++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/xrCore/xr_ini.cpp b/src/xrCore/xr_ini.cpp index 7997af2c13b..9ac7808b6db 100644 --- a/src/xrCore/xr_ini.cpp +++ b/src/xrCore/xr_ini.cpp @@ -455,24 +455,45 @@ void CInifile::Load(IReader* F, pcstr path, allow_include_func_t allow_include_f R_ASSERT(path && path[0]); if (_GetItem(str, 1, inc_name, '"')) { - string_path fn; - strconcat(sizeof fn, fn, path, inc_name); - if (!allow_include_func || allow_include_func(fn)) + string_path fn, inc_path, folder; + strconcat(sizeof(fn), fn, path, inc_name); + _splitpath(fn, inc_path, folder, 0, 0); + xr_strcat(inc_path, sizeof(inc_path), folder); + + const auto loadFile = [&](const string_path _fn, const string_path name) { - IReader* I = FS.r_open(fn); + if (!allow_include_func || allow_include_func(_fn)) + { + IReader* I = FS.r_open(_fn); #ifndef XR_PLATFORM_WINDOWS // XXX: replace with runtime check for case-sensitivity - if (I == nullptr) + if (I == nullptr) + { + xr_fs_nostrlwr(inc_name); + strconcat(fn, path, inc_name); + I = FS.r_open(fn); + } +#endif + R_ASSERT3(I, "Can't find include file:", name); + Load(I, inc_path, allow_include_func); + FS.r_close(I); + } + }; + + if (strstr(inc_name, "*.ltx")) + { + FS_FileSet fset; + FS.file_list(fset, inc_path, FS_ListFiles, inc_name); + + for (FS_FileSet::iterator it = fset.begin(); it != fset.end(); it++) { - xr_fs_nostrlwr(inc_name); - strconcat(fn, path, inc_name); - I = FS.r_open(fn); + LPCSTR _name = it->name.c_str(); + string_path _fn; + strconcat(sizeof(_fn), _fn, inc_path, _name); + loadFile(_fn, _name); } -#endif - R_ASSERT3(I, "Can't find include file:", inc_name); - const xr_string inc_path = EFS_Utils::ExtractFilePath(fn); - Load(I, inc_path.c_str(), allow_include_func); - FS.r_close(I); } + else + loadFile(fn, inc_name); } } else if (str[0] && str[0] == '[') // new section ? From 3f8e02f5c594f8da05ff5fae1ed13e07e2c1b457 Mon Sep 17 00:00:00 2001 From: Igor Anpilogov Date: Wed, 24 Dec 2025 19:15:56 +0100 Subject: [PATCH 098/261] xr_ini: add wildcard ltx imports (#2005) --- src/xrCore/xr_ini.cpp | 47 +++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/xrCore/xr_ini.cpp b/src/xrCore/xr_ini.cpp index 7997af2c13b..9ac7808b6db 100644 --- a/src/xrCore/xr_ini.cpp +++ b/src/xrCore/xr_ini.cpp @@ -455,24 +455,45 @@ void CInifile::Load(IReader* F, pcstr path, allow_include_func_t allow_include_f R_ASSERT(path && path[0]); if (_GetItem(str, 1, inc_name, '"')) { - string_path fn; - strconcat(sizeof fn, fn, path, inc_name); - if (!allow_include_func || allow_include_func(fn)) + string_path fn, inc_path, folder; + strconcat(sizeof(fn), fn, path, inc_name); + _splitpath(fn, inc_path, folder, 0, 0); + xr_strcat(inc_path, sizeof(inc_path), folder); + + const auto loadFile = [&](const string_path _fn, const string_path name) { - IReader* I = FS.r_open(fn); + if (!allow_include_func || allow_include_func(_fn)) + { + IReader* I = FS.r_open(_fn); #ifndef XR_PLATFORM_WINDOWS // XXX: replace with runtime check for case-sensitivity - if (I == nullptr) + if (I == nullptr) + { + xr_fs_nostrlwr(inc_name); + strconcat(fn, path, inc_name); + I = FS.r_open(fn); + } +#endif + R_ASSERT3(I, "Can't find include file:", name); + Load(I, inc_path, allow_include_func); + FS.r_close(I); + } + }; + + if (strstr(inc_name, "*.ltx")) + { + FS_FileSet fset; + FS.file_list(fset, inc_path, FS_ListFiles, inc_name); + + for (FS_FileSet::iterator it = fset.begin(); it != fset.end(); it++) { - xr_fs_nostrlwr(inc_name); - strconcat(fn, path, inc_name); - I = FS.r_open(fn); + LPCSTR _name = it->name.c_str(); + string_path _fn; + strconcat(sizeof(_fn), _fn, inc_path, _name); + loadFile(_fn, _name); } -#endif - R_ASSERT3(I, "Can't find include file:", inc_name); - const xr_string inc_path = EFS_Utils::ExtractFilePath(fn); - Load(I, inc_path.c_str(), allow_include_func); - FS.r_close(I); } + else + loadFile(fn, inc_name); } } else if (str[0] && str[0] == '[') // new section ? From 2dce6e7b3bb6f4dd57a99ecd208b286251122d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=8B=D0=B1=D0=B0=D0=B5=D0=B2=20=D0=98=D0=BB=D1=8C?= =?UTF-8?q?=D1=8F?= <61088459+Aardes-Roiven@users.noreply.github.com> Date: Thu, 25 Dec 2025 14:06:05 +0300 Subject: [PATCH 099/261] Remove shared_str::operator* (#1948) (#2019) --- src/Layers/xrRender/ModelPool.cpp | 2 +- src/xrCore/xrstring.h | 4 +- src/xrGame/Actor.cpp | 10 ++-- src/xrGame/ActorCondition.cpp | 2 +- src/xrGame/Actor_Network.cpp | 20 +++---- src/xrGame/ContextMenu.cpp | 4 +- src/xrGame/CustomMonster.cpp | 2 +- src/xrGame/DestroyablePhysicsObject.cpp | 2 +- src/xrGame/GameObject.cpp | 52 +++++++++---------- src/xrGame/GameTask.cpp | 12 ++--- src/xrGame/GametaskManager.cpp | 2 +- src/xrGame/HangingLamp.cpp | 16 +++--- src/xrGame/Helicopter.cpp | 10 ++-- src/xrGame/Helicopter2.cpp | 12 ++--- src/xrGame/InfoPortion.cpp | 2 +- src/xrGame/Inventory.cpp | 8 +-- src/xrGame/InventoryOwner.cpp | 4 +- src/xrGame/Level.cpp | 4 +- src/xrGame/Level_network.cpp | 14 ++--- src/xrGame/Level_network_spawn.cpp | 8 +-- src/xrGame/PhraseDialog.cpp | 14 ++--- src/xrGame/PhraseScript.cpp | 28 +++++----- src/xrGame/PhysicObject.cpp | 4 +- src/xrGame/WeaponMagazined.cpp | 14 ++--- src/xrGame/actor_communication.cpp | 6 +-- src/xrGame/agent_enemy_manager.cpp | 4 +- .../basemonster/base_monster_debug.cpp | 30 +++++------ .../basemonster/base_monster_script.cpp | 2 +- src/xrGame/ai/monsters/monster_home.cpp | 12 +++-- src/xrGame/ai/stalker/ai_stalker.cpp | 12 ++--- src/xrGame/ai/stalker/ai_stalker_cover.cpp | 2 +- src/xrGame/ai/stalker/ai_stalker_debug.cpp | 46 ++++++++-------- src/xrGame/ai/trader/ai_trader.cpp | 2 +- src/xrGame/alife_simulator_base.cpp | 10 ++-- src/xrGame/alife_simulator_script.cpp | 14 ++--- src/xrGame/animation_script_callback.cpp | 2 +- src/xrGame/console_commands.cpp | 6 +-- src/xrGame/ef_storage_script.cpp | 4 +- src/xrGame/enemy_manager.cpp | 4 +- src/xrGame/fs_registrator_script.cpp | 2 +- src/xrGame/game_cl_base.cpp | 6 +-- .../game_cl_base_weapon_usage_statistic.cpp | 18 +++---- ...me_cl_base_weapon_usage_statistic_save.cpp | 2 +- src/xrGame/game_cl_capture_the_artefact.cpp | 2 +- src/xrGame/game_cl_deathmatch.cpp | 22 ++++---- src/xrGame/game_cl_deathmatch_buywnd.cpp | 10 ++-- src/xrGame/game_cl_teamdeathmatch.cpp | 10 ++-- src/xrGame/game_sv_artefacthunt.cpp | 10 ++-- src/xrGame/game_sv_capture_the_artefact.cpp | 36 ++++++------- src/xrGame/game_sv_deathmatch.cpp | 26 +++++----- src/xrGame/game_sv_mp.cpp | 12 ++--- src/xrGame/game_sv_single.cpp | 10 ++-- src/xrGame/game_sv_teamdeathmatch.cpp | 10 ++-- src/xrGame/inventory_item_upgrade.cpp | 2 +- src/xrGame/inventory_owner_info.cpp | 2 +- src/xrGame/item_manager.cpp | 8 +-- src/xrGame/level_debug.cpp | 4 +- src/xrGame/level_script.cpp | 4 +- src/xrGame/level_sounds.cpp | 2 +- src/xrGame/map_location.cpp | 16 +++--- src/xrGame/map_manager.cpp | 4 +- src/xrGame/material_manager.cpp | 4 +- src/xrGame/movement_manager.cpp | 2 +- src/xrGame/moving_objects.cpp | 6 +-- .../moving_objects_dynamic_collision.cpp | 10 ++-- src/xrGame/object_handler.cpp | 12 ++--- src/xrGame/object_handler_planner.cpp | 6 +-- src/xrGame/patrol_path_manager.cpp | 48 ++++++++--------- src/xrGame/physics_game.cpp | 2 +- src/xrGame/purchase_list.cpp | 6 +-- src/xrGame/restricted_object.cpp | 12 ++--- src/xrGame/script_entity.cpp | 22 ++++---- src/xrGame/script_game_object.cpp | 4 +- src/xrGame/script_game_object3.cpp | 6 +-- .../script_game_object_inventory_owner.cpp | 16 +++--- src/xrGame/script_game_object_use.cpp | 14 ++--- src/xrGame/script_particle_action.cpp | 2 +- src/xrGame/script_sound.cpp | 6 +-- src/xrGame/script_sound_action.cpp | 2 +- src/xrGame/seniority_hierarchy_holder.cpp | 2 +- src/xrGame/smart_cover_description.cpp | 4 +- src/xrGame/sound_player.cpp | 10 ++-- src/xrGame/space_restriction.cpp | 8 +-- src/xrGame/space_restriction_bridge_inline.h | 2 +- src/xrGame/space_restriction_composition.cpp | 10 ++-- src/xrGame/space_restriction_holder.cpp | 10 ++-- src/xrGame/space_restriction_manager.cpp | 20 +++---- src/xrGame/space_restriction_shape.cpp | 2 +- src/xrGame/squad_hierarchy_holder.cpp | 2 +- src/xrGame/stalker_animation_callbacks.cpp | 6 +-- src/xrGame/stalker_animation_global.cpp | 2 +- src/xrGame/stalker_animation_manager.cpp | 12 ++--- .../stalker_animation_manager_debug.cpp | 10 ++-- .../stalker_animation_manager_update.cpp | 2 +- src/xrGame/stalker_animation_script.cpp | 4 +- src/xrGame/stalker_animation_torso.cpp | 4 +- src/xrGame/stalker_combat_action_base.cpp | 6 +-- src/xrGame/stalker_movement_manager_base.cpp | 12 ++--- .../stalker_movement_manager_smart_cover.cpp | 4 +- src/xrGame/stalker_planner.cpp | 2 +- src/xrGame/stalker_sound_data_visitor.cpp | 2 +- src/xrGame/step_manager.cpp | 18 +++---- src/xrGame/team_hierarchy_holder.cpp | 2 +- src/xrGame/trade_parameters_inline.h | 2 +- src/xrGame/ui/ChangeWeatherDialog.cpp | 2 +- src/xrGame/ui/ServerList.cpp | 24 ++++----- src/xrGame/ui/TeamInfo.cpp | 8 +-- src/xrGame/ui/UIActorInfo.cpp | 4 +- src/xrGame/ui/UICellCustomItems.cpp | 18 +++---- src/xrGame/ui/UIColorAnimatorWrapper.cpp | 6 +-- src/xrGame/ui/UIInventoryUtilities.cpp | 10 ++-- src/xrGame/ui/UIItemInfo.cpp | 2 +- src/xrGame/ui/UIKeyBinding.cpp | 4 +- src/xrGame/ui/UIListItemServer.cpp | 2 +- src/xrGame/ui/UIMainIngameWnd.cpp | 6 +-- src/xrGame/ui/UIMap.cpp | 2 +- src/xrGame/ui/UIMapInfo.cpp | 16 +++--- src/xrGame/ui/UIMapList.cpp | 2 +- src/xrGame/ui/UIMapWnd.cpp | 6 +-- src/xrGame/ui/UIOutfitSlot.cpp | 4 +- src/xrGame/ui/UISkinSelector.cpp | 4 +- src/xrGame/ui/UIStatsPlayerInfo.cpp | 4 +- src/xrGame/ui/UIStatsPlayerList.cpp | 10 ++-- src/xrGame/ui/UITalkWnd.cpp | 2 +- src/xrGame/vision_client.cpp | 2 +- src/xrGame/xrGameSpyServer.cpp | 22 ++++---- src/xrGame/xrServer_Connect.cpp | 8 +-- src/xrGame/xrServer_process_event.cpp | 4 +- .../xrServer_process_event_activate.cpp | 2 +- src/xrGame/xrServer_process_spawn.cpp | 4 +- src/xrGame/xr_time.cpp | 4 +- src/xrGame/zone_effector.cpp | 2 +- src/xrNetServer/empty/NET_Client.cpp | 2 +- src/xrNetServer/empty/NET_Client.h | 2 +- src/xrPhysics/PHScriptCall.cpp | 4 +- src/xrServerEntities/PropertiesListTypes.h | 2 +- .../object_factory_script.cpp | 2 +- .../script_net_packet_script.cpp | 2 +- ...xrServer_Objects_ALife_Monsters_script.cpp | 2 +- .../xrServer_Objects_Alife_Smartcovers.cpp | 14 ++--- 140 files changed, 588 insertions(+), 588 deletions(-) diff --git a/src/Layers/xrRender/ModelPool.cpp b/src/Layers/xrRender/ModelPool.cpp index 678236a5492..753bb678e01 100644 --- a/src/Layers/xrRender/ModelPool.cpp +++ b/src/Layers/xrRender/ModelPool.cpp @@ -194,7 +194,7 @@ void CModelPool::Destroy() REGISTRY_IT it = Registry.begin(); dxRender_Visual* V = (dxRender_Visual*)it->first; #ifdef _DEBUG - Msg("ModelPool: Destroy object: '%s'", *V->dbg_name); + Msg("ModelPool: Destroy object: '%s'", V->dbg_name.c_str()); #endif DeleteInternal(V, TRUE); } diff --git a/src/xrCore/xrstring.h b/src/xrCore/xrstring.h index 29e0824ce51..c53123b8e31 100644 --- a/src/xrCore/xrstring.h +++ b/src/xrCore/xrstring.h @@ -138,9 +138,6 @@ class shared_str _set(nullptr); return *this; } - // XXX tamlin: Remove operator*(). It may be convenient, but it's dangerous. Use - [[nodiscard]] - pcstr operator*() const { return p_ ? p_->value : nullptr; } [[nodiscard]] bool operator!() const { return p_ == nullptr; } @@ -258,3 +255,4 @@ IC void xr_strlwr(shared_str& src) } #pragma pack(pop) +//// diff --git a/src/xrGame/Actor.cpp b/src/xrGame/Actor.cpp index a975c8f43ce..899af0eddfd 100644 --- a/src/xrGame/Actor.cpp +++ b/src/xrGame/Actor.cpp @@ -416,10 +416,10 @@ void CActor::Load(LPCSTR section) } } - sndDie[0].create(strconcat(buf, *cName(), "\\die0"), st_Effect, SOUND_TYPE_MONSTER_DYING); - sndDie[1].create(strconcat(buf, *cName(), "\\die1"), st_Effect, SOUND_TYPE_MONSTER_DYING); - sndDie[2].create(strconcat(buf, *cName(), "\\die2"), st_Effect, SOUND_TYPE_MONSTER_DYING); - sndDie[3].create(strconcat(buf, *cName(), "\\die3"), st_Effect, SOUND_TYPE_MONSTER_DYING); + sndDie[0].create(strconcat(buf, cName().c_str(), "\\die0"), st_Effect, SOUND_TYPE_MONSTER_DYING); + sndDie[1].create(strconcat(buf, cName().c_str(), "\\die1"), st_Effect, SOUND_TYPE_MONSTER_DYING); + sndDie[2].create(strconcat(buf, cName().c_str(), "\\die2"), st_Effect, SOUND_TYPE_MONSTER_DYING); + sndDie[3].create(strconcat(buf, cName().c_str(), "\\die3"), st_Effect, SOUND_TYPE_MONSTER_DYING); m_HeavyBreathSnd.create( pSettings->r_string(section, "heavy_breath_snd"), st_Effect, SOUND_TYPE_MONSTER_INJURING); @@ -2064,7 +2064,7 @@ void CActor::OnDifficultyChanged() conditions().LoadImmunities(tmp, pSettings); // hit probability strconcat(sizeof(tmp), tmp, "hit_probability_", diff_name); - m_hit_probability = pSettings->r_float(*cNameSect(), tmp); + m_hit_probability = pSettings->r_float(cNameSect().c_str(), tmp); // two hits death parameters strconcat(sizeof(tmp), tmp, "actor_thd_", diff_name); conditions().LoadTwoHitsDeathParams(tmp); diff --git a/src/xrGame/ActorCondition.cpp b/src/xrGame/ActorCondition.cpp index d35cee23df8..c6a7af5d9b5 100644 --- a/src/xrGame/ActorCondition.cpp +++ b/src/xrGame/ActorCondition.cpp @@ -248,7 +248,7 @@ void CActorCondition::UpdateCondition() { CEffectorPP* ppe = object().Cameras().GetPPEffector((EEffectorPPType)effPsyHealth); - strconcat(sizeof(pp_sect_name), pp_sect_name, "effector_psy_health", "_", *ln); + strconcat(sizeof(pp_sect_name), pp_sect_name, "effector_psy_health", "_", ln.c_str()); if (!pSettings->section_exist(pp_sect_name)) xr_strcpy(pp_sect_name, "effector_psy_health"); diff --git a/src/xrGame/Actor_Network.cpp b/src/xrGame/Actor_Network.cpp index 6c8187dd9ca..e8fc54927d6 100644 --- a/src/xrGame/Actor_Network.cpp +++ b/src/xrGame/Actor_Network.cpp @@ -873,14 +873,14 @@ void CActor::OnChangeVisual() SetCallbacks(); m_anims->Create(V); m_vehicle_anims->Create(V); - CDamageManager::reload(*cNameSect(), "damage", pSettings); + CDamageManager::reload(cNameSect().c_str(), "damage", pSettings); //------------------------------------------------------------------------------- m_head = smart_cast(Visual())->LL_BoneID("bip01_head"); m_eye_left = smart_cast(Visual())->LL_BoneID("eye_left"); m_eye_right = smart_cast(Visual())->LL_BoneID("eye_right"); - m_r_hand = smart_cast(Visual())->LL_BoneID(pSettings->r_string(*cNameSect(), "weapon_bone0")); - m_l_finger1 = smart_cast(Visual())->LL_BoneID(pSettings->r_string(*cNameSect(), "weapon_bone1")); - m_r_finger2 = smart_cast(Visual())->LL_BoneID(pSettings->r_string(*cNameSect(), "weapon_bone2")); + m_r_hand = smart_cast(Visual())->LL_BoneID(pSettings->r_string(cNameSect().c_str(), "weapon_bone0")); + m_l_finger1 = smart_cast(Visual())->LL_BoneID(pSettings->r_string(cNameSect().c_str(), "weapon_bone1")); + m_r_finger2 = smart_cast(Visual())->LL_BoneID(pSettings->r_string(cNameSect().c_str(), "weapon_bone2")); //------------------------------------------------------------------------------- m_neck = smart_cast(Visual())->LL_BoneID("bip01_neck"); m_l_clavicle = smart_cast(Visual())->LL_BoneID("bip01_l_clavicle"); @@ -1270,7 +1270,7 @@ void CActor::make_Interpolation() if (!pSyncObj) return; pSyncObj->set_State(PredictedState); //, PredictedState.enabled); - VERIFY2(_valid(renderable.xform), *cName()); + VERIFY2(_valid(renderable.xform), cName().c_str()); } else { @@ -1282,7 +1282,7 @@ void CActor::make_Interpolation() Fvector NewPos; NewPos.lerp(IStart.Pos, IEnd.Pos, factor); - VERIFY2(_valid(renderable.xform), *cName()); + VERIFY2(_valid(renderable.xform), cName().c_str()); // r_model_yaw = angle_lerp (IStart.o_model,IEnd.o_model, factor); unaffected_r_torso.yaw = angle_lerp(IStart.o_torso.yaw, IEnd.o_torso.yaw, factor); @@ -1916,8 +1916,8 @@ void CActor::OnCriticalHitHealthLoss() #ifdef DEBUG IGameObject* pLastHitter = Level().Objects.net_Find(m_iLastHitterID); - Msg("%s killed by hit from %s %s", *cName(), (pLastHitter ? *(pLastHitter->cName()) : ""), - ((pLastHittingWeapon && pLastHittingWeapon != pLastHitter) ? *(pLastHittingWeapon->cName()) : "")); + Msg("%s killed by hit from %s %s", cName().c_str(), (pLastHitter ? pLastHitter->cName().c_str() : ""), + ((pLastHittingWeapon && pLastHittingWeapon != pLastHitter) ? pLastHittingWeapon->cName().c_str() : "")); #endif //------------------------------------------------------------------- if (m_iLastHitterID != u16(-1)) @@ -2023,7 +2023,7 @@ void CActor::OnCriticalWoundHealthLoss() if (GameID() == eGameIDSingle || !OnServer()) return; #ifdef DEBUG - Msg("--- %s is bleed out", *cName()); + Msg("--- %s is bleed out", cName().c_str()); #endif // #ifdef DEBUG //------------------------------- NET_Packet P; @@ -2044,7 +2044,7 @@ void CActor::OnCriticalRadiationHealthLoss() if (GameID() == eGameIDSingle || !OnServer()) return; //------------------------------- - Msg("%s killed by radiation", *cName()); + Msg("%s killed by radiation", cName().c_str()); NET_Packet P; u_EventGen(P, GE_GAME_EVENT, ID()); P.w_u16(GAME_EVENT_PLAYER_KILLED); diff --git a/src/xrGame/ContextMenu.cpp b/src/xrGame/ContextMenu.cpp index 514bbcd0ecd..fcfa4009f91 100644 --- a/src/xrGame/ContextMenu.cpp +++ b/src/xrGame/ContextMenu.cpp @@ -22,9 +22,9 @@ void CContextMenu::Load(CInifile* INI, LPCSTR SECT) char Event[128], Param[128]; Event[0] = 0; Param[0] = 0; - sscanf(*I->second, "%[^,],%s", Event, Param); + sscanf(I->second.c_str(), "%[^,],%s", Event, Param); MenuItem Item; - Item.Name = xr_strdup(*I->first); + Item.Name = xr_strdup(I->first.c_str()); Item.Event = Engine.Event.Create(Event); Item.Param = xr_strdup(Param); Items.push_back(Item); diff --git a/src/xrGame/CustomMonster.cpp b/src/xrGame/CustomMonster.cpp index 8edb4abb4bf..b3621c08401 100644 --- a/src/xrGame/CustomMonster.cpp +++ b/src/xrGame/CustomMonster.cpp @@ -700,7 +700,7 @@ void CCustomMonster::Die(IGameObject* who) bool CCustomMonster::net_Spawn(CSE_Abstract* DC) { - memory().reload(*cNameSect()); + memory().reload(cNameSect().c_str()); memory().reinit(); if (!movement().net_Spawn(DC) || !inherited::net_Spawn(DC) || !CScriptEntity::net_Spawn(DC)) diff --git a/src/xrGame/DestroyablePhysicsObject.cpp b/src/xrGame/DestroyablePhysicsObject.cpp index f5d0a00d228..49a10a9a715 100644 --- a/src/xrGame/DestroyablePhysicsObject.cpp +++ b/src/xrGame/DestroyablePhysicsObject.cpp @@ -110,7 +110,7 @@ void CDestroyablePhysicsObject::Destroy() { m_destroy_sound.play_at_pos(this, Position()); } - if (*m_destroy_particles) + if (m_destroy_particles.c_str()) { // Fvector dir;dir.set(0,1,0); Fmatrix m; diff --git a/src/xrGame/GameObject.cpp b/src/xrGame/GameObject.cpp index 3892ee1f4f5..026802b4ff6 100644 --- a/src/xrGame/GameObject.cpp +++ b/src/xrGame/GameObject.cpp @@ -119,15 +119,15 @@ void CGameObject::cNameSect_set(shared_str N) { NameSection = N; } void CGameObject::cNameVisual_set(shared_str N) { // check if equal - if (*N && *NameVisual) + if (N.c_str() && NameVisual.c_str()) if (N == NameVisual) return; // replace model - if (*N && N[0]) + if (N.c_str() && N[0]) { IRenderVisual* old_v = renderable.visual; NameVisual = N; - renderable.visual = GEnv.Render->model_Create(*N); + renderable.visual = GEnv.Render->model_Create(N.c_str()); IKinematics* old_k = old_v ? old_v->dcast_PKinematics() : NULL; IKinematics* new_k = renderable.visual->dcast_PKinematics(); /* @@ -154,7 +154,7 @@ void CGameObject::cNameVisual_set(shared_str N) // flagging void CGameObject::processing_activate() { - VERIFY3(255 != Props.bActiveCounter, "Invalid sequence of processing enable/disable calls: overflow", *cName()); + VERIFY3(255 != Props.bActiveCounter, "Invalid sequence of processing enable/disable calls: overflow", cName().c_str()); Props.bActiveCounter++; if (!(Props.bActiveCounter - 1)) g_pGameLevel->Objects.o_activate(this); @@ -162,7 +162,7 @@ void CGameObject::processing_activate() void CGameObject::processing_deactivate() { - VERIFY3(Props.bActiveCounter, "Invalid sequence of processing enable/disable calls: underflow", *cName()); + VERIFY3(Props.bActiveCounter, "Invalid sequence of processing enable/disable calls: underflow", cName().c_str()); Props.bActiveCounter--; if (!Props.bActiveCounter) g_pGameLevel->Objects.o_sleep(this); @@ -201,19 +201,19 @@ void CGameObject::setVisible(bool _visible) void CGameObject::Center(Fvector& C) const { - VERIFY2(renderable.visual, *cName()); + VERIFY2(renderable.visual, cName().c_str()); renderable.xform.transform_tiny(C, renderable.visual->getVisData().sphere.P); } float CGameObject::Radius() const { - VERIFY2(renderable.visual, *cName()); + VERIFY2(renderable.visual, cName().c_str()); return renderable.visual->getVisData().sphere.R; } const Fbox& CGameObject::BoundingBox() const { - VERIFY2(renderable.visual, *cName()); + VERIFY2(renderable.visual, cName().c_str()); return renderable.visual->getVisData().box; } @@ -274,7 +274,7 @@ void CGameObject::net_Destroy() { #ifdef DEBUG if (psAI_Flags.test(aiDestroy)) - Msg("Destroying client object [%d][%s][%x]", ID(), *cName(), this); + Msg("Destroying client object [%d][%s][%x]", ID(), cName().c_str(), this); #endif VERIFY(m_spawned); @@ -471,7 +471,7 @@ bool CGameObject::net_Spawn(CSE_Abstract* DC) XFORM().setXYZ(E->o_Angle); Position().set(E->o_Position); #ifdef DEBUG - if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), *cName()) == 0) + if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), cName().c_str()) == 0) { Msg("CGameObject::net_Spawn obj %s Position set from CSE_Abstract %f,%f,%f", PH_DBG_ObjectTrackName(), Position().x, Position().y, Position().z); @@ -484,7 +484,7 @@ bool CGameObject::net_Spawn(CSE_Abstract* DC) { #pragma warning(push) #pragma warning(disable : 4238) - IReader reader((void*)(*(O->m_ini_string)), O->m_ini_string.size()); + IReader reader((void*)((O->m_ini_string).c_str()), O->m_ini_string.size()); m_ini_file = xr_new(&reader, FS.get_path("$game_config$")->m_Path); #pragma warning(pop) } @@ -517,15 +517,15 @@ bool CGameObject::net_Spawn(CSE_Abstract* DC) spatial.type = (spatial.type | STYPE_VISIBLEFORAI) ^ STYPE_VISIBLEFORAI; } - reload(*cNameSect()); + reload(cNameSect().c_str()); if (!GEnv.isDedicatedServer) - scriptBinder.reload(*cNameSect()); + scriptBinder.reload(cNameSect().c_str()); reinit(); if (!GEnv.isDedicatedServer) scriptBinder.reinit(); #ifdef DEBUG - if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), *cName()) == 0) + if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), cName().c_str()) == 0) { Msg("CGameObject::net_Spawn obj %s After Script Binder reinit %f,%f,%f", PH_DBG_ObjectTrackName(), Position().x, Position().y, Position().z); @@ -588,7 +588,7 @@ bool CGameObject::net_Spawn(CSE_Abstract* DC) { if (pSettings->line_exist(cNameSect(), "cform")) { - VERIFY3(*NameVisual, "Model isn't assigned for object, but cform requisted", *cName()); + VERIFY3(NameVisual.c_str(), "Model isn't assigned for object, but cform requisted", cName().c_str()); CForm = xr_new(this); } } @@ -604,7 +604,7 @@ bool CGameObject::net_Spawn(CSE_Abstract* DC) m_bObjectRemoved = false; spawn_supplies(); #ifdef DEBUG - if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), *cName()) == 0) + if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), cName().c_str()) == 0) { Msg("CGameObject::net_Spawn obj %s Before CScriptBinder::net_Spawn %f,%f,%f", PH_DBG_ObjectTrackName(), Position().x, Position().y, Position().z); @@ -615,7 +615,7 @@ bool CGameObject::net_Spawn(CSE_Abstract* DC) #endif #ifdef DEBUG - if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), *cName()) == 0) + if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), cName().c_str()) == 0) { Msg("CGameObject::net_Spawn obj %s Before CScriptBinder::net_Spawn %f,%f,%f", PH_DBG_ObjectTrackName(), Position().x, Position().y, Position().z); @@ -634,7 +634,7 @@ void CGameObject::net_Save(NET_Packet& net_packet) #ifdef DEBUG if (psAI_Flags.test(aiSerialize)) { - Msg(">> **** Save script object [%s] *****", *cName()); + Msg(">> **** Save script object [%s] *****", cName().c_str()); Msg(">> Before save :: packet position = [%u]", net_packet.w_tell()); } @@ -663,7 +663,7 @@ void CGameObject::net_Load(IReader& ireader) #ifdef DEBUG if (psAI_Flags.test(aiSerialize)) { - Msg(">> **** Load script object [%s] *****", *cName()); + Msg(">> **** Load script object [%s] *****", cName().c_str()); Msg(">> Before load :: reader position = [%i]", ireader.tell()); } @@ -680,7 +680,7 @@ void CGameObject::net_Load(IReader& ireader) #endif // ---------------------------------------------------------- #ifdef DEBUG - if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), *cName()) == 0) + if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), cName().c_str()) == 0) { Msg("CGameObject::net_Load obj %s (loaded) %f,%f,%f", PH_DBG_ObjectTrackName(), Position().x, Position().y, Position().z); @@ -1201,7 +1201,7 @@ void CGameObject::shedule_Update(u32 dt) bool CGameObject::net_SaveRelevant() { return scriptBinder.net_SaveRelevant(); } //игровое имя объекта -LPCSTR CGameObject::Name() const { return (*cName()); } +LPCSTR CGameObject::Name() const { return (cName().c_str()); } u32 CGameObject::ef_creature_type() const { string16 temp; @@ -1358,14 +1358,14 @@ void CGameObject::UpdateCL() // IGameObject::UpdateCL(); // consistency check #ifdef DEBUG - VERIFY2(_valid(renderable.xform), *cName()); + VERIFY2(_valid(renderable.xform), cName().c_str()); if (Device.dwFrame == dbg_update_cl) - xrDebug::Fatal(DEBUG_INFO, "'UpdateCL' called twice per frame for %s", *cName()); + xrDebug::Fatal(DEBUG_INFO, "'UpdateCL' called twice per frame for %s", cName().c_str()); dbg_update_cl = Device.dwFrame; if (Parent && spatial.node_ptr) - xrDebug::Fatal(DEBUG_INFO, "Object %s has parent but is still registered inside spatial DB", *cName()); + xrDebug::Fatal(DEBUG_INFO, "Object %s has parent but is still registered inside spatial DB", cName().c_str()); if (!CForm && (spatial.type & STYPE_COLLIDEABLE)) - xrDebug::Fatal(DEBUG_INFO, "Object %s registered as 'collidable' but has no collidable model", *cName()); + xrDebug::Fatal(DEBUG_INFO, "Object %s registered as 'collidable' but has no collidable model", cName().c_str()); #endif spatial_update(base_spu_epsP * 5, base_spu_epsR * 5); // crow @@ -1522,7 +1522,7 @@ bool CGameObject::use(IGameObject* obj) return true; } -LPCSTR CGameObject::tip_text() { return *m_sTipText; } +LPCSTR CGameObject::tip_text() { return m_sTipText.c_str(); } void CGameObject::set_tip_text(LPCSTR new_text) { m_sTipText = new_text; } void CGameObject::set_tip_text_default() { m_sTipText = nullptr; } bool CGameObject::nonscript_usable() { return m_bNonscriptUsable; } diff --git a/src/xrGame/GameTask.cpp b/src/xrGame/GameTask.cpp index 967fe321c9c..8441bb9f76c 100644 --- a/src/xrGame/GameTask.cpp +++ b/src/xrGame/GameTask.cpp @@ -84,7 +84,7 @@ void CGameTask::Load(const TASK_ID& id) return; } - const XML_NODE task_node = gameTaskXml.NavigateToNodeWithAttribute("game_task", "id", *id); + const XML_NODE task_node = gameTaskXml.NavigateToNodeWithAttribute("game_task", "id", id.c_str()); THROW3(task_node, "game task id = ", id.c_str()); gameTaskXml.SetLocalRoot(task_node); @@ -128,11 +128,11 @@ void CGameTask::Load(const TASK_ID& id) { objective.m_icon_texture_name = gameTaskXml.Read(gameTaskXml.GetLocalRoot(), "icon", 0, nullptr); if (objective.m_icon_texture_name.size() && - 0 != xr_stricmp(*objective.m_icon_texture_name, "ui\\ui_icons_task")) + 0 != xr_stricmp(objective.m_icon_texture_name.c_str(), "ui\\ui_icons_task")) { - objective.m_icon_rect = CUITextureMaster::GetTextureRect(*objective.m_icon_texture_name); + objective.m_icon_rect = CUITextureMaster::GetTextureRect(objective.m_icon_texture_name.c_str()); objective.m_icon_rect.rb.sub(objective.m_icon_rect.rb, objective.m_icon_rect.lt); - objective.m_icon_texture_name = CUITextureMaster::GetTextureFileName(*objective.m_icon_texture_name); + objective.m_icon_texture_name = CUITextureMaster::GetTextureFileName(objective.m_icon_texture_name.c_str()); } else if (objective.m_icon_texture_name.size()) { @@ -673,9 +673,9 @@ void SScriptTaskHelper::init_functors(xr_vector& v_src, task_state_f for (u32 idx = 0; it != it_e; ++it, ++idx) { - const bool functor_exists = GEnv.ScriptEngine->functor(*(*it), v_dest[idx]); + const bool functor_exists = GEnv.ScriptEngine->functor(it->c_str(), v_dest[idx]); if (!functor_exists) - Log("Cannot find script function described in task objective ", *(*it)); + Log("Cannot find script function described in task objective ", it->c_str()); } } diff --git a/src/xrGame/GametaskManager.cpp b/src/xrGame/GametaskManager.cpp index db04ae4a1da..2bce9e42e19 100644 --- a/src/xrGame/GametaskManager.cpp +++ b/src/xrGame/GametaskManager.cpp @@ -176,7 +176,7 @@ void CGameTaskManager::SetTaskState(const TASK_ID& id, ETaskState state, TASK_OB CGameTask* t = HasGameTask(id, objectiveSpecified); if (NULL == t) { - Msg("! actor does not has task [%s]%s", *id, objectiveSpecified ? "" : " or it is completed"); + Msg("! actor does not has task [%s]%s", id.c_str(), objectiveSpecified ? "" : " or it is completed"); return; } SetTaskState(t, state, objective_id); diff --git a/src/xrGame/HangingLamp.cpp b/src/xrGame/HangingLamp.cpp index fa0102810fa..72b2c5091d2 100644 --- a/src/xrGame/HangingLamp.cpp +++ b/src/xrGame/HangingLamp.cpp @@ -83,9 +83,9 @@ bool CHangingLamp::net_Spawn(CSE_Abstract* DC) { IKinematics* K = smart_cast(Visual()); R_ASSERT(Visual() && smart_cast(Visual())); - light_bone = K->LL_BoneID(*lamp->light_main_bone); + light_bone = K->LL_BoneID(lamp->light_main_bone.c_str()); VERIFY(light_bone != BI_NONE); - ambient_bone = K->LL_BoneID(*lamp->light_ambient_bone); + ambient_bone = K->LL_BoneID(lamp->light_ambient_bone.c_str()); VERIFY(ambient_bone != BI_NONE); CForm = xr_new(this); } @@ -103,7 +103,7 @@ bool CHangingLamp::net_Spawn(CSE_Abstract* DC) light_render->set_virtual_size(lamp->m_virtual_size); light_render->set_color(clr); light_render->set_cone(lamp->spot_cone_angle); - light_render->set_texture(*lamp->light_texture); + light_render->set_texture(lamp->light_texture.c_str()); light_render->set_volumetric_quality(lamp->m_volumetric_quality); light_render->set_volumetric_intensity(lamp->m_volumetric_intensity); @@ -112,7 +112,7 @@ bool CHangingLamp::net_Spawn(CSE_Abstract* DC) if (lamp->glow_texture.size()) { glow_render = GEnv.Render->glow_create(); - glow_render->set_texture(*lamp->glow_texture); + glow_render->set_texture(lamp->glow_texture.c_str()); glow_render->set_color(clr); glow_render->set_radius(lamp->glow_radius); } @@ -126,12 +126,12 @@ bool CHangingLamp::net_Spawn(CSE_Abstract* DC) clr.mul_rgb(ambient_power); light_ambient->set_range(lamp->m_ambient_radius); light_ambient->set_color(clr); - light_ambient->set_texture(*lamp->m_ambient_texture); + light_ambient->set_texture(lamp->m_ambient_texture.c_str()); } fHealth = lamp->m_health; - lanim = LALib.FindItem(*lamp->color_animator); + lanim = LALib.FindItem(lamp->color_animator.c_str()); CPHSkeleton::Spawn(e); if (smart_cast(Visual())) @@ -143,7 +143,7 @@ bool CHangingLamp::net_Spawn(CSE_Abstract* DC) //.intepolate_pos } if (lamp->flags.is(CSE_ALifeObjectHangingLamp::flPhysic) && !Visual()) - Msg("! WARNING: lamp, obj name [%s],flag physics set, but has no visual", *cName()); + Msg("! WARNING: lamp, obj name [%s],flag physics set, but has no visual", cName().c_str()); //. if (lamp->flags.is(CSE_ALifeObjectHangingLamp::flPhysic)&&Visual()&&!guid_physic_bone) fHealth=0.f; if (Alive() && m_bState) TurnOn(); @@ -358,7 +358,7 @@ void CHangingLamp::CreateBody(CSE_ALifeObjectHangingLamp* lamp) m_pPhysicsShell = P_create_Shell(); bone_map.clear(); - LPCSTR fixed_bones = *lamp->fixed_bones; + LPCSTR fixed_bones = lamp->fixed_bones.c_str(); if (fixed_bones) { int count = _GetItemCount(fixed_bones); diff --git a/src/xrGame/Helicopter.cpp b/src/xrGame/Helicopter.cpp index b4d52c0583e..bb4153c7da5 100644 --- a/src/xrGame/Helicopter.cpp +++ b/src/xrGame/Helicopter.cpp @@ -94,7 +94,7 @@ void CHelicopter::Load(LPCSTR section) UseFireTrail(m_enemy.bUseFireTrail); // temp force reloar disp params m_sAmmoType = pSettings->r_string(section, "ammo_class"); - m_CurrentAmmo.Load(*m_sAmmoType, 0); + m_CurrentAmmo.Load(m_sAmmoType.c_str(), 0); m_sRocketSection = pSettings->r_string(section, "rocket_class"); @@ -143,7 +143,7 @@ bool CHelicopter::net_Spawn(CSE_Abstract* DC) CPHSkeleton::Spawn((CSE_Abstract*)(DC)); for (u32 i = 0; i < 4; ++i) - CRocketLauncher::SpawnRocket(*m_sRocketSection, smart_cast(this)); + CRocketLauncher::SpawnRocket(m_sRocketSection.c_str(), smart_cast(this)); // assigning m_animator here CSE_Abstract* abstract = (CSE_Abstract*)(DC); @@ -206,11 +206,11 @@ bool CHelicopter::net_Spawn(CSE_Abstract* DC) IKinematicsAnimated* A = smart_cast(Visual()); if (A) { - A->PlayCycle(*heli->startup_animation); + A->PlayCycle(heli->startup_animation.c_str()); K->CalculateBones(TRUE); } - m_engineSound.create(*heli->engine_sound, st_Effect, sg_SourceType); + m_engineSound.create(heli->engine_sound.c_str(), st_Effect, sg_SourceType); m_engineSound.play_at_pos(0, XFORM().c, sm_Looped); if (m_bLightShotEnabled) @@ -440,7 +440,7 @@ void CHelicopter::shedule_Update(u32 time_delta) if (state() != CHelicopter::eDead) { for (u32 i = getRocketCount(); i < 4; ++i) - CRocketLauncher::SpawnRocket(*m_sRocketSection, this); + CRocketLauncher::SpawnRocket(m_sRocketSection.c_str(), this); } if (m_ready_explode) ExplodeHelicopter(); diff --git a/src/xrGame/Helicopter2.cpp b/src/xrGame/Helicopter2.cpp index ca39bc59377..cf28e97801e 100644 --- a/src/xrGame/Helicopter2.cpp +++ b/src/xrGame/Helicopter2.cpp @@ -46,7 +46,7 @@ void CHelicopter::StartFlame() { if (m_pParticle) return; - m_pParticle = CParticlesObject::Create(*m_smoke_particle, FALSE); + m_pParticle = CParticlesObject::Create(m_smoke_particle.c_str(), FALSE); Fvector zero_vector; zero_vector.set(0.f, 0.f, 0.f); @@ -250,7 +250,7 @@ void CHelicopter::DieHelicopter() m_engineSound.stop(); - m_brokenSound.create(pSettings->r_string(*cNameSect(), "broken_snd"), st_Effect, sg_SourceType); + m_brokenSound.create(pSettings->r_string(cNameSect().c_str(), "broken_snd"), st_Effect, sg_SourceType); m_brokenSound.play_at_pos(0, XFORM().c, sm_Looped); IKinematics* K = smart_cast(Visual()); @@ -260,9 +260,9 @@ void CHelicopter::DieHelicopter() LPCSTR bone; u16 bone_id; - for (u32 i = 0, n = _GetItemCount(*m_death_bones_to_hide); i < n; ++i) + for (u32 i = 0, n = _GetItemCount(m_death_bones_to_hide.c_str()); i < n; ++i) { - bone = _GetItem(*m_death_bones_to_hide, i, I); + bone = _GetItem(m_death_bones_to_hide.c_str(), i, I); bone_id = K->LL_BoneID(bone); K->LL_SetBoneVisible(bone_id, FALSE, TRUE); } @@ -352,12 +352,12 @@ void CHelicopter::UseFireTrail(bool val) m_enemy.bUseFireTrail = val; if (val) { - fireDispersionBase = pSettings->r_float(*cNameSect(), "fire_dispersion_null"); + fireDispersionBase = pSettings->r_float(cNameSect().c_str(), "fire_dispersion_null"); fireDispersionBase = deg2rad(fireDispersionBase); } else { - fireDispersionBase = pSettings->r_float(*cNameSect(), "fire_dispersion_base"); + fireDispersionBase = pSettings->r_float(cNameSect().c_str(), "fire_dispersion_base"); fireDispersionBase = deg2rad(fireDispersionBase); } } diff --git a/src/xrGame/InfoPortion.cpp b/src/xrGame/InfoPortion.cpp index 17da1414e36..820fd35ca49 100644 --- a/src/xrGame/InfoPortion.cpp +++ b/src/xrGame/InfoPortion.cpp @@ -41,7 +41,7 @@ void CInfoPortion::load_shared(LPCSTR) //loading from XML const XML_NODE pNode = pXML->NavigateToNode(tag_name, item_data->pos_in_file); - THROW3(pNode, "info_portion id=", *item_data->id); + THROW3(pNode, "info_portion id=", item_data->id.c_str()); //список названий диалогов const int dialogs_num = pXML->GetNodesNum(pNode, "dialog"); diff --git a/src/xrGame/Inventory.cpp b/src/xrGame/Inventory.cpp index ff39a1707e8..033a4119455 100644 --- a/src/xrGame/Inventory.cpp +++ b/src/xrGame/Inventory.cpp @@ -150,7 +150,7 @@ void CInventory::Take(CGameObject* pObj, bool bNotActivate, bool strict_placemen pIItem->m_ItemCurrPlace.type = eItemPlaceUndefined; #ifdef DEBUG if (!result) - Msg("cant put in belt item %s", *pIItem->object().cName()); + Msg("cant put in belt item %s", pIItem->object().cName().c_str()); #endif break; @@ -160,7 +160,7 @@ void CInventory::Take(CGameObject* pObj, bool bNotActivate, bool strict_placemen pIItem->m_ItemCurrPlace.type = eItemPlaceUndefined; #ifdef DEBUG if (!result) - Msg("cant put in ruck item %s", *pIItem->object().cName()); + Msg("cant put in ruck item %s", pIItem->object().cName().c_str()); #endif break; @@ -170,7 +170,7 @@ void CInventory::Take(CGameObject* pObj, bool bNotActivate, bool strict_placemen pIItem->m_ItemCurrPlace.type = eItemPlaceUndefined; #ifdef DEBUG if (!result) - Msg("cant slot in slot item %s", *pIItem->object().cName()); + Msg("cant slot in slot item %s", pIItem->object().cName().c_str()); #endif break; } @@ -1268,7 +1268,7 @@ bool CInventory::CanTakeItem(CInventoryItem* inventory_item) const for (it = m_all.begin(); it != m_all.end(); ++it) if ((*it)->object().ID() == inventory_item->object().ID()) break; - VERIFY3(it == m_all.end(), "item already exists in inventory", *inventory_item->object().cName()); + VERIFY3(it == m_all.end(), "item already exists in inventory", inventory_item->object().cName().c_str()); CActor* pActor = smart_cast(m_pOwner); //актер всегда может взять вещь diff --git a/src/xrGame/InventoryOwner.cpp b/src/xrGame/InventoryOwner.cpp index 4694577a72e..b080fd3cad4 100644 --- a/src/xrGame/InventoryOwner.cpp +++ b/src/xrGame/InventoryOwner.cpp @@ -138,8 +138,8 @@ bool CInventoryOwner::net_Spawn(CSE_Abstract* DC) CharacterInfo().m_SpecificCharacter.Load("mp_actor"); CharacterInfo().InitSpecificCharacter("mp_actor"); CharacterInfo().m_SpecificCharacter.data()->m_sGameName = - (E->name_replace()[0]) ? E->name_replace() : *pThis->cName(); - m_game_name = (E->name_replace()[0]) ? E->name_replace() : *pThis->cName(); + (E->name_replace()[0]) ? E->name_replace() : pThis->cName().c_str(); + m_game_name = (E->name_replace()[0]) ? E->name_replace() : pThis->cName().c_str(); } if (!pThis->Local()) diff --git a/src/xrGame/Level.cpp b/src/xrGame/Level.cpp index 08857070f7d..fef6a22ec36 100644 --- a/src/xrGame/Level.cpp +++ b/src/xrGame/Level.cpp @@ -390,7 +390,7 @@ void CLevel::MakeReconnect() char const* client_options = nullptr; if (m_caServerOptions.c_str()) { - server_options = xr_strdup(*m_caServerOptions); + server_options = xr_strdup(m_caServerOptions.c_str()); } else { @@ -398,7 +398,7 @@ void CLevel::MakeReconnect() } if (m_caClientOptions.c_str()) { - client_options = xr_strdup(*m_caClientOptions); + client_options = xr_strdup(m_caClientOptions.c_str()); } else { diff --git a/src/xrGame/Level_network.cpp b/src/xrGame/Level_network.cpp index c6f8664726f..500ae3def86 100644 --- a/src/xrGame/Level_network.cpp +++ b/src/xrGame/Level_network.cpp @@ -252,7 +252,7 @@ u32 CLevel::Objects_net_Save(NET_Packet* _Packet, u32 start, u32 max_object_size if (size >= 65536) { xrDebug::Fatal(DEBUG_INFO, "Object [%s][%d] exceed network-data limit\n size=%d, Pend=%d, Pstart=%d", - *P->cName(), P->ID(), size, Packet.w_tell(), position); + P->cName().c_str(), P->ID(), size, Packet.w_tell(), position); } #endif Packet.w_chunk_close16(position); @@ -545,7 +545,7 @@ void CLevel::ClearAllObjects() ParentFound = true; //------------------------------------------------------------- #ifdef DEBUG - Msg("Rejection of %s[%d] from %s[%d]", *(pObj->cNameSect()), pObj->ID(), *(pObj->H_Parent()->cNameSect()), + Msg("Rejection of %s[%d] from %s[%d]", pObj->cNameSect().c_str(), pObj->ID(), pObj->H_Parent()->cNameSect().c_str(), pObj->H_Parent()->ID()); #endif }; @@ -583,7 +583,7 @@ void CLevel::ClearAllObjects() ParentFound = true; //------------------------------------------------------------- #ifdef DEBUG - Msg("Destruction of %s[%d]", *(pObj->cNameSect()), pObj->ID()); + Msg("Destruction of %s[%d]", pObj->cNameSect().c_str(), pObj->ID()); #endif }; ProcessGameEvents(); @@ -623,10 +623,10 @@ void CLevel::net_OnChangeSelfName(NET_Packet* P) return; string64 NewName; P->r_stringZ(NewName); - if (!strstr(*m_caClientOptions, "/name=")) + if (!strstr(m_caClientOptions.c_str(), "/name=")) { string1024 tmpstr; - xr_strcpy(tmpstr, *m_caClientOptions); + xr_strcpy(tmpstr, m_caClientOptions.c_str()); xr_strcat(tmpstr, "/name="); xr_strcat(tmpstr, NewName); m_caClientOptions = tmpstr; @@ -634,10 +634,10 @@ void CLevel::net_OnChangeSelfName(NET_Packet* P) else { string1024 tmpstr; - xr_strcpy(tmpstr, *m_caClientOptions); + xr_strcpy(tmpstr, m_caClientOptions.c_str()); *(strstr(tmpstr, "name=") + 5) = 0; xr_strcat(tmpstr, NewName); - pcstr ptmp = strchr(strstr(*m_caClientOptions, "name="), '/'); + pcstr ptmp = strchr(strstr(m_caClientOptions.c_str(), "name="), '/'); if (ptmp) xr_strcat(tmpstr, ptmp); m_caClientOptions = tmpstr; diff --git a/src/xrGame/Level_network_spawn.cpp b/src/xrGame/Level_network_spawn.cpp index c6ebc36599e..9627dd57d2d 100644 --- a/src/xrGame/Level_network_spawn.cpp +++ b/src/xrGame/Level_network_spawn.cpp @@ -18,8 +18,8 @@ void CLevel::cl_Process_Spawn(NET_Packet& P) P.r_stringZ(s_name); // Create DC (xrSE) - CSE_Abstract* E = F_entity_Create(*s_name); - R_ASSERT2(E, *s_name); + CSE_Abstract* E = F_entity_Create(s_name.c_str()); + R_ASSERT2(E, s_name.c_str()); E->Spawn_Read(P); if (E->s_flags.is(M_SPAWN_UPDATE)) @@ -102,7 +102,7 @@ void CLevel::g_sv_Spawn(CSE_Abstract* E) // Client spawn // T.Start (); - IGameObject* O = Objects.Create(*E->s_name); + IGameObject* O = Objects.Create(E->s_name.c_str()); // Msg ("--spawn--CREATE: %f ms",1000.f*T.GetAsync()); // T.Start (); @@ -112,7 +112,7 @@ void CLevel::g_sv_Spawn(CSE_Abstract* E) if (!GEnv.isDedicatedServer) client_spawn_manager().clear(O->ID()); Objects.Destroy(O); - Msg("! Failed to spawn entity '%s'", *E->s_name); + Msg("! Failed to spawn entity '%s'", E->s_name.c_str()); } else { diff --git a/src/xrGame/PhraseDialog.cpp b/src/xrGame/PhraseDialog.cpp index c697c0e301c..4606c701446 100644 --- a/src/xrGame/PhraseDialog.cpp +++ b/src/xrGame/PhraseDialog.cpp @@ -92,7 +92,7 @@ bool CPhraseDialog::SayPhrase(DIALOG_SHARED_PTR& phrase_dialog, const shared_str //вызвать скриптовую присоединенную функцию //активируется после сказанной фразы //первый параметр - тот кто говорит фразу, второй - тот кто слушает - last_phrase->GetScriptHelper()->Action(pSpeakerGO1, pSpeakerGO2, *phrase_dialog->m_DialogId, phrase_id.c_str()); + last_phrase->GetScriptHelper()->Action(pSpeakerGO1, pSpeakerGO2, phrase_dialog->m_DialogId.c_str(), phrase_id.c_str()); //больше нет фраз, чтоб говорить phrase_dialog->m_PhraseVector.clear(); @@ -111,7 +111,7 @@ bool CPhraseDialog::SayPhrase(DIALOG_SHARED_PTR& phrase_dialog, const shared_str THROW(next_phrase_vertex); shared_str next_phrase_id = next_phrase_vertex->vertex_id(); if (next_phrase_vertex->data()->GetScriptHelper()->Precondition( - pSpeakerGO2, pSpeakerGO1, *phrase_dialog->m_DialogId, phrase_id.c_str(), next_phrase_id.c_str())) + pSpeakerGO2, pSpeakerGO1, phrase_dialog->m_DialogId.c_str(), phrase_id.c_str(), next_phrase_id.c_str())) { phrase_dialog->m_PhraseVector.push_back(next_phrase_vertex->data()); #ifdef DEBUG @@ -127,7 +127,7 @@ bool CPhraseDialog::SayPhrase(DIALOG_SHARED_PTR& phrase_dialog, const shared_str } R_ASSERT2(!phrase_dialog->m_PhraseVector.empty(), - make_string("No available phrase to say, dialog[%s]", *phrase_dialog->m_DialogId)); + make_string("No available phrase to say, dialog[%s]", phrase_dialog->m_DialogId.c_str())); //упорядочить списко по убыванию благосклонности std::sort(phrase_dialog->m_PhraseVector.begin(), phrase_dialog->m_PhraseVector.end(), PhraseGoodwillPred); @@ -184,7 +184,7 @@ LPCSTR CPhraseDialog::GetPhraseText(const shared_str& phrase_id, bool current_sp ph->GetText(), pSpeakerGO1, pSpeakerGO2, m_DialogId.c_str(), phrase_id.c_str()); } -LPCSTR CPhraseDialog::DialogCaption() { return data()->m_sCaption.size() ? *data()->m_sCaption : GetPhraseText("0"); } +LPCSTR CPhraseDialog::DialogCaption() { return data()->m_sCaption.size() ? data()->m_sCaption.c_str() : GetPhraseText("0"); } int CPhraseDialog::Priority() { return data()->m_iPriority; } void CPhraseDialog::Load(shared_str dialog_id) { @@ -204,7 +204,7 @@ void CPhraseDialog::load_shared(LPCSTR) // loading from XML XML_NODE dialog_node = pXML->NavigateToNode(id_to_index::tag_name, item_data.pos_in_file); - THROW3(dialog_node, "dialog id=", *item_data.id); + THROW3(dialog_node, "dialog id=", item_data.id.c_str()); pXML->SetLocalRoot(dialog_node); @@ -232,13 +232,13 @@ void CPhraseDialog::load_shared(LPCSTR) } [[maybe_unused]] int phrase_num = pXML->GetNodesNum(phrase_list_node, "phrase"); - THROW3(phrase_num, "dialog %s has no phrases at all", *item_data.id); + THROW3(phrase_num, "dialog %s has no phrases at all", item_data.id.c_str()); pXML->SetLocalRoot(phrase_list_node); #ifdef DEBUG // debug & mixed LPCSTR wrong_phrase_id = pXML->CheckUniqueAttrib(phrase_list_node, "phrase", "id"); - THROW3(wrong_phrase_id == NULL, *item_data.id, wrong_phrase_id); + THROW3(wrong_phrase_id == NULL, item_data.id.c_str(), wrong_phrase_id); #endif //ищем стартовую фразу diff --git a/src/xrGame/PhraseScript.cpp b/src/xrGame/PhraseScript.cpp index db2da6be5e1..8d7480a1ded 100644 --- a/src/xrGame/PhraseScript.cpp +++ b/src/xrGame/PhraseScript.cpp @@ -45,7 +45,7 @@ bool CDialogScriptHelper::CheckInfo(const CInventoryOwner* pOwner) const { #ifdef DEBUG if (psAI_Flags.test(aiDialogs)) - Msg("----rejected: [%s] has info %s", pOwner->Name(), *m_HasInfo[i]); + Msg("----rejected: [%s] has info %s", pOwner->Name(), m_HasInfo[i].c_str()); #endif return false; } @@ -57,7 +57,7 @@ bool CDialogScriptHelper::CheckInfo(const CInventoryOwner* pOwner) const { #ifdef DEBUG if (psAI_Flags.test(aiDialogs)) - Msg("----rejected: [%s] dont has info %s", pOwner->Name(), *m_DontHasInfo[i]); + Msg("----rejected: [%s] dont has info %s", pOwner->Name(), m_DontHasInfo[i].c_str()); #endif return false; } @@ -107,9 +107,9 @@ bool CDialogScriptHelper::Precondition(const CGameObject* pSpeakerGO, LPCSTR dia for (u32 i = 0; i < Preconditions().size(); ++i) { luabind::functor lua_function; - THROW(*Preconditions()[i]); - [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(*Preconditions()[i], lua_function); - THROW3(functor_exists, "Cannot find precondition", *Preconditions()[i]); + THROW(Preconditions()[i].c_str()); + [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(Preconditions()[i].c_str(), lua_function); + THROW3(functor_exists, "Cannot find precondition", Preconditions()[i].c_str()); predicate_result = lua_function(pSpeakerGO->lua_game_object()); if (!predicate_result) { @@ -128,9 +128,9 @@ void CDialogScriptHelper::Action(const CGameObject* pSpeakerGO, LPCSTR dialog_id for (u32 i = 0; i < Actions().size(); ++i) { luabind::functor lua_function; - THROW(*Actions()[i]); - [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(*Actions()[i], lua_function); - THROW3(functor_exists, "Cannot find phrase dialog script function", *Actions()[i]); + THROW(Actions()[i].c_str()); + [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(Actions()[i].c_str(), lua_function); + THROW3(functor_exists, "Cannot find phrase dialog script function", Actions()[i].c_str()); lua_function(pSpeakerGO->lua_game_object(), dialog_id); } TransferInfo(smart_cast(pSpeakerGO)); @@ -152,9 +152,9 @@ bool CDialogScriptHelper::Precondition(const CGameObject* pSpeakerGO1, const CGa for (u32 i = 0; i < Preconditions().size(); ++i) { luabind::functor lua_function; - THROW(*Preconditions()[i]); - [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(*Preconditions()[i], lua_function); - THROW3(functor_exists, "Cannot find phrase precondition", *Preconditions()[i]); + THROW(Preconditions()[i].c_str()); + [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(Preconditions()[i].c_str(), lua_function); + THROW3(functor_exists, "Cannot find phrase precondition", Preconditions()[i].c_str()); predicate_result = lua_function( pSpeakerGO1->lua_game_object(), pSpeakerGO2->lua_game_object(), dialog_id, phrase_id, next_phrase_id); if (!predicate_result) @@ -177,9 +177,9 @@ void CDialogScriptHelper::Action( for (u32 i = 0; i < Actions().size(); ++i) { luabind::functor lua_function; - THROW(*Actions()[i]); - [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(*Actions()[i], lua_function); - THROW3(functor_exists, "Cannot find phrase dialog script function", *Actions()[i]); + THROW(Actions()[i].c_str()); + [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(Actions()[i].c_str(), lua_function); + THROW3(functor_exists, "Cannot find phrase dialog script function", Actions()[i].c_str()); try { lua_function(pSpeakerGO1->lua_game_object(), pSpeakerGO2->lua_game_object(), dialog_id, phrase_id); diff --git a/src/xrGame/PhysicObject.cpp b/src/xrGame/PhysicObject.cpp index d7f8e08e27e..94fa6a0704c 100644 --- a/src/xrGame/PhysicObject.cpp +++ b/src/xrGame/PhysicObject.cpp @@ -194,7 +194,7 @@ void CPhysicObject::RunStartupAnim(CSE_Abstract* D) { CSE_Visual* visual = smart_cast(D); R_ASSERT(visual); - R_ASSERT2(*visual->startup_animation, "no startup animation"); + R_ASSERT2(visual->startup_animation.c_str(), "no startup animation"); VERIFY2((!!PKinematicsAnimated->LL_MotionID(visual->startup_animation.c_str()).valid()), (make_string(" animation %s not faund ", visual->startup_animation.c_str()) + @@ -296,7 +296,7 @@ void CPhysicObject::CreateSkeleton(CSE_ALifeObjectPhysic* po) return; if (!Visual()) return; - LPCSTR fixed_bones = *po->fixed_bones; + LPCSTR fixed_bones = po->fixed_bones.c_str(); m_pPhysicsShell = P_build_Shell(this, !po->_flags.test(CSE_PHSkeleton::flActive), fixed_bones); ApplySpawnIniToPhysicShell(&po->spawn_ini(), m_pPhysicsShell, fixed_bones[0] != '\0'); ApplySpawnIniToPhysicShell( diff --git a/src/xrGame/WeaponMagazined.cpp b/src/xrGame/WeaponMagazined.cpp index 61cfcf36730..19c01ca8839 100644 --- a/src/xrGame/WeaponMagazined.cpp +++ b/src/xrGame/WeaponMagazined.cpp @@ -285,7 +285,7 @@ void CWeaponMagazined::UnloadMagazine(bool spawn_ammo) xr_map::iterator l_it; for (l_it = l_ammo.begin(); l_ammo.end() != l_it; ++l_it) { - if (!xr_strcmp(*l_cartridge.m_ammoSect, l_it->first)) + if (!xr_strcmp(l_cartridge.m_ammoSect.c_str(), l_it->first)) { ++(l_it->second); break; @@ -293,7 +293,7 @@ void CWeaponMagazined::UnloadMagazine(bool spawn_ammo) } if (l_it == l_ammo.end()) - l_ammo[*l_cartridge.m_ammoSect] = 1; + l_ammo[l_cartridge.m_ammoSect.c_str()] = 1; m_magazine.pop_back(); --iAmmoElapsed; } @@ -383,7 +383,7 @@ void CWeaponMagazined::ReloadMagazine() //разрядить магазин, если загружаем патронами другого типа if (!m_bLockType && !m_magazine.empty() && - (!m_pCurrentAmmo || xr_strcmp(m_pCurrentAmmo->cNameSect(), *m_magazine.back().m_ammoSect))) + (!m_pCurrentAmmo || xr_strcmp(m_pCurrentAmmo->cNameSect(), m_magazine.back().m_ammoSect.c_str()))) UnloadMagazine(); VERIFY((u32)iAmmoElapsed == m_magazine.size()); @@ -700,8 +700,8 @@ void CWeaponMagazined::switch2_Fire() #ifdef DEBUG if (ii != io->inventory().ActiveItem()) - Msg("! not an active item, item %s, owner %s, active item %s", *cName(), *H_Parent()->cName(), - io->inventory().ActiveItem() ? *io->inventory().ActiveItem()->object().cName() : "no_active_item"); + Msg("! not an active item, item %s, owner %s, active item %s", cName().c_str(), H_Parent()->cName().c_str(), + io->inventory().ActiveItem() ? io->inventory().ActiveItem()->object().cName().c_str() : "no_active_item"); if (!(io && (ii == io->inventory().ActiveItem()))) { @@ -1076,7 +1076,7 @@ void CWeaponMagazined::InitAddons() m_sSndShotCurrent = "sndSilencerShot"; //подсветка от выстрела - LoadLights(*cNameSect(), "silencer_"); + LoadLights(cNameSect().c_str(), "silencer_"); } else { @@ -1085,7 +1085,7 @@ void CWeaponMagazined::InitAddons() m_sSndShotCurrent = "sndShot"; //подсветка от выстрела - LoadLights(*cNameSect(), ""); + LoadLights(cNameSect().c_str(), ""); } if (silencer) diff --git a/src/xrGame/actor_communication.cpp b/src/xrGame/actor_communication.cpp index 7f4e403afe1..f1ad8e41ae7 100644 --- a/src/xrGame/actor_communication.cpp +++ b/src/xrGame/actor_communication.cpp @@ -61,8 +61,8 @@ void CActor::AddEncyclopediaArticle(const CInfoPortion* info_portion) const article_vector.emplace_back(*it, Level().GetGameTime(), article.data()->articleType); LPCSTR g, n; int _atype = article.data()->articleType; - g = *(article.data()->group); - n = *(article.data()->name); + g = (article.data()->group).c_str(); + n = (article.data()->name).c_str(); callback(GameObject::eArticleInfo)(lua_game_object(), g, n, _atype); /* XXX: Shadow of Chernobyl encyclopedia, return this code @@ -144,7 +144,7 @@ bool CActor::OnReceiveInfo(shared_str info_id) const AddEncyclopediaArticle(&info_portion); AddGameTask(&info_portion); - callback(GameObject::eInventoryInfo)(lua_game_object(), *info_id); + callback(GameObject::eInventoryInfo)(lua_game_object(), info_id.c_str()); if (!CurrentGameUI()) return false; diff --git a/src/xrGame/agent_enemy_manager.cpp b/src/xrGame/agent_enemy_manager.cpp index d42d43f9e1c..18bf90a7320 100644 --- a/src/xrGame/agent_enemy_manager.cpp +++ b/src/xrGame/agent_enemy_manager.cpp @@ -531,7 +531,7 @@ void CAgentEnemyManager::assign_wounded() iterator I = m_enemies.begin(); iterator E = m_enemies.end(); for (; I != E; ++I) - Msg(" [%s][0x%08x][0x%08x][%.2f]", *(*I).m_object->cName(), (*I).m_mask.get(), + Msg(" [%s][0x%08x][0x%08x][%.2f]", (*I).m_object->cName().c_str(), (*I).m_mask.get(), (*I).m_distribute_mask.get(), (*I).m_probability); } Msg("combat members(%d):", object().member().combat_members().size()); @@ -540,7 +540,7 @@ void CAgentEnemyManager::assign_wounded() const_iterator I = object().member().combat_members().begin(); const_iterator E = object().member().combat_members().end(); for (; I != E; ++I) - Msg(" [%s][0x%08x][0x%08x]", *(*I)->object().cName(), object().member().mask(&(*I)->object()), + Msg(" [%s][0x%08x][0x%08x]", (*I)->object().cName().c_str(), object().member().mask(&(*I)->object()), (*I)->selected_enemy()); } } diff --git a/src/xrGame/ai/monsters/basemonster/base_monster_debug.cpp b/src/xrGame/ai/monsters/basemonster/base_monster_debug.cpp index 19f50ab4d4a..3460cf21b18 100644 --- a/src/xrGame/ai/monsters/basemonster/base_monster_debug.cpp +++ b/src/xrGame/ai/monsters/basemonster/base_monster_debug.cpp @@ -56,7 +56,7 @@ CBaseMonster::SDebugInfo CBaseMonster::show_debug_info() DBG().text(this).clear(); DBG().text(this).add_item("---------------------------------------", x, y += delta_y, delimiter_color); - xr_sprintf(text, "-- Monster : [%s] Current Time = [%u]", *cName(), Device.dwTimeGlobal); + xr_sprintf(text, "-- Monster : [%s] Current Time = [%u]", cName().c_str(), Device.dwTimeGlobal); DBG().text(this).add_item(text, x, y += delta_y, color); DBG().text(this).add_item("----------- PROPERTIES ------------", x, y += delta_y, delimiter_color); @@ -70,7 +70,7 @@ CBaseMonster::SDebugInfo CBaseMonster::show_debug_info() if (EnemyMan.get_enemy()) { - xr_sprintf(text, "Current Enemy = [%s]", *EnemyMan.get_enemy()->cName()); + xr_sprintf(text, "Current Enemy = [%s]", EnemyMan.get_enemy()->cName().c_str()); } else xr_sprintf(text, "Current Enemy = [NONE]"); @@ -85,7 +85,7 @@ CBaseMonster::SDebugInfo CBaseMonster::show_debug_info() if (CorpseMan.get_corpse()) { - xr_sprintf(text, "Current Corpse = [%s] Satiety = [%.2f]", *CorpseMan.get_corpse()->cName(), GetSatiety()); + xr_sprintf(text, "Current Corpse = [%s] Satiety = [%.2f]", CorpseMan.get_corpse()->cName().c_str(), GetSatiety()); } else xr_sprintf(text, "Current Corpse = [NONE] Satiety = [%.2f]", GetSatiety()); @@ -144,7 +144,7 @@ CBaseMonster::SDebugInfo CBaseMonster::show_debug_info() { if (HitMemory.get_last_hit_object()) { - xr_sprintf(text, "Hit Info: object=[%s] time=[%u]", *(HitMemory.get_last_hit_object()->cName()), + xr_sprintf(text, "Hit Info: object=[%s] time=[%u]", HitMemory.get_last_hit_object()->cName().c_str(), HitMemory.get_last_hit_time()); } else @@ -173,7 +173,7 @@ CBaseMonster::SDebugInfo CBaseMonster::show_debug_info() if (EnemyMan.get_enemy()) { - xr_sprintf(text, "Current Enemy = [%s]", *EnemyMan.get_enemy()->cName()); + xr_sprintf(text, "Current Enemy = [%s]", EnemyMan.get_enemy()->cName().c_str()); } else xr_sprintf(text, "Current Enemy = [NONE]"); @@ -303,7 +303,7 @@ void CBaseMonster::debug_fsm() DBG().object_info(this, this).remove_item(u32(1)); DBG().object_info(this, this).remove_item(u32(2)); - DBG().object_info(this, this).add_item(*cName(), color_xrgb(255, 0, 0), 0); + DBG().object_info(this, this).add_item(cName().c_str(), color_xrgb(255, 0, 0), 0); DBG().object_info(this, this).add_item(st, color_xrgb(255, 0, 0), 1); xr_sprintf(st, "Team[%u]Squad[%u]Group[%u]", g_Team(), g_Squad(), g_Group()); @@ -392,7 +392,7 @@ void add_debug_info(debug::text_tree& root_s, const CEntity* pE) { if (pE) { - root_s.add_line(*pE->cName()); + root_s.add_line(pE->cName().c_str()); root_s.add_line("ID", pE->ID()); } else @@ -537,7 +537,7 @@ void CBaseMonster::add_debug_info(debug::text_tree& root_s) ::detail::add_debug_info(general_s, this); TextTree& current_visual_s = general_s.add_line("Current_Visual"); - current_visual_s.add_line(*cNameVisual()); + current_visual_s.add_line(cNameVisual().c_str()); general_s.add_line("Health", conditions().GetHealth()); general_s.add_line("Morale", Morale.get_morale()); @@ -615,7 +615,7 @@ void CBaseMonster::add_debug_info(debug::text_tree& root_s) //----------------------------------------------- TextTree& corpse_s = general_s.find_or_add("Corpse_Man"); - corpse_s.add_line("Current_Corpse", CorpseMan.get_corpse() ? *CorpseMan.get_corpse()->cName() : "none"); + corpse_s.add_line("Current_Corpse", CorpseMan.get_corpse() ? CorpseMan.get_corpse()->cName().c_str() : "none"); corpse_s.add_line("Satiety", make_xrstr("%.2f", GetSatiety())); //----------------------------------------------- @@ -748,7 +748,7 @@ void CBaseMonster::add_debug_info(debug::text_tree& root_s) TextTree& controller_s = root_s.find_or_add("Controllers"); TextTree& animation_s = controller_s.find_or_add("Animations"); - TextTree& current_animation_s = animation_s.add_line(*anim().cur_anim_info().name); + TextTree& current_animation_s = animation_s.add_line(anim().cur_anim_info().name.c_str()); CBlend* p_blend = control().animation().current_blend(); if (!p_blend) @@ -798,7 +798,7 @@ void CBaseMonster::add_debug_info(debug::text_tree& root_s) movement_s.add_line("Path_Type", pc_path_type); if (movement().path_type() == MovementManager::ePathTypePatrolPath) { - movement_s.add_line("Path_Name", *movement().patrol().path_name()); + movement_s.add_line("Path_Name", movement().patrol().path_name().c_str()); movement_s.add_line("Completed", movement().patrol().completed()); movement_s.add_line("Current_Point", movement().patrol().get_current_point_index()); @@ -872,13 +872,13 @@ void CBaseMonster::add_debug_info(debug::text_tree& root_s) movement().restrictions().base_in_restrictions().size()) { ::detail::add_debug_info_restrictions( - restrictions_s.add_line("out"), *movement().restrictions().out_restrictions()); + restrictions_s.add_line("out"), movement().restrictions().out_restrictions().c_str()); ::detail::add_debug_info_restrictions( - restrictions_s.add_line("in"), *movement().restrictions().in_restrictions()); + restrictions_s.add_line("in"), movement().restrictions().in_restrictions().c_str()); ::detail::add_debug_info_restrictions( - restrictions_s.add_line("base_out"), *movement().restrictions().base_out_restrictions()); + restrictions_s.add_line("base_out"), movement().restrictions().base_out_restrictions().c_str()); ::detail::add_debug_info_restrictions( - restrictions_s.add_line("base_in"), *movement().restrictions().base_in_restrictions()); + restrictions_s.add_line("base_in"), movement().restrictions().base_in_restrictions().c_str()); restrictions_s.add_line( "Actor_Accessible?", actor ? movement().restrictions().accessible(actor->Position()) : false); diff --git a/src/xrGame/ai/monsters/basemonster/base_monster_script.cpp b/src/xrGame/ai/monsters/basemonster/base_monster_script.cpp index 241fbf1088e..904ec594772 100644 --- a/src/xrGame/ai/monsters/basemonster/base_monster_script.cpp +++ b/src/xrGame/ai/monsters/basemonster/base_monster_script.cpp @@ -498,7 +498,7 @@ void CBaseMonster::ProcessScripts() { DBG().object_info(this, this).remove_item(u32(0)); DBG().object_info(this, this).remove_item(u32(1)); - DBG().object_info(this, this).add_item(*cName(), color_xrgb(255, 0, 0), 0); + DBG().object_info(this, this).add_item(cName().c_str(), color_xrgb(255, 0, 0), 0); DBG().object_info(this, this).add_item("Under script", color_xrgb(255, 0, 0), 1); } else diff --git a/src/xrGame/ai/monsters/monster_home.cpp b/src/xrGame/ai/monsters/monster_home.cpp index 0eb9b7f00b8..35f05d5f800 100644 --- a/src/xrGame/ai/monsters/monster_home.cpp +++ b/src/xrGame/ai/monsters/monster_home.cpp @@ -19,15 +19,17 @@ void check_path(const CBaseMonster* monster, const CPatrolPath* path) ai().level_graph().level_id(), make_string("invalid patrol path [%s] as home specified for monster [%s]\nmonster is on level %s\npatrol path " "is on level %s", - *path->m_name, *monster->cName(), - *ai().game_graph() + path->m_name.c_str(), monster->cName().c_str(), + ai().game_graph() .header() .level(ai().game_graph().vertex(monster->ai_location().game_vertex_id())->level_id()) - .name(), - *ai().game_graph() + .name() + .c_str(), + ai().game_graph() .header() .level(ai().game_graph().vertex(path->vertices().begin()->second->data().game_vertex_id())->level_id()) - .name())); + .name() + .c_str())); } #else // DEBUG #define check_path(a, b) diff --git a/src/xrGame/ai/stalker/ai_stalker.cpp b/src/xrGame/ai/stalker/ai_stalker.cpp index 4f3c82aa31b..b4a84029aac 100644 --- a/src/xrGame/ai/stalker/ai_stalker.cpp +++ b/src/xrGame/ai/stalker/ai_stalker.cpp @@ -111,7 +111,7 @@ void CAI_Stalker::reinit() //загрузка спецевической звуковой схемы для сталкера согласно m_SpecificCharacter sound().sound_prefix(SpecificCharacter().sound_voice_prefix()); - LoadSounds(*cNameSect()); + LoadSounds(cNameSect().c_str()); m_pPhysics_support->in_Init(); @@ -308,11 +308,11 @@ void CAI_Stalker::reload(LPCSTR section) m_can_select_weapon = true; - LPCSTR queue_sect = READ_IF_EXISTS(pSettings, r_string, *cNameSect(), "fire_queue_section", nullptr); + LPCSTR queue_sect = READ_IF_EXISTS(pSettings, r_string, cNameSect().c_str(), "fire_queue_section", nullptr); if (!queue_sect || xr_strcmp(queue_sect, "") != 0 || !pSettings->section_exist(queue_sect)) { - queue_sect = *cNameSect(); + queue_sect = cNameSect().c_str(); } const auto tryToRead = [&](pcstr lineToRead, u32 defaultValue) -> u32 @@ -610,7 +610,7 @@ bool CAI_Stalker::net_Spawn(CSE_Abstract* DC) if (SpecificCharacter().terrain_sect().size()) { - movement().locations().Load(*SpecificCharacter().terrain_sect()); + movement().locations().Load(SpecificCharacter().terrain_sect().c_str()); } sight().update(); @@ -818,7 +818,7 @@ void CAI_Stalker::UpdateCL() { START_PROFILE("stalker") START_PROFILE("stalker/client_update") - VERIFY2(PPhysicsShell() || getEnabled(), *cName()); + VERIFY2(PPhysicsShell() || getEnabled(), cName().c_str()); if (g_Alive()) { @@ -907,7 +907,7 @@ void CAI_Stalker::shedule_Update(u32 DT) { START_PROFILE("stalker") START_PROFILE("stalker/schedule_update") - VERIFY2(getEnabled() || PPhysicsShell(), *cName()); + VERIFY2(getEnabled() || PPhysicsShell(), cName().c_str()); if (!CObjectHandler::planner().initialized()) { diff --git a/src/xrGame/ai/stalker/ai_stalker_cover.cpp b/src/xrGame/ai/stalker/ai_stalker_cover.cpp index 91870b525ab..aa572fea2ca 100644 --- a/src/xrGame/ai/stalker/ai_stalker_cover.cpp +++ b/src/xrGame/ai/stalker/ai_stalker_cover.cpp @@ -269,7 +269,7 @@ void CAI_Stalker::on_restrictions_change() inherited::on_restrictions_change(); m_best_cover_actual = false; #ifdef _DEBUG - Msg("* [%6d][%s] on_restrictions_change", Device.dwTimeGlobal, *cName()); + Msg("* [%6d][%s] on_restrictions_change", Device.dwTimeGlobal, cName().c_str()); #endif } diff --git a/src/xrGame/ai/stalker/ai_stalker_debug.cpp b/src/xrGame/ai/stalker/ai_stalker_debug.cpp index 861ee17491d..311a9dc31e4 100644 --- a/src/xrGame/ai/stalker/ai_stalker_debug.cpp +++ b/src/xrGame/ai/stalker/ai_stalker_debug.cpp @@ -208,8 +208,8 @@ void draw_restrictions(const shared_str& restrictions, LPCSTR start_indent, LPCS { DBG_OutText("%s%s%s", start_indent, indent, header); string256 temp; - for (u32 i = 0, n = _GetItemCount(*restrictions); i < n; ++i) - DBG_OutText("%s%s%s%s", start_indent, indent, indent, _GetItem(*restrictions, i, temp)); + for (u32 i = 0, n = _GetItemCount(restrictions.c_str()); i < n; ++i) + DBG_OutText("%s%s%s%s", start_indent, indent, indent, _GetItem(restrictions.c_str(), i, temp)); } LPCSTR movement_type(const MonsterSpace::EMovementType& movement_type) @@ -267,8 +267,8 @@ void CAI_Stalker::debug_text() DBG_TextOutSet(0, up_indent); // memory DBG_OutText("memory"); - DBG_OutText("%sname : %s", indent, *cName()); - DBG_OutText("%ssection : %s", indent, *cNameSect()); + DBG_OutText("%sname : %s", indent, cName().c_str()); + DBG_OutText("%ssection : %s", indent, cNameSect().c_str()); DBG_OutText("%sid : %d", indent, ID()); DBG_OutText("%shealth : %f", indent, conditions().health()); DBG_OutText("%swounded : %c", indent, wounded() ? '+' : '-'); @@ -304,7 +304,7 @@ void CAI_Stalker::debug_text() DBG_OutText("%s%s%stype", indent, indent, indent); DBG_OutText("%s%s%spower : %f", indent, indent, indent, memory().sound().sound()->m_power); DBG_OutText("%s%s%sobject : %s", indent, indent, indent, - memory().sound().sound()->m_object ? *memory().sound().sound()->m_object->cName() : "unknown"); + memory().sound().sound()->m_object ? memory().sound().sound()->m_object->cName().c_str() : "unknown"); if (g_Alive() && memory().sound().sound()->m_object) DBG_OutText("%s%s%svisible : %s", indent, indent, indent, memory().visual().visible_now(memory().sound().sound()->m_object) ? "+" : "-"); @@ -316,14 +316,14 @@ void CAI_Stalker::debug_text() ALife::_OBJECT_ID object_id = memory().hit().last_hit_object_id(); DBG_OutText("%s%slast hit object id : %d", indent, indent, object_id); IGameObject* object = (object_id == ALife::_OBJECT_ID(-1)) ? 0 : Level().Objects.net_Find(object_id); - DBG_OutText("%s%slast hit object name : %s", indent, indent, object ? *object->cName() : ""); + DBG_OutText("%s%slast hit object name : %s", indent, indent, object ? object->cName().c_str() : ""); #ifdef USE_SELECTED_HIT if (memory().hit().hit()) { DBG_OutText("%s%sselected", indent, indent); DBG_OutText("%s%s%spower : %f", indent, indent, indent, memory().hit().hit()->m_amount); DBG_OutText("%s%s%sobject : %s", indent, indent, indent, - memory().hit().hit()->m_object ? *memory().hit().hit()->m_object->cName() : "unknown"); + memory().hit().hit()->m_object ? memory().hit().hit()->m_object->cName().c_str() : "unknown"); if (g_Alive() && memory().hit().hit()->m_object) DBG_OutText("%s%s%svisible : %s", indent, indent, indent, memory().visual().visible_now(memory().hit().hit()->m_object) ? "+" : "-"); @@ -348,7 +348,7 @@ void CAI_Stalker::debug_text() CEnemyManager::OBJECTS::const_iterator I = memory().enemy().objects().begin(); CEnemyManager::OBJECTS::const_iterator E = memory().enemy().objects().end(); for (; I != E; ++I) - DBG_OutText("%s%s%s%s : %s", indent, indent, indent, *(*I)->cName(), + DBG_OutText("%s%s%s%s : %s", indent, indent, indent, (*I)->cName().c_str(), memory().visual().visible_now(*I) ? "visible" : "invisible"); } @@ -372,7 +372,7 @@ void CAI_Stalker::debug_text() DBG_OutText("%s%s%svisible : %s %f", indent, indent, indent, memory().visual().visible_now(memory().enemy().selected()) ? "+" : "-", fuzzy); } - DBG_OutText("%s%s%sobject : %s", indent, indent, indent, *memory().enemy().selected()->cName()); + DBG_OutText("%s%s%sobject : %s", indent, indent, indent, memory().enemy().selected()->cName().c_str()); if (g_Alive()) { float interval = (1.f - panic_threshold()) * .25f, left = -1.f, right = -1.f; @@ -432,7 +432,7 @@ void CAI_Stalker::debug_text() DBG_OutText("%s%s%stime : %.3f (%.3f)", indent, indent, indent, float(memory().danger().selected()->time()) / 1000.f, float(Device.dwTimeGlobal - memory().danger().selected()->time()) / 1000.f); - DBG_OutText("%s%s%sinitiator : %s", indent, indent, indent, *memory().danger().selected()->object()->cName()); + DBG_OutText("%s%s%sinitiator : %s", indent, indent, indent, memory().danger().selected()->object()->cName().c_str()); if (g_Alive() && memory().danger().selected()->object()) DBG_OutText("%s%s%svisible : %s", indent, indent, indent, memory().visual().visible_now(memory().danger().selected()->object()) ? "+" : "-"); @@ -441,7 +441,7 @@ void CAI_Stalker::debug_text() !!memory().danger().selected()->dependent_object()->cName()) { DBG_OutText("%s%s%sdependent : %s", indent, indent, indent, - *memory().danger().selected()->dependent_object()->cName()); + memory().danger().selected()->dependent_object()->cName().c_str()); if (g_Alive()) DBG_OutText("%s%s%svisible : %s", indent, indent, indent, memory().visual().visible_now( @@ -483,12 +483,12 @@ void CAI_Stalker::debug_text() if (agent_manager().member().member(this).member_death_reaction().m_processing) DBG_OutText("%react on death : %s", indent, - *agent_manager().member().member(this).member_death_reaction().m_member->cName()); + agent_manager().member().member(this).member_death_reaction().m_member->cName().c_str()); if (agent_manager().member().member(this).grenade_reaction().m_processing) DBG_OutText("%react on grenade : %s", indent, agent_manager().member().member(this).grenade_reaction().m_game_object ? - *agent_manager().member().member(this).grenade_reaction().m_game_object->cName() : + agent_manager().member().member(this).grenade_reaction().m_game_object->cName().c_str() : "unknown"); } } @@ -498,18 +498,18 @@ void CAI_Stalker::debug_text() DBG_OutText("%sobjects", indent); DBG_OutText("%s%sobjects : %d", indent, indent, inventory().m_all.size()); DBG_OutText("%s%sactive item : %s", indent, indent, - inventory().ActiveItem() ? *inventory().ActiveItem()->object().cName() : ""); - DBG_OutText("%s%sbest weapon : %s", indent, indent, best_weapon() ? *best_weapon()->object().cName() : ""); + inventory().ActiveItem() ? inventory().ActiveItem()->object().cName().c_str() : ""); + DBG_OutText("%s%sbest weapon : %s", indent, indent, best_weapon() ? best_weapon()->object().cName().c_str() : ""); DBG_OutText( - "%s%sitem to kill : %s", indent, indent, item_to_kill() ? *m_best_item_to_kill->object().cName() : ""); + "%s%sitem to kill : %s", indent, indent, item_to_kill() ? m_best_item_to_kill->object().cName().c_str() : ""); DBG_OutText("%s%sitem can kill : %s", indent, indent, item_can_kill() ? "+" : "-"); DBG_OutText("%s%smemory item to kill : %s", indent, indent, - remember_item_to_kill() ? *m_best_found_item_to_kill->object().cName() : ""); + remember_item_to_kill() ? m_best_found_item_to_kill->object().cName().c_str() : ""); DBG_OutText( - "%s%smemory ammo : %s", indent, indent, remember_ammo() ? *m_best_found_ammo->object().cName() : ""); + "%s%smemory ammo : %s", indent, indent, remember_ammo() ? m_best_found_ammo->object().cName().c_str() : ""); DBG_OutText("%s%sinfinite ammo : %s", indent, indent, m_infinite_ammo ? "+" : "-"); DBG_OutText( - "%s%sitem to spawn : %s", indent, indent, item_to_spawn().size() ? *item_to_spawn() : "no item to spawn"); + "%s%sitem to spawn : %s", indent, indent, item_to_spawn().size() ? item_to_spawn().c_str() : "no item to spawn"); DBG_OutText("%s%sammo in box to spawn: %d", indent, indent, item_to_spawn().size() ? ammo_in_box_to_spawn() : 0); CWeaponMagazined* weapon = smart_cast(inventory().ActiveItem()); @@ -525,7 +525,7 @@ void CAI_Stalker::debug_text() { DBG_OutText("%s%sactive item", indent, indent); DBG_OutText("%s%s%sobject : %s", indent, indent, indent, - inventory().ActiveItem() ? *inventory().ActiveItem()->object().cName() : ""); + inventory().ActiveItem() ? inventory().ActiveItem()->object().cName().c_str() : ""); CWeapon* weapon = smart_cast(inventory().ActiveItem()); if (weapon) { @@ -654,7 +654,7 @@ void CAI_Stalker::debug_text() if (movement().path_type() == MovementManager::ePathTypePatrolPath) { DBG_OutText("%s%spatrol", indent, indent); - DBG_OutText("%s%s%spath : %s", indent, indent, indent, *movement().patrol().path_name()); + DBG_OutText("%s%s%spath : %s", indent, indent, indent, movement().patrol().path_name().c_str()); DBG_OutText("%s%s%scompleted : %s", indent, indent, indent, movement().patrol().completed() ? "+" : "-"); DBG_OutText("%s%s%scurrent point : %d", indent, indent, indent, movement().patrol().get_current_point_index()); if (movement().patrol().get_path() && @@ -924,7 +924,7 @@ void CAI_Stalker::debug_text() } case SightManager::eSightTypeObject: { - DBG_OutText("%s%sobject : %s", indent, indent, *sight().current_action().object().cName()); + DBG_OutText("%s%sobject : %s", indent, indent, sight().current_action().object().cName().c_str()); DBG_OutText( "%s%sposition : [%f][%f][%f]", indent, indent, VPUSH(sight().current_action().object().Position())); break; @@ -939,7 +939,7 @@ void CAI_Stalker::debug_text() } case SightManager::eSightTypeFireObject: { - DBG_OutText("%s%sobject : %s", indent, indent, *sight().current_action().object().cName()); + DBG_OutText("%s%sobject : %s", indent, indent, sight().current_action().object().cName().c_str()); DBG_OutText( "%s%sposition : [%f][%f][%f]", indent, indent, VPUSH(sight().current_action().object().Position())); DBG_OutText("%s%svisible point : %s", indent, indent, false ? "-" : "+"); diff --git a/src/xrGame/ai/trader/ai_trader.cpp b/src/xrGame/ai/trader/ai_trader.cpp index 8748e6a4e84..d0c7c4bedf1 100644 --- a/src/xrGame/ai/trader/ai_trader.cpp +++ b/src/xrGame/ai/trader/ai_trader.cpp @@ -224,7 +224,7 @@ void CAI_Trader::feel_touch_new(IGameObject* O) if (I && I->useful_for_NPC()) { - Msg("Taking item %s!", *I->object().cName()); + Msg("Taking item %s!", I->object().cName().c_str()); NET_Packet P; u_EventGen(P, GE_OWNERSHIP_TAKE, ID()); P.w_u16(u16(I->object().ID())); diff --git a/src/xrGame/alife_simulator_base.cpp b/src/xrGame/alife_simulator_base.cpp index fadb911e70b..eaed45ff50e 100644 --- a/src/xrGame/alife_simulator_base.cpp +++ b/src/xrGame/alife_simulator_base.cpp @@ -102,7 +102,7 @@ CSE_Abstract* CALifeSimulatorBase::spawn_item(LPCSTR section, const Fvector& pos abstract->m_wVersion = SPAWN_VERSION; string256 s_name_replace; - xr_strcpy(s_name_replace, *abstract->s_name); + xr_strcpy(s_name_replace, abstract->s_name.c_str()); if (abstract->ID < 1000) xr_strcat(s_name_replace, "0"); if (abstract->ID < 100) @@ -159,7 +159,7 @@ CSE_Abstract* CALifeSimulatorBase::create(CSE_ALifeGroupAbstract* tpALifeGroupAb k->m_bALifeControl = true; string256 s_name_replace; - xr_strcpy(s_name_replace, *k->s_name); + xr_strcpy(s_name_replace, k->s_name.c_str()); if (k->ID < 1000) xr_strcat(s_name_replace, "0"); if (k->ID < 100) @@ -178,8 +178,8 @@ CSE_Abstract* CALifeSimulatorBase::create(CSE_ALifeGroupAbstract* tpALifeGroupAb void CALifeSimulatorBase::create(CSE_ALifeDynamicObject*& i, CSE_ALifeDynamicObject* j, const _SPAWN_ID& tSpawnID) { - CSE_Abstract* tpSE_Abstract = F_entity_Create(*j->s_name); - R_ASSERT3(tpSE_Abstract, "Cannot find item with section", *j->s_name); + CSE_Abstract* tpSE_Abstract = F_entity_Create(j->s_name.c_str()); + R_ASSERT3(tpSE_Abstract, "Cannot find item with section", j->s_name.c_str()); i = smart_cast(tpSE_Abstract); R_ASSERT2(i, "Non-ALife object in the 'game.spawn'"); @@ -264,7 +264,7 @@ void CALifeSimulatorBase::release(CSE_Abstract* abstract, bool alife_query) #ifdef DEBUG if (psAI_Flags.test(aiALife)) { - Msg("[LSS] Releasing object [%s][%s][%d][%x]", abstract->name_replace(), *abstract->s_name, abstract->ID, + Msg("[LSS] Releasing object [%s][%s][%d][%x]", abstract->name_replace(), abstract->s_name.c_str(), abstract->ID, smart_cast(abstract)); } #endif diff --git a/src/xrGame/alife_simulator_script.cpp b/src/xrGame/alife_simulator_script.cpp index fcf26f6db78..7f4df74754c 100644 --- a/src/xrGame/alife_simulator_script.cpp +++ b/src/xrGame/alife_simulator_script.cpp @@ -92,13 +92,13 @@ void generate_story_ids(STORY_PAIRS& result, _id_type INVALID_ID, LPCSTR section { const shared_str& temp = Ini->r_string_wb(section_name, N); - R_ASSERT3(!strchr(*temp, ' '), invalid_id_description, *temp); - R_ASSERT2(xr_strcmp(*temp, INVALID_ID_STRING), invalid_id_redefinition); + R_ASSERT3(!strchr(temp.c_str(), ' '), invalid_id_description, temp.c_str()); + R_ASSERT2(xr_strcmp(temp.c_str(), INVALID_ID_STRING), invalid_id_redefinition); for (const auto& story : result) - R_ASSERT3(story.first != temp, duplicated_id_description, *temp); + R_ASSERT3(story.first != temp, duplicated_id_description, temp.c_str()); - result.emplace_back(*temp, atoi(N)); + result.emplace_back(temp.c_str(), atoi(N)); ++k; } @@ -302,7 +302,7 @@ void CALifeSimulator__release(CALifeSimulator* self, CSE_Abstract* object, bool) LPCSTR get_level_name(const CALifeSimulator* self, int level_id) { - LPCSTR result = *ai().game_graph().header().level((GameGraph::_LEVEL_ID)level_id).name(); + LPCSTR result = ai().game_graph().header().level((GameGraph::_LEVEL_ID)level_id).name().c_str(); return (result); } @@ -523,7 +523,7 @@ void CALifeSimulator::script_register(lua_State* luaState) STORY_PAIRS::const_iterator I = story_ids.begin(); STORY_PAIRS::const_iterator E = story_ids.end(); for (; I != E; ++I) - instance.enum_("_story_ids")[luabind::value(*(*I).first, (*I).second)]; + instance.enum_("_story_ids")[luabind::value((*I).first.c_str(), (*I).second)]; luabind::module(luaState)[instance]; } @@ -543,7 +543,7 @@ void CALifeSimulator::script_register(lua_State* luaState) SPAWN_STORY_PAIRS::const_iterator I = spawn_story_ids.begin(); SPAWN_STORY_PAIRS::const_iterator E = spawn_story_ids.end(); for (; I != E; ++I) - instance.enum_("_spawn_story_ids")[luabind::value(*(*I).first, (*I).second)]; + instance.enum_("_spawn_story_ids")[luabind::value((*I).first.c_str(), (*I).second)]; luabind::module(luaState)[instance]; } diff --git a/src/xrGame/animation_script_callback.cpp b/src/xrGame/animation_script_callback.cpp index e8a4c1ff60a..c18bac844ef 100644 --- a/src/xrGame/animation_script_callback.cpp +++ b/src/xrGame/animation_script_callback.cpp @@ -27,7 +27,7 @@ CBlend* PlayMotionByParts( CBlend* anim_script_callback::play_cycle(IKinematicsAnimated* sa, const shared_str& anim) { - MotionID m = sa->LL_MotionID(*anim); + MotionID m = sa->LL_MotionID(anim.c_str()); R_ASSERT(m.valid()); if (sa->LL_GetMotionDef(m)->StopAtEnd()) { diff --git a/src/xrGame/console_commands.cpp b/src/xrGame/console_commands.cpp index e0f325402bf..d50f275ea61 100644 --- a/src/xrGame/console_commands.cpp +++ b/src/xrGame/console_commands.cpp @@ -1402,13 +1402,13 @@ class CCC_ScriptCommand : public IConsole_Command l_iErrorCode = lua_pcall(GEnv.ScriptEngine->lua(), 0, 0, 0); if (l_iErrorCode) { - GEnv.ScriptEngine->print_output(GEnv.ScriptEngine->lua(), *m_script_name, l_iErrorCode); + GEnv.ScriptEngine->print_output(GEnv.ScriptEngine->lua(), m_script_name.c_str(), l_iErrorCode); GEnv.ScriptEngine->on_error(GEnv.ScriptEngine->lua()); return; } } - GEnv.ScriptEngine->print_output(GEnv.ScriptEngine->lua(), *m_script_name, l_iErrorCode); + GEnv.ScriptEngine->print_output(GEnv.ScriptEngine->lua(), m_script_name.c_str(), l_iErrorCode); } } // void Execute @@ -1757,7 +1757,7 @@ class CCC_DumpModelBones : public IConsole_Command Msg("bones for model \"%s\"", arguments); for (u16 i = 0, n = kinematics->LL_BoneCount(); i < n; ++i) - Msg("%s", *kinematics->LL_GetData(i).name); + Msg("%s", kinematics->LL_GetData(i).name.c_str()); GEnv.Render->model_Delete(visual); } diff --git a/src/xrGame/ef_storage_script.cpp b/src/xrGame/ef_storage_script.cpp index 66d1c06f3bc..ec2c0d703d7 100644 --- a/src/xrGame/ef_storage_script.cpp +++ b/src/xrGame/ef_storage_script.cpp @@ -35,7 +35,7 @@ float evaluate(CEF_Storage* ef_storage, LPCSTR function, CScriptGameObject* _0, if (_0 && !ef_storage->non_alife().member()) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "object %s is not herited from CSE_ALifeSchedulable!", *_0->cName()); + LuaMessageType::Error, "object %s is not herited from CSE_ALifeSchedulable!", _0->cName().c_str()); return (0.f); } @@ -43,7 +43,7 @@ float evaluate(CEF_Storage* ef_storage, LPCSTR function, CScriptGameObject* _0, if (_1 && !ef_storage->non_alife().enemy()) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "object %s is not herited from CSE_ALifeSchedulable!", *_1->cName()); + LuaMessageType::Error, "object %s is not herited from CSE_ALifeSchedulable!", _1->cName().c_str()); return (0.f); } diff --git a/src/xrGame/enemy_manager.cpp b/src/xrGame/enemy_manager.cpp index 6d524249382..909753b519a 100644 --- a/src/xrGame/enemy_manager.cpp +++ b/src/xrGame/enemy_manager.cpp @@ -195,7 +195,7 @@ void CEnemyManager::ignore_monster_threshold(const float& ignore_monster_thresho void CEnemyManager::restore_ignore_monster_threshold() { m_ignore_monster_threshold = - READ_IF_EXISTS(pSettings, r_float, *m_object->cNameSect(), "ignore_monster_threshold", 1.f); + READ_IF_EXISTS(pSettings, r_float, m_object->cNameSect().c_str(), "ignore_monster_threshold", 1.f); } float CEnemyManager::ignore_monster_threshold() const { return (m_ignore_monster_threshold); } @@ -206,7 +206,7 @@ void CEnemyManager::max_ignore_monster_distance(const float& max_ignore_monster_ void CEnemyManager::restore_max_ignore_monster_distance() { - m_max_ignore_distance = READ_IF_EXISTS(pSettings, r_float, *m_object->cNameSect(), "max_ignore_distance", 0.f); + m_max_ignore_distance = READ_IF_EXISTS(pSettings, r_float, m_object->cNameSect().c_str(), "max_ignore_distance", 0.f); } float CEnemyManager::max_ignore_monster_distance() const { return (m_max_ignore_distance); } diff --git a/src/xrGame/fs_registrator_script.cpp b/src/xrGame/fs_registrator_script.cpp index c47b25b6e41..9fc7799eb2f 100644 --- a/src/xrGame/fs_registrator_script.cpp +++ b/src/xrGame/fs_registrator_script.cpp @@ -15,7 +15,7 @@ LPCSTR update_path_script(CLocatorAPI* fs, LPCSTR initial, LPCSTR src) convert_path_separators(temp); #endif temp_2 = temp; - return *temp_2; + return temp_2.c_str(); } //Alundaio: Set flag to rescan all files in path diff --git a/src/xrGame/game_cl_base.cpp b/src/xrGame/game_cl_base.cpp index ac3f160a7e2..3961f4ad9f9 100644 --- a/src/xrGame/game_cl_base.cpp +++ b/src/xrGame/game_cl_base.cpp @@ -236,7 +236,7 @@ void game_cl_GameState::TranslateGameMessage(u32 msg, NET_Packet& P) players.emplace(newClientId, PS); OnNewPlayerConnected(newClientId); } - xr_sprintf(Text, "%s%s %s%s", Color_Teams[0], PS->getName(), Color_Main, *StringTable().translate("mp_connected")); + xr_sprintf(Text, "%s%s %s%s", Color_Teams[0], PS->getName(), Color_Main, StringTable().translate("mp_connected").c_str()); if (CurrentGameUI()) CurrentGameUI()->CommonMessageOut(Text); //--------------------------------------- @@ -248,7 +248,7 @@ void game_cl_GameState::TranslateGameMessage(u32 msg, NET_Packet& P) string64 PlayerName; P.r_stringZ(PlayerName); - xr_sprintf(Text, "%s%s %s%s", Color_Teams[0], PlayerName, Color_Main, *StringTable().translate("mp_disconnected")); + xr_sprintf(Text, "%s%s %s%s", Color_Teams[0], PlayerName, Color_Main, StringTable().translate("mp_disconnected").c_str()); if (CurrentGameUI()) CurrentGameUI()->CommonMessageOut(Text); //--------------------------------------- @@ -260,7 +260,7 @@ void game_cl_GameState::TranslateGameMessage(u32 msg, NET_Packet& P) string64 PlayerName; P.r_stringZ(PlayerName); - xr_sprintf(Text, "%s%s %s%s", Color_Teams[0], PlayerName, Color_Main, *StringTable().translate("mp_entered_game")); + xr_sprintf(Text, "%s%s %s%s", Color_Teams[0], PlayerName, Color_Main, StringTable().translate("mp_entered_game").c_str()); if (CurrentGameUI()) CurrentGameUI()->CommonMessageOut(Text); } diff --git a/src/xrGame/game_cl_base_weapon_usage_statistic.cpp b/src/xrGame/game_cl_base_weapon_usage_statistic.cpp index fdcedbfde11..8a156d26bb2 100644 --- a/src/xrGame/game_cl_base_weapon_usage_statistic.cpp +++ b/src/xrGame/game_cl_base_weapon_usage_statistic.cpp @@ -449,8 +449,8 @@ void WeaponUsageStatistic::RemoveBullet(ABULLETS_it& Bullet_it) if (!Bullet_it->Removed || Bullet_it->HitRefCount != Bullet_it->HitResponds) return; //------------------------------------------------------------- - auto PlayerIt = FindPlayer(*(Bullet_it->FirerName)); - auto WeaponIt = PlayerIt->FindPlayersWeapon(*(Bullet_it->WeaponName)); + auto PlayerIt = FindPlayer(Bullet_it->FirerName.c_str()); + auto WeaponIt = PlayerIt->FindPlayersWeapon(Bullet_it->WeaponName.c_str()); HITS_VEC_it HitIt; if (WeaponIt->FindHit(Bullet_it->Bullet.m_dwID, HitIt)) { @@ -504,10 +504,10 @@ void WeaponUsageStatistic::OnBullet_Fire(SBullet* pBullet, const CCartridge& car if (!pActor) return; //----------------------------------------------------------------------------------- - auto PlayerIt = FindPlayer(*object_parent->cName()); + auto PlayerIt = FindPlayer(object_parent->cName().c_str()); pBullet->m_dwID = PlayerIt->m_dwTotalShots++; PlayerIt->m_dwTotalShots_d++; - auto WeaponIt = PlayerIt->FindPlayersWeapon(*object_weapon->cNameSect()); + auto WeaponIt = PlayerIt->FindPlayersWeapon(object_weapon->cNameSect().c_str()); WeaponIt->m_dwRoundsFired = (++WeaponIt->m_dwBulletsFired) / cartridge.param_s.buckShot; WeaponIt->m_dwBulletsFired_d++; //----------------------------------------------------------------------------------- @@ -526,8 +526,8 @@ void WeaponUsageStatistic::OnBullet_Hit(SBullet* pBullet, u16 TargetID, s16 elem if (!FindBullet(pBullet->m_dwID, BulletIt)) return; //----------------------------------------------------- - auto PlayerIt = FindPlayer(*(BulletIt->FirerName)); - auto WeaponIt = PlayerIt->FindPlayersWeapon(*(BulletIt->WeaponName)); + auto PlayerIt = FindPlayer(BulletIt->FirerName.c_str()); + auto WeaponIt = PlayerIt->FindPlayersWeapon(BulletIt->WeaponName.c_str()); if (!BulletIt->HitRefCount++) { WeaponIt->m_dwHitsScored++; @@ -717,8 +717,8 @@ void WeaponUsageStatistic::On_Check_Respond(NET_Packet* P) BulletIt->HitResponds++; //--------------------------------------------------------------- - auto PlayerIt = FindPlayer(*(BulletIt->FirerName)); - auto WeaponIt = PlayerIt->FindPlayersWeapon(*(BulletIt->WeaponName)); + auto PlayerIt = FindPlayer(BulletIt->FirerName.c_str()); + auto WeaponIt = PlayerIt->FindPlayersWeapon(BulletIt->WeaponName.c_str()); (*WeaponIt).m_dwKillsScored++; (*WeaponIt).m_dwKillsScored_d++; @@ -998,7 +998,7 @@ void WeaponUsageStatistic::OnUpdateRespond(NET_Packet* P, shared_str const& send shared_str PName; P->r_stringZ(PName); - Player_Statistic& PS = *(FindPlayer(*PName)); + Player_Statistic& PS = *(FindPlayer(PName.c_str())); PS.PDigest = sender_digest; PS.PID = sender_pid; Msg("--- CL: On Update Respond from [%s]", PName.c_str()); diff --git a/src/xrGame/game_cl_base_weapon_usage_statistic_save.cpp b/src/xrGame/game_cl_base_weapon_usage_statistic_save.cpp index cbca57095ac..ca0d88658d2 100644 --- a/src/xrGame/game_cl_base_weapon_usage_statistic_save.cpp +++ b/src/xrGame/game_cl_base_weapon_usage_statistic_save.cpp @@ -86,7 +86,7 @@ void WeaponUsageStatistic::SaveData() #else time_t Time; time(&Time); - xr_sprintf(mFileName, "(%s)_(%s)_%s.wus", *(Level().name()), GameType, ctime(&Time)); + xr_sprintf(mFileName, "(%s)_(%s)_%s.wus", Level().name().c_str(), GameType, ctime(&Time)); #endif //--------------------------------------------------------- FS.update_path(mFileName, "$logs$", mFileName); diff --git a/src/xrGame/game_cl_capture_the_artefact.cpp b/src/xrGame/game_cl_capture_the_artefact.cpp index fd2831b7731..15eb265ce45 100644 --- a/src/xrGame/game_cl_capture_the_artefact.cpp +++ b/src/xrGame/game_cl_capture_the_artefact.cpp @@ -299,7 +299,7 @@ void game_cl_CaptureTheArtefact::TranslateGameMessage(u32 msg, NET_Packet& P) } else { - xr_sprintf(Text, "%s%s", Color_Main, *st.translate("mp_you_captured_artefact")); + xr_sprintf(Text, "%s%s", Color_Main, st.translate("mp_you_captured_artefact").c_str()); PlayCapturedTheArtefact(ps); diff --git a/src/xrGame/game_cl_deathmatch.cpp b/src/xrGame/game_cl_deathmatch.cpp index 3221f860816..c72fc4b2d53 100644 --- a/src/xrGame/game_cl_deathmatch.cpp +++ b/src/xrGame/game_cl_deathmatch.cpp @@ -516,11 +516,11 @@ void game_cl_Deathmatch::shedule_Update(u32 dt) ConvertTime2String(&S, TimeRemains); string1024 tmpStr = ""; if (TimeRemains > 10000) - strconcat(sizeof(tmpStr), tmpStr, *st.translate("mp_time2start"), " ", S); + strconcat(sizeof(tmpStr), tmpStr, st.translate("mp_time2start").c_str(), " ", S); else { if (TimeRemains < 1000) - strconcat(sizeof(tmpStr), tmpStr, *st.translate("mp_go"), ""); + strconcat(sizeof(tmpStr), tmpStr, st.translate("mp_go").c_str(), ""); else { static u32 dwLastTimeRemains = 10; @@ -532,7 +532,7 @@ void game_cl_Deathmatch::shedule_Update(u32 dt) } dwLastTimeRemains = dwCurTimeRemains; xr_itoa(dwCurTimeRemains, S, 10); - strconcat(sizeof(tmpStr), tmpStr, *st.translate("mp_ready"), "...", S); + strconcat(sizeof(tmpStr), tmpStr, st.translate("mp_ready").c_str(), "...", S); } }; @@ -596,7 +596,7 @@ void game_cl_Deathmatch::shedule_Update(u32 dt) string64 S; ConvertTime2String(&S, Rest); string128 FullS; - xr_sprintf(FullS, "%s : %s", *st.translate("mp_time2respawn"), S); + xr_sprintf(FullS, "%s : %s", st.translate("mp_time2respawn").c_str(), S); m_game_ui->SetForceRespawnTimeCaption(FullS); }; @@ -812,15 +812,15 @@ void game_cl_Deathmatch::OnVoteStart(NET_Packet& P) if (!xr_strcmp(CmdName, "restart")) { - xr_sprintf(NewCmd, "%s", *st.translate("mp_restart")); + xr_sprintf(NewCmd, "%s", st.translate("mp_restart").c_str()); } else if (!xr_strcmp(CmdName, "restart_fast")) { - xr_sprintf(NewCmd, "%s", *st.translate("mp_restart_fast")); + xr_sprintf(NewCmd, "%s", st.translate("mp_restart_fast").c_str()); } else if (!xr_strcmp(CmdName, "kick")) { - xr_sprintf(NewCmd, "%s %s", *st.translate("mp_kick"), CmdParams[0]); + xr_sprintf(NewCmd, "%s %s", st.translate("mp_kick").c_str(), CmdParams[0]); for (size_t i = 1; i < MAX_VOTE_PARAMS; i++) { if (xr_strlen(CmdParams[i])) @@ -832,7 +832,7 @@ void game_cl_Deathmatch::OnVoteStart(NET_Packet& P) } else if (!xr_strcmp(CmdName, "ban")) { - xr_sprintf(NewCmd, "%s %s", *st.translate("mp_ban"), CmdParams[0]); + xr_sprintf(NewCmd, "%s %s", st.translate("mp_ban").c_str(), CmdParams[0]); for (size_t i = 1; i < MAX_VOTE_PARAMS; i++) { if (xr_strlen(CmdParams[i])) @@ -844,15 +844,15 @@ void game_cl_Deathmatch::OnVoteStart(NET_Packet& P) } else if (!xr_strcmp(CmdName, "changemap")) { - xr_sprintf(NewCmd, "%s %s", *st.translate("mp_change_map"), *st.translate(CmdParams[0])); + xr_sprintf(NewCmd, "%s %s", st.translate("mp_change_map").c_str(), st.translate(CmdParams[0]).c_str()); } else if (!xr_strcmp(CmdName, "changeweather")) { - xr_sprintf(NewCmd, "%s %s", *st.translate("mp_change_weather"), *st.translate(CmdParams[0])); + xr_sprintf(NewCmd, "%s %s", st.translate("mp_change_weather").c_str(), st.translate(CmdParams[0]).c_str()); } string1024 VoteStr; - xr_sprintf(VoteStr, *st.translate("mp_voting_started"), NewCmd, Player); + xr_sprintf(VoteStr, st.translate("mp_voting_started").c_str(), NewCmd, Player); m_game_ui->SetVoteMessage(VoteStr); m_game_ui->SetVoteTimeResultMsg(""); diff --git a/src/xrGame/game_cl_deathmatch_buywnd.cpp b/src/xrGame/game_cl_deathmatch_buywnd.cpp index 0538e119eca..ccc34d0ad0f 100644 --- a/src/xrGame/game_cl_deathmatch_buywnd.cpp +++ b/src/xrGame/game_cl_deathmatch_buywnd.cpp @@ -279,7 +279,7 @@ void game_cl_Deathmatch::CheckItem(PIItem pItem, PRESET_ITEMS* pPresetItems, BOO return; u8 SlotID, ItemID; - pCurBuyMenu->GetWeaponIndexByName(*pItem->object().cNameSect(), SlotID, ItemID); + pCurBuyMenu->GetWeaponIndexByName(pItem->object().cNameSect().c_str(), SlotID, ItemID); if (SlotID == 0xff || ItemID == 0xff) return; s16 BigID = GetBuyMenuItemIndex(SlotID, ItemID); @@ -318,7 +318,7 @@ void game_cl_Deathmatch::CheckItem(PIItem pItem, PRESET_ITEMS* pPresetItems, BOO { if (pWeapon->ScopeAttachable()) { - pCurBuyMenu->GetWeaponIndexByName(*pWeapon->GetScopeName(), SlotID, ItemID); + pCurBuyMenu->GetWeaponIndexByName(pWeapon->GetScopeName().c_str(), SlotID, ItemID); if (SlotID != 0xff && ItemID != 0xff) { if (pWeapon->IsScopeAttached()) @@ -336,7 +336,7 @@ void game_cl_Deathmatch::CheckItem(PIItem pItem, PRESET_ITEMS* pPresetItems, BOO if (pWeapon->GrenadeLauncherAttachable()) { - pCurBuyMenu->GetWeaponIndexByName(*pWeapon->GetGrenadeLauncherName(), SlotID, ItemID); + pCurBuyMenu->GetWeaponIndexByName(pWeapon->GetGrenadeLauncherName().c_str(), SlotID, ItemID); if (SlotID != 0xff && ItemID != 0xff) { if (pWeapon->IsGrenadeLauncherAttached()) @@ -356,7 +356,7 @@ void game_cl_Deathmatch::CheckItem(PIItem pItem, PRESET_ITEMS* pPresetItems, BOO if (pWeapon->SilencerAttachable()) { - pCurBuyMenu->GetWeaponIndexByName(*pWeapon->GetSilencerName(), SlotID, ItemID); + pCurBuyMenu->GetWeaponIndexByName(pWeapon->GetSilencerName().c_str(), SlotID, ItemID); if (SlotID != 0xff && ItemID != 0xff) { if (pWeapon->IsSilencerAttached()) @@ -458,7 +458,7 @@ void game_cl_Deathmatch::LoadDefItemsForRank(IBuyWnd* pBuyMenu) const shared_str& ItemName = pBuyMenu->GetWeaponNameByIndex(pDefItem->SlotID, pDefItem->ItemID); if (!ItemName.size()) continue; - if (!xr_strcmp(*ItemName, "mp_wpn_knife")) + if (!xr_strcmp(ItemName.c_str(), "mp_wpn_knife")) continue; if (!pSettings->line_exist(ItemName, "ammo_class")) continue; diff --git a/src/xrGame/game_cl_teamdeathmatch.cpp b/src/xrGame/game_cl_teamdeathmatch.cpp index eb93ac084ee..5ee44867dff 100644 --- a/src/xrGame/game_cl_teamdeathmatch.cpp +++ b/src/xrGame/game_cl_teamdeathmatch.cpp @@ -122,12 +122,12 @@ void game_cl_TeamDeathmatch::TranslateGameMessage(u32 msg, NET_Packet& P) xr_sprintf(Text, "%s%s %s%s %s%s", "", // no color - PlayerName, Color_Main, *StringTable().translate("mp_joined"), CTeamInfo::GetTeam_color_tag(int(Team)), + PlayerName, Color_Main, StringTable().translate("mp_joined").c_str(), CTeamInfo::GetTeam_color_tag(int(Team)), CTeamInfo::GetTeam_name(int(Team))); if (CurrentGameUI()) CurrentGameUI()->CommonMessageOut(Text); //--------------------------------------- - Msg("%s %s %s", PlayerName, *StringTable().translate("mp_joined"), CTeamInfo::GetTeam_name(int(Team))); + Msg("%s %s %s", PlayerName, StringTable().translate("mp_joined").c_str(), CTeamInfo::GetTeam_name(int(Team))); } break; @@ -143,12 +143,12 @@ void game_cl_TeamDeathmatch::TranslateGameMessage(u32 msg, NET_Packet& P) break; xr_sprintf(Text, "%s%s %s%s %s%s", CTeamInfo::GetTeam_color_tag(int(OldTeam)), pPlayer->getName(), Color_Main, - *StringTable().translate("mp_switched_to"), CTeamInfo::GetTeam_color_tag(int(NewTeam)), + StringTable().translate("mp_switched_to").c_str(), CTeamInfo::GetTeam_color_tag(int(NewTeam)), CTeamInfo::GetTeam_name(int(NewTeam))); if (CurrentGameUI()) CurrentGameUI()->CommonMessageOut(Text); //--------------------------------------- - Msg("%s *s %s", pPlayer->getName(), *StringTable().translate("mp_switched_to"), CTeamInfo::GetTeam_name(int(NewTeam))); + Msg("%s *s %s", pPlayer->getName(), StringTable().translate("mp_switched_to").c_str(), CTeamInfo::GetTeam_name(int(NewTeam))); } break; @@ -436,7 +436,7 @@ void game_cl_TeamDeathmatch::shedule_Update(u32 dt) !local_player->testFlag(GAME_PLAYER_FLAG_VERY_VERY_DEAD)) { string1024 msg; - xr_sprintf(msg, *StringTable().translate("mp_press_to_buy"), "B"); + xr_sprintf(msg, StringTable().translate("mp_press_to_buy").c_str(), "B"); if (m_game_ui) m_game_ui->SetBuyMsgCaption(msg); m_bBuyEnabled = true; diff --git a/src/xrGame/game_sv_artefacthunt.cpp b/src/xrGame/game_sv_artefacthunt.cpp index cfaefe7ee67..61a0452e2cd 100644 --- a/src/xrGame/game_sv_artefacthunt.cpp +++ b/src/xrGame/game_sv_artefacthunt.cpp @@ -561,7 +561,7 @@ BOOL game_sv_ArtefactHunt::OnTouch(u16 eid_who, u16 eid_what, BOOL bForced) if (W) { //--------------------------------------------------------------- - if (IsBuyableItem(*e_what->s_name)) + if (IsBuyableItem(e_what->s_name.c_str())) return TRUE; //--------------------------------------------------------------- } @@ -1437,12 +1437,12 @@ void game_sv_ArtefactHunt::ReadOptions(shared_str& options) { inherited::ReadOptions(options); //------------------------------- - g_sv_ah_dwArtefactRespawnDelta = get_option_i(*options, "ardelta", g_sv_ah_dwArtefactRespawnDelta); // sec - g_sv_ah_dwArtefactsNum = get_option_i(*options, "anum", g_sv_ah_dwArtefactsNum); - g_sv_ah_dwArtefactStayTime = get_option_i(*options, "astime", g_sv_ah_dwArtefactStayTime); + g_sv_ah_dwArtefactRespawnDelta = get_option_i(options.c_str(), "ardelta", g_sv_ah_dwArtefactRespawnDelta); // sec + g_sv_ah_dwArtefactsNum = get_option_i(options.c_str(), "anum", g_sv_ah_dwArtefactsNum); + g_sv_ah_dwArtefactStayTime = get_option_i(options.c_str(), "astime", g_sv_ah_dwArtefactStayTime); g_sv_dm_dwFragLimit = 0; //---------------------------------------------------------------------------- - g_sv_ah_iReinforcementTime = get_option_i(*options, "reinf", g_sv_ah_iReinforcementTime); + g_sv_ah_iReinforcementTime = get_option_i(options.c_str(), "reinf", g_sv_ah_iReinforcementTime); if (g_sv_ah_iReinforcementTime < 0) g_sv_ah_iReinforcementTime = -1; //---------------------------------------------------------------------------- diff --git a/src/xrGame/game_sv_capture_the_artefact.cpp b/src/xrGame/game_sv_capture_the_artefact.cpp index ddc95ace7f3..f99dd96c07a 100644 --- a/src/xrGame/game_sv_capture_the_artefact.cpp +++ b/src/xrGame/game_sv_capture_the_artefact.cpp @@ -1279,7 +1279,7 @@ void game_sv_CaptureTheArtefact::SpawnWeaponsForActor(CSE_Abstract* pE, game_Pla { u16 ItemID = ps->pItemList.front(); SpawnWeapon4Actor( - pA->ID, *m_strWeaponsData->GetItemName(ItemID & 0x00FF), u8((ItemID & 0xFF00) >> 0x08), ps->pItemList); + pA->ID, m_strWeaponsData->GetItemName(ItemID & 0x00FF).c_str(), u8((ItemID & 0xFF00) >> 0x08), ps->pItemList); // Game().m_WeaponUsageStatistic->OnWeaponBought(ps, *m_strWeaponsData->GetItemName(ItemID& 0x00FF)); R_ASSERT(ps->pItemList.size()); ps->pItemList.erase(ps->pItemList.begin()); @@ -2453,37 +2453,37 @@ void game_sv_CaptureTheArtefact::ReadOptions(shared_str& options) inherited::ReadOptions(options); //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - g_sv_dm_bAnomaliesEnabled = (get_option_i(*options, "ans", (isAnomaliesEnabled() ? 1 : 0)) != 0); - g_sv_dm_dwAnomalySetLengthTime = get_option_i(*options, "anslen", g_sv_dm_dwAnomalySetLengthTime); // in (min) - g_sv_dm_bPDAHunt = (get_option_i(*options, "pdahunt", (isPDAHuntEnabled() ? 1 : 0)) != 0); - g_sv_dm_bDamageBlockIndicators = (get_option_i(*options, "dmbi", (g_sv_dm_bDamageBlockIndicators ? 1 : 0)) != 0); - g_sv_dm_dwWarmUp_MaxTime = get_option_i(*options, "warmup", g_sv_dm_dwWarmUp_MaxTime); + g_sv_dm_bAnomaliesEnabled = (get_option_i(options.c_str(), "ans", (isAnomaliesEnabled() ? 1 : 0)) != 0); + g_sv_dm_dwAnomalySetLengthTime = get_option_i(options.c_str(), "anslen", g_sv_dm_dwAnomalySetLengthTime); // in (min) + g_sv_dm_bPDAHunt = (get_option_i(options.c_str(), "pdahunt", (isPDAHuntEnabled() ? 1 : 0)) != 0); + g_sv_dm_bDamageBlockIndicators = (get_option_i(options.c_str(), "dmbi", (g_sv_dm_bDamageBlockIndicators ? 1 : 0)) != 0); + g_sv_dm_dwWarmUp_MaxTime = get_option_i(options.c_str(), "warmup", g_sv_dm_dwWarmUp_MaxTime); //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //------------------------------------------------------------------------- - g_sv_tdm_bAutoTeamBalance = get_option_i(*options, "abalance", (g_sv_tdm_bAutoTeamBalance ? 1 : 0)) != 0; - g_sv_tdm_bAutoTeamSwap = get_option_i(*options, "aswap", (g_sv_tdm_bAutoTeamSwap ? 1 : 0)) != 0; - g_sv_tdm_bFriendlyIndicators = get_option_i(*options, "fi", (g_sv_tdm_bFriendlyIndicators ? 1 : 0)) != 0; - g_sv_tdm_bFriendlyNames = get_option_i(*options, "fn", (g_sv_tdm_bFriendlyNames ? 1 : 0)) != 0; + g_sv_tdm_bAutoTeamBalance = get_option_i(options.c_str(), "abalance", (g_sv_tdm_bAutoTeamBalance ? 1 : 0)) != 0; + g_sv_tdm_bAutoTeamSwap = get_option_i(options.c_str(), "aswap", (g_sv_tdm_bAutoTeamSwap ? 1 : 0)) != 0; + g_sv_tdm_bFriendlyIndicators = get_option_i(options.c_str(), "fi", (g_sv_tdm_bFriendlyIndicators ? 1 : 0)) != 0; + g_sv_tdm_bFriendlyNames = get_option_i(options.c_str(), "fn", (g_sv_tdm_bFriendlyNames ? 1 : 0)) != 0; - float fFF = get_option_f(*options, "ffire", g_sv_tdm_fFriendlyFireModifier); + float fFF = get_option_f(options.c_str(), "ffire", g_sv_tdm_fFriendlyFireModifier); g_sv_tdm_fFriendlyFireModifier = fFF; //------------------------------------------------------------------------- - g_sv_ah_dwArtefactsNum = get_option_i(*options, "anum", g_sv_ah_dwArtefactsNum); + g_sv_ah_dwArtefactsNum = get_option_i(options.c_str(), "anum", g_sv_ah_dwArtefactsNum); //---------------------------------------------------------------------------- - g_sv_ah_iReinforcementTime = get_option_i(*options, "reinf", g_sv_ah_iReinforcementTime); + g_sv_ah_iReinforcementTime = get_option_i(options.c_str(), "reinf", g_sv_ah_iReinforcementTime); if (g_sv_ah_iReinforcementTime <= 0) g_sv_ah_iReinforcementTime = 1; //---------------------------------------------------------------------------- - g_sv_cta_dwInvincibleTime = get_option_i(*options, "dmgblock", g_sv_cta_dwInvincibleTime); // in (sec) - g_sv_cta_artefactReturningTime = get_option_i(*options, "artrettime", g_sv_cta_artefactReturningTime); // in (sec) - g_sv_cta_activatedArtefactRet = get_option_i(*options, "actret", g_sv_cta_activatedArtefactRet); // in (sec) + g_sv_cta_dwInvincibleTime = get_option_i(options.c_str(), "dmgblock", g_sv_cta_dwInvincibleTime); // in (sec) + g_sv_cta_artefactReturningTime = get_option_i(options.c_str(), "artrettime", g_sv_cta_artefactReturningTime); // in (sec) + g_sv_cta_activatedArtefactRet = get_option_i(options.c_str(), "actret", g_sv_cta_activatedArtefactRet); // in (sec) m_bSpectatorMode = false; - if (!GEnv.isDedicatedServer && (get_option_i(*options, "spectr", -1) != -1)) + if (!GEnv.isDedicatedServer && (get_option_i(options.c_str(), "spectr", -1) != -1)) { m_bSpectatorMode = true; - m_dwSM_SwitchDelta = get_option_i(*options, "spectr", 0) * 1000; + m_dwSM_SwitchDelta = get_option_i(options.c_str(), "spectr", 0) * 1000; if (m_dwSM_SwitchDelta < 1000) m_dwSM_SwitchDelta = 1000; }; diff --git a/src/xrGame/game_sv_deathmatch.cpp b/src/xrGame/game_sv_deathmatch.cpp index 978f132eb9f..85cd98390c9 100644 --- a/src/xrGame/game_sv_deathmatch.cpp +++ b/src/xrGame/game_sv_deathmatch.cpp @@ -1076,7 +1076,7 @@ void game_sv_Deathmatch::SpawnWeaponsForActor(CSE_Abstract* pE, game_PlayerState Msg("--- Server: spawning item [%d] for actor [%s]", ItemID, ps->getName()); #endif // #ifdef DEBUG SpawnWeapon4Actor( - pA->ID, *m_strWeaponsData->GetItemName(ItemID & 0x00FF), u8((ItemID & 0xFF00) >> 0x08), ps->pItemList); + pA->ID, m_strWeaponsData->GetItemName(ItemID & 0x00FF).c_str(), u8((ItemID & 0xFF00) >> 0x08), ps->pItemList); // Game().m_WeaponUsageStatistic->OnWeaponBought(ps, *m_strWeaponsData->GetItemName(ItemID& 0x00FF)); R_ASSERT(ps->pItemList.size()); ps->pItemList.erase(ps->pItemList.begin()); @@ -1729,7 +1729,7 @@ BOOL game_sv_Deathmatch::OnTouch(u16 eid_who, u16 eid_what, BOOL bForced) } }; //--------------------------------------------------------------- - if (IsBuyableItem(*e_what->s_name)) + if (IsBuyableItem(e_what->s_name.c_str())) return TRUE; //--------------------------------------------------------------- }; @@ -2169,28 +2169,28 @@ void game_sv_Deathmatch::ReadOptions(shared_str& options) { inherited::ReadOptions(options); //------------------------------- - g_sv_dm_dwForceRespawn = get_option_i(*options, "frcrspwn", g_sv_dm_dwForceRespawn); - g_sv_dm_dwFragLimit = get_option_i(*options, "fraglimit", g_sv_dm_dwFragLimit); - g_sv_dm_dwTimeLimit = get_option_i(*options, "timelimit", g_sv_dm_dwTimeLimit); // in (min) - g_sv_dm_dwDamageBlockTime = get_option_i(*options, "dmgblock", g_sv_dm_dwDamageBlockTime); // in (sec) + g_sv_dm_dwForceRespawn = get_option_i(options.c_str(), "frcrspwn", g_sv_dm_dwForceRespawn); + g_sv_dm_dwFragLimit = get_option_i(options.c_str(), "fraglimit", g_sv_dm_dwFragLimit); + g_sv_dm_dwTimeLimit = get_option_i(options.c_str(), "timelimit", g_sv_dm_dwTimeLimit); // in (min) + g_sv_dm_dwDamageBlockTime = get_option_i(options.c_str(), "dmgblock", g_sv_dm_dwDamageBlockTime); // in (sec) //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - g_sv_dm_bDamageBlockIndicators = (get_option_i(*options, "dmbi", (g_sv_dm_bDamageBlockIndicators ? 1 : 0)) != 0); + g_sv_dm_bDamageBlockIndicators = (get_option_i(options.c_str(), "dmbi", (g_sv_dm_bDamageBlockIndicators ? 1 : 0)) != 0); //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - g_sv_dm_bAnomaliesEnabled = (get_option_i(*options, "ans", (IsAnomaliesEnabled() ? 1 : 0)) != 0); - g_sv_dm_dwAnomalySetLengthTime = get_option_i(*options, "anslen", g_sv_dm_dwAnomalySetLengthTime); // in (min) + g_sv_dm_bAnomaliesEnabled = (get_option_i(options.c_str(), "ans", (IsAnomaliesEnabled() ? 1 : 0)) != 0); + g_sv_dm_dwAnomalySetLengthTime = get_option_i(options.c_str(), "anslen", g_sv_dm_dwAnomalySetLengthTime); // in (min) //----------------------------------------------------------------------- m_bSpectatorMode = false; - if (!GEnv.isDedicatedServer && (get_option_i(*options, "spectr", -1) != -1)) + if (!GEnv.isDedicatedServer && (get_option_i(options.c_str(), "spectr", -1) != -1)) { m_bSpectatorMode = true; - m_dwSM_SwitchDelta = get_option_i(*options, "spectr", 0) * 1000; + m_dwSM_SwitchDelta = get_option_i(options.c_str(), "spectr", 0) * 1000; if (m_dwSM_SwitchDelta < 1000) m_dwSM_SwitchDelta = 1000; }; //------------------------------------------------------------------------- - g_sv_dm_dwWarmUp_MaxTime = get_option_i(*options, "warmup", g_sv_dm_dwWarmUp_MaxTime); + g_sv_dm_dwWarmUp_MaxTime = get_option_i(options.c_str(), "warmup", g_sv_dm_dwWarmUp_MaxTime); - g_sv_dm_bPDAHunt = (get_option_i(*options, "pdahunt", (g_sv_dm_bPDAHunt ? 1 : 0)) != 0); + g_sv_dm_bPDAHunt = (get_option_i(options.c_str(), "pdahunt", (g_sv_dm_bPDAHunt ? 1 : 0)) != 0); }; [[maybe_unused]] void game_sv_Deathmatch::ConsoleCommands_Create() {} diff --git a/src/xrGame/game_sv_mp.cpp b/src/xrGame/game_sv_mp.cpp index d9be6fb1425..e3953ccbfd7 100644 --- a/src/xrGame/game_sv_mp.cpp +++ b/src/xrGame/game_sv_mp.cpp @@ -1346,7 +1346,7 @@ void game_sv_mp::SetPlayersDefItems(game_PlayerState* ps) continue; shared_str WeaponName = m_strWeaponsData->GetItemName((*pItemID) & 0x00FF); // strconcat(ItemStr, "def_item_repl_", pWpnS->WeaponName.c_str()); - strconcat(sizeof(ItemStr), ItemStr, "def_item_repl_", *WeaponName); + strconcat(sizeof(ItemStr), ItemStr, "def_item_repl_", WeaponName.c_str()); if (!pSettings->line_exist(RankStr, ItemStr)) continue; @@ -1369,7 +1369,7 @@ void game_sv_mp::SetPlayersDefItems(game_PlayerState* ps) continue; shared_str WeaponName = m_strWeaponsData->GetItemName((*pItemID) & 0x00FF); - if (!xr_strcmp(*WeaponName, "mp_wpn_knife")) + if (!xr_strcmp(WeaponName.c_str(), "mp_wpn_knife")) continue; u16 AmmoID = u16(-1); if (pSettings->line_exist(WeaponName, "ammo_class")) @@ -1773,14 +1773,14 @@ void game_sv_mp::ReadOptions(shared_str& options) inherited::ReadOptions(options); u8 SpectatorModes = SpectatorModes_Pack(); - SpectatorModes = u8(get_option_i(*options, "spectrmds", s32(SpectatorModes)) & 0x00ff); + SpectatorModes = u8(get_option_i(options.c_str(), "spectrmds", s32(SpectatorModes)) & 0x00ff); SpectatorModes_UnPack(SpectatorModes); - g_sv_dwMaxClientPing = get_option_i(*options, "maxping", g_sv_dwMaxClientPing); + g_sv_dwMaxClientPing = get_option_i(options.c_str(), "maxping", g_sv_dwMaxClientPing); string64 StartTime, TimeFactor; - xr_strcpy(StartTime, get_option_s(*options, "estime", "9:00")); - xr_strcpy(TimeFactor, get_option_s(*options, "etimef", "1")); + xr_strcpy(StartTime, get_option_s(options.c_str(), "estime", "9:00")); + xr_strcpy(TimeFactor, get_option_s(options.c_str(), "etimef", "1")); u32 hours = 0, mins = 0; sscanf(StartTime, "%d:%d", &hours, &mins); diff --git a/src/xrGame/game_sv_single.cpp b/src/xrGame/game_sv_single.cpp index c7be6bc8d7b..39412469c18 100644 --- a/src/xrGame/game_sv_single.cpp +++ b/src/xrGame/game_sv_single.cpp @@ -19,7 +19,7 @@ game_sv_Single::~game_sv_Single() { delete_data(m_alife_simulator); } void game_sv_Single::Create(shared_str& options) { inherited::Create(options); - if (strstr(*options, "/alife")) + if (strstr(options.c_str(), "/alife")) m_alife_simulator = xr_new(&server(), &options); switch_Phase(GAME_PHASE_INPROGRESS); } @@ -97,8 +97,8 @@ BOOL game_sv_Single::OnTouch(u16 eid_who, u16 eid_what, BOOL bForced) #ifdef DEBUG else if (psAI_Flags.test(aiALife)) { - Msg("Cannot attach object [%s][%s][%d] to object [%s][%s][%d]", e_what->name_replace(), *e_what->s_name, - e_what->ID, e_who->name_replace(), *e_who->s_name, e_who->ID); + Msg("Cannot attach object [%s][%s][%d] to object [%s][%s][%d]", e_what->name_replace(), e_what->s_name.c_str(), + e_what->ID, e_who->name_replace(), e_who->s_name.c_str(), e_who->ID); } #endif } @@ -146,7 +146,7 @@ void game_sv_Single::OnDetach(u16 eid_who, u16 eid_what) else if (psAI_Flags.test(aiALife)) { Msg("Cannot detach object [%s][%s][%d] from object [%s][%s][%d]", - l_tpALifeInventoryItem->base()->name_replace(), *l_tpALifeInventoryItem->base()->s_name, + l_tpALifeInventoryItem->base()->name_replace(), l_tpALifeInventoryItem->base()->s_name.c_str(), l_tpALifeInventoryItem->base()->ID, l_tpDynamicObject->base()->name_replace(), l_tpDynamicObject->base()->s_name.c_str(), l_tpDynamicObject->ID); } @@ -243,7 +243,7 @@ bool game_sv_Single::load_game(NET_Packet& net_packet, ClientID sender) return (inherited::load_game(net_packet, sender)); shared_str game_name; net_packet.r_stringZ(game_name); - return (alife().load_game(*game_name, true)); + return (alife().load_game(game_name.c_str(), true)); } void game_sv_Single::reload_game(NET_Packet& net_packet, ClientID sender) {} diff --git a/src/xrGame/game_sv_teamdeathmatch.cpp b/src/xrGame/game_sv_teamdeathmatch.cpp index 0739b1814d4..f255babd991 100644 --- a/src/xrGame/game_sv_teamdeathmatch.cpp +++ b/src/xrGame/game_sv_teamdeathmatch.cpp @@ -561,12 +561,12 @@ void game_sv_TeamDeathmatch::ReadOptions(shared_str& options) { inherited::ReadOptions(options); //------------------------------- - g_sv_tdm_bAutoTeamBalance = get_option_i(*options, "abalance", (g_sv_tdm_bAutoTeamBalance ? 1 : 0)) != 0; - g_sv_tdm_bAutoTeamSwap = get_option_i(*options, "aswap", (g_sv_tdm_bAutoTeamSwap ? 1 : 0)) != 0; - g_sv_tdm_bFriendlyIndicators = get_option_i(*options, "fi", (g_sv_tdm_bFriendlyIndicators ? 1 : 0)) != 0; - g_sv_tdm_bFriendlyNames = get_option_i(*options, "fn", (g_sv_tdm_bFriendlyNames ? 1 : 0)) != 0; + g_sv_tdm_bAutoTeamBalance = get_option_i(options.c_str(), "abalance", (g_sv_tdm_bAutoTeamBalance ? 1 : 0)) != 0; + g_sv_tdm_bAutoTeamSwap = get_option_i(options.c_str(), "aswap", (g_sv_tdm_bAutoTeamSwap ? 1 : 0)) != 0; + g_sv_tdm_bFriendlyIndicators = get_option_i(options.c_str(), "fi", (g_sv_tdm_bFriendlyIndicators ? 1 : 0)) != 0; + g_sv_tdm_bFriendlyNames = get_option_i(options.c_str(), "fn", (g_sv_tdm_bFriendlyNames ? 1 : 0)) != 0; - float fFF = get_option_f(*options, "ffire", g_sv_tdm_fFriendlyFireModifier); + float fFF = get_option_f(options.c_str(), "ffire", g_sv_tdm_fFriendlyFireModifier); g_sv_tdm_fFriendlyFireModifier = fFF; } diff --git a/src/xrGame/inventory_item_upgrade.cpp b/src/xrGame/inventory_item_upgrade.cpp index c5439654de0..0e5f7895e71 100644 --- a/src/xrGame/inventory_item_upgrade.cpp +++ b/src/xrGame/inventory_item_upgrade.cpp @@ -170,7 +170,7 @@ void CInventoryItem::net_Spawn_install_upgrades(CSE_Abstract* DC) // net_Spawn for (const auto& upgrade : saved_upgrades) { - ai().alife().inventory_upgrade_manager().upgrade_install(*this, *upgrade, true); + ai().alife().inventory_upgrade_manager().upgrade_install(*this, upgrade.c_str(), true); } } diff --git a/src/xrGame/inventory_owner_info.cpp b/src/xrGame/inventory_owner_info.cpp index 08df5e1df32..b1fbe594825 100644 --- a/src/xrGame/inventory_owner_info.cpp +++ b/src/xrGame/inventory_owner_info.cpp @@ -54,7 +54,7 @@ bool CInventoryOwner::OnReceiveInfo(shared_str info_id) const #ifdef DEBUG if (psAI_Flags.test(aiInfoPortion)) - Msg("[%s] Received Info [%s]", Name(), *info_id); + Msg("[%s] Received Info [%s]", Name(), info_id.c_str()); #endif //Запустить скриптовый callback diff --git a/src/xrGame/item_manager.cpp b/src/xrGame/item_manager.cpp index 14cd72bce3f..5244a7863b0 100644 --- a/src/xrGame/item_manager.cpp +++ b/src/xrGame/item_manager.cpp @@ -72,8 +72,8 @@ bool CItemManager::useful(const CGameObject* object) const float CItemManager::do_evaluate(const CGameObject* object) const { - VERIFY3(m_object->movement().restrictions().accessible(object->ai_location().level_vertex_id()), *m_object->cName(), - *object->cName()); + VERIFY3(m_object->movement().restrictions().accessible(object->ai_location().level_vertex_id()), m_object->cName().c_str(), + object->cName().c_str()); return (m_object->evaluate(this, object)); } @@ -94,13 +94,13 @@ void CItemManager::update() OBJECTS::const_iterator E = m_objects.end(); for (; I != E; ++I) VERIFY3(m_object->movement().restrictions().accessible((*I)->ai_location().level_vertex_id()), - *m_object->cName(), *(*I)->cName()); + m_object->cName().c_str(), (*I)->cName().c_str()); #endif // DEBUG inherited::update(); VERIFY3(!selected() || m_object->movement().restrictions().accessible(selected()->ai_location().level_vertex_id()), - *m_object->cName(), selected() ? *selected()->cName() : ""); + m_object->cName().c_str(), selected() ? selected()->cName().c_str() : ""); STOP_PROFILE } diff --git a/src/xrGame/level_debug.cpp b/src/xrGame/level_debug.cpp index 914410d7e01..bd765ea80ca 100644 --- a/src/xrGame/level_debug.cpp +++ b/src/xrGame/level_debug.cpp @@ -258,7 +258,7 @@ struct DrawInfoPredicate UI().Font().pFontMedium->SetAligment(CGameFont::alLeft); UI().Font().pFontMedium->SetColor(s.color); UI().Font().pFontMedium->OutSet(x, y -= delta_height); - UI().Font().pFontMedium->OutNext(*(s.text)); + UI().Font().pFontMedium->OutNext(s.text.c_str()); } }; @@ -285,7 +285,7 @@ struct DrawTextPredicate UI().Font().pFontMedium->SetAligment(CGameFont::alLeft); UI().Font().pFontMedium->SetColor(s.color); UI().Font().pFontMedium->OutSet(s.x, s.y); - UI().Font().pFontMedium->OutNext(*(s.text)); + UI().Font().pFontMedium->OutNext(s.text.c_str()); } }; diff --git a/src/xrGame/level_script.cpp b/src/xrGame/level_script.cpp index bc74c3e6366..204941cf8dd 100644 --- a/src/xrGame/level_script.cpp +++ b/src/xrGame/level_script.cpp @@ -94,7 +94,7 @@ CScriptGameObject* get_object_by_id(u16 id) return pGameObject->lua_game_object(); } -LPCSTR get_weather() { return *g_pGamePersistent->Environment().GetWeather(); } +LPCSTR get_weather() { return g_pGamePersistent->Environment().GetWeather().c_str(); } void set_weather(pcstr const weather_name, const bool forced) { if (!Device.editor_mode()) @@ -571,7 +571,7 @@ void stop_tutorial() g_tutorial->Stop(); } -LPCSTR translate_string(LPCSTR str) { return *StringTable().translate(str); } +LPCSTR translate_string(LPCSTR str) { return StringTable().translate(str).c_str(); } bool has_active_tutotial() { return (g_tutorial != NULL); } // Alundaio: namespace level exports extension diff --git a/src/xrGame/level_sounds.cpp b/src/xrGame/level_sounds.cpp index 1edcfe201da..0417482447a 100644 --- a/src/xrGame/level_sounds.cpp +++ b/src/xrGame/level_sounds.cpp @@ -207,7 +207,7 @@ void CLevelSoundManager::Load() for (; it != end; ++it) { m_MusicTracks.push_back(SMusicTrack()); - m_MusicTracks.back().Load(*it->first, *it->second); + m_MusicTracks.back().Load(it->first.c_str(), it->second.c_str()); } } } diff --git a/src/xrGame/map_location.cpp b/src/xrGame/map_location.cpp index 219b4a69185..596a82e6d54 100644 --- a/src/xrGame/map_location.cpp +++ b/src/xrGame/map_location.cpp @@ -234,7 +234,7 @@ void CMapLocation::InitUserSpot(const shared_str& level_name, const Fvector& pos if (ai().get_alife()) { - const CGameGraph::SLevel& level = ai().game_graph().header().level(*level_name); + const CGameGraph::SLevel& level = ai().game_graph().header().level(level_name.c_str()); float min_dist = 128; // flt_max; GameGraph::_GRAPH_ID n = ai().game_graph().header().vertex_count(); @@ -253,7 +253,7 @@ void CMapLocation::InitUserSpot(const shared_str& level_name, const Fvector& pos } if (!ai().game_graph().vertex(m_cached.m_graphID)) { - Msg("qweasdd! Cannot assign game vertex for CUserDefinedMapLocation [map=%s]", *level_name); + Msg("qweasdd! Cannot assign game vertex for CUserDefinedMapLocation [map=%s]", level_name.c_str()); R_ASSERT(ai().game_graph().vertex(m_cached.m_graphID)); } } @@ -504,7 +504,7 @@ void CMapLocation::UpdateSpot(CUICustomMap* map, CMapSpot* sp) { // Msg("%d-%s",(*it),ai().game_graph().vertex(*it)); Msg("[%d] level[%s]", (*it), - *ai().game_graph().header().level(ai().game_graph().vertex(*it)->level_id()).name()); + ai().game_graph().header().level(ai().game_graph().vertex(*it)->level_id()).name().c_str()); } Msg("- Available LevelChangers:"); xr_vector::iterator lit, lit_e; @@ -515,7 +515,7 @@ void CMapLocation::UpdateSpot(CUICustomMap* map, CMapSpot* sp) Msg("[%d]", gid); Fvector p = ai().game_graph().vertex(gid)->level_point(); Msg("lch_name=%s pos=%f %f %f", - *ai().game_graph().header().level(ai().game_graph().vertex(gid)->level_id()).name(), p.x, p.y, + ai().game_graph().header().level(ai().game_graph().vertex(gid)->level_id()).name().c_str(), p.x, p.y, p.z); } }; @@ -532,7 +532,7 @@ void CMapLocation::UpdateSpot(CUICustomMap* map, CMapSpot* sp) xr_vector::reverse_iterator it_e = map_point_path.rend(); for (; (it != it_e) && (!bDone); ++it) { - if (*ai().game_graph().header().level(ai().game_graph().vertex(*it)->level_id()).name() == + if (ai().game_graph().header().level(ai().game_graph().vertex(*it)->level_id()).name().c_str() == Level().name()) break; } @@ -771,7 +771,7 @@ Fvector2 CMapLocation::SpotSize() } CRelationMapLocation::CRelationMapLocation(const shared_str& type, u16 object_id, u16 pInvOwnerActorID) - : CMapLocation(*type, object_id), m_last_relation() + : CMapLocation(type.c_str(), object_id), m_last_relation() { m_curr_spot_name = type; m_pInvOwnerActorID = pInvOwnerActorID; @@ -829,7 +829,7 @@ bool CRelationMapLocation::Update() if (m_curr_spot_name != sname) { - LoadSpot(*sname); + LoadSpot(sname.c_str()); m_curr_spot_name = sname; } // update visibility @@ -924,6 +924,6 @@ void CRelationMapLocation::UpdateLevelMap(CUICustomMap* map) void CRelationMapLocation::Dump() { inherited::Dump(); - Msg("--CRelationMapLocation m_curr_spot_name=[%s]", *m_curr_spot_name); + Msg("--CRelationMapLocation m_curr_spot_name=[%s]", m_curr_spot_name.c_str()); } #endif diff --git a/src/xrGame/map_manager.cpp b/src/xrGame/map_manager.cpp index 132c09cd7f3..f0c7a262b05 100644 --- a/src/xrGame/map_manager.cpp +++ b/src/xrGame/map_manager.cpp @@ -49,7 +49,7 @@ void SLocationKey::load(IReader& stream) stream.r_stringZ(spot_type); stream.r_u8(); - location = xr_new(*spot_type, object_id); + location = xr_new(spot_type.c_str(), object_id); location->load(stream); } @@ -293,7 +293,7 @@ void CMapManager::Dump() auto it_e = Locations().end(); for (; it != it_e; ++it) { - Msg("spot_type=[%s] object_id=[%d]", *((*it).spot_type), (*it).object_id); + Msg("spot_type=[%s] object_id=[%d]", (*it).spot_type.c_str(), (*it).object_id); (*it).location->Dump(); } diff --git a/src/xrGame/material_manager.cpp b/src/xrGame/material_manager.cpp index cc386dbdc72..5df8bcc9c4e 100644 --- a/src/xrGame/material_manager.cpp +++ b/src/xrGame/material_manager.cpp @@ -34,7 +34,7 @@ BOOL debug_character_material_load = FALSE; void CMaterialManager::Load(LPCSTR section) { R_ASSERT3( - pSettings->line_exist(section, "material"), "Material not found in the section ", *(m_object->cNameSect())); + pSettings->line_exist(section, "material"), "Material not found in the section ", m_object->cNameSect().c_str()); m_my_material_idx = GMLib.GetMaterialIdx(pSettings->r_string(section, "material")); #ifdef DEBUG @@ -85,7 +85,7 @@ void CMaterialManager::update(float time_delta, float volume, float step_time, b VERIFY(GAMEMTL_NONE_IDX != m_my_material_idx); VERIFY(GAMEMTL_NONE_IDX != m_last_material_idx); SGameMtlPair* mtl_pair = GMLib.GetMaterialPairByIndices(m_my_material_idx, m_last_material_idx); - VERIFY3(mtl_pair, "Undefined material pair: ", *GMLib.GetMaterialByIdx(m_last_material_idx)->m_Name); + VERIFY3(mtl_pair, "Undefined material pair: ", GMLib.GetMaterialByIdx(m_last_material_idx)->m_Name.c_str()); Fvector position = m_object->Position(); if (m_movement_control->CharacterExist()) { diff --git a/src/xrGame/movement_manager.cpp b/src/xrGame/movement_manager.cpp index b166372312f..956405808e8 100644 --- a/src/xrGame/movement_manager.cpp +++ b/src/xrGame/movement_manager.cpp @@ -131,7 +131,7 @@ GameGraph::_GRAPH_ID CMovementManager::game_dest_vertex_id() const void CMovementManager::set_level_dest_vertex(u32 const& level_vertex_id) { - VERIFY2(restrictions().accessible(level_vertex_id), *object().cName()); + VERIFY2(restrictions().accessible(level_vertex_id), object().cName().c_str()); level_path().set_dest_vertex(level_vertex_id); m_path_actuality = m_path_actuality && level_path().actual(); } diff --git a/src/xrGame/moving_objects.cpp b/src/xrGame/moving_objects.cpp index 8529493cbe4..7aaf60d82e5 100644 --- a/src/xrGame/moving_objects.cpp +++ b/src/xrGame/moving_objects.cpp @@ -24,7 +24,7 @@ void moving_objects::on_level_load() void moving_objects::register_object(moving_object* moving_object) { VERIFY2(m_objects.find(moving_object) == m_objects.end(), - make_string("moving object %s is registers twice", *moving_object->id())); + make_string("moving object %s is registers twice", moving_object->id().c_str())); #ifdef DEBUG m_objects.insert(moving_object); @@ -37,7 +37,7 @@ void moving_objects::register_object(moving_object* moving_object) void moving_objects::unregister_object(moving_object* moving_object) { VERIFY2(m_objects.find(moving_object) != m_objects.end(), - make_string("moving object %s is not yet registered or unregisters twice", *moving_object->id())); + make_string("moving object %s is not yet registered or unregisters twice", moving_object->id().c_str())); #ifdef DEBUG m_objects.erase(m_objects.find(moving_object)); @@ -50,7 +50,7 @@ void moving_objects::unregister_object(moving_object* moving_object) void moving_objects::on_object_move(moving_object* moving_object) { VERIFY2(m_objects.find(moving_object) != m_objects.end(), - make_string("moving object %s is not yet registered", *moving_object->id())); + make_string("moving object %s is not yet registered", moving_object->id().c_str())); #pragma todo("this place can be optimized in case of slowdowns") VERIFY(m_tree); diff --git a/src/xrGame/moving_objects_dynamic_collision.cpp b/src/xrGame/moving_objects_dynamic_collision.cpp index 5ec59480ef0..c037dc5ce65 100644 --- a/src/xrGame/moving_objects_dynamic_collision.cpp +++ b/src/xrGame/moving_objects_dynamic_collision.cpp @@ -206,12 +206,12 @@ void moving_objects::resolve_collision(boxes& current, moving_object* object0, c } #endif // 0 VERIFY2( - object0->action_frame() != Device.dwFrame, make_string("%d %s", Device.dwFrame, *object0->object().cName())); - VERIFY2(object0->action_frame() < Device.dwFrame, make_string("%d %s", Device.dwFrame, *object0->object().cName())); + object0->action_frame() != Device.dwFrame, make_string("%d %s", Device.dwFrame, object0->object().cName().c_str())); + VERIFY2(object0->action_frame() < Device.dwFrame, make_string("%d %s", Device.dwFrame, object0->object().cName().c_str())); VERIFY2( - object1->action_frame() != Device.dwFrame, make_string("%d %s", Device.dwFrame, *object0->object().cName())); - VERIFY2(object1->action_frame() < Device.dwFrame, make_string("%d %s", Device.dwFrame, *object0->object().cName())); + object1->action_frame() != Device.dwFrame, make_string("%d %s", Device.dwFrame, object0->object().cName().c_str())); + VERIFY2(object1->action_frame() < Device.dwFrame, make_string("%d %s", Device.dwFrame, object0->object().cName().c_str())); bool first_time = (std::find_if(m_previous_collisions.begin(), m_previous_collisions.end(), collision_predicate(std::make_pair(object0, object1))) == m_previous_collisions.end()); @@ -235,7 +235,7 @@ void moving_objects::resolve_collision(boxes& current, moving_object* object0, c return; } - VERIFY2(false, make_string("NODEFAULT: [%s][%s]", *object0->object().cName(), *object1->object().cName())); + VERIFY2(false, make_string("NODEFAULT: [%s][%s]", object0->object().cName().c_str(), object1->object().cName().c_str())); } bool moving_objects::collided_dynamic(moving_object* object0, const Fvector& position0, moving_object* object1, diff --git a/src/xrGame/object_handler.cpp b/src/xrGame/object_handler.cpp index 7ecc3d4b2e5..0662efb94f1 100644 --- a/src/xrGame/object_handler.cpp +++ b/src/xrGame/object_handler.cpp @@ -42,9 +42,9 @@ void CObjectHandler::reinit(CAI_Stalker* object) m_hammer_is_clutched = false; planner().setup(object); IKinematics* kinematics = smart_cast(planner().m_object->Visual()); - m_r_hand = kinematics->LL_BoneID(pSettings->r_string(*planner().m_object->cNameSect(), "weapon_bone0")); - m_l_finger1 = kinematics->LL_BoneID(pSettings->r_string(*planner().m_object->cNameSect(), "weapon_bone1")); - m_r_finger2 = kinematics->LL_BoneID(pSettings->r_string(*planner().m_object->cNameSect(), "weapon_bone2")); + m_r_hand = kinematics->LL_BoneID(pSettings->r_string(planner().m_object->cNameSect().c_str(), "weapon_bone0")); + m_l_finger1 = kinematics->LL_BoneID(pSettings->r_string(planner().m_object->cNameSect().c_str(), "weapon_bone1")); + m_r_finger2 = kinematics->LL_BoneID(pSettings->r_string(planner().m_object->cNameSect().c_str(), "weapon_bone2")); m_strap_object_id = ALife::_OBJECT_ID(-1); m_strap_bone0 = -1; m_strap_bone1 = -1; @@ -103,7 +103,7 @@ void CObjectHandler::OnItemDrop(CInventoryItem* inventory_item, bool just_before CWeaponAmmo* weapon_ammo = smart_cast(inventory_item); if (weapon_ammo) { - Level().spawn_item(*weapon_ammo->cNameSect(), planner().object().Position(), + Level().spawn_item(weapon_ammo->cNameSect().c_str(), planner().object().Position(), planner().object().ai_location().level_vertex_id(), planner().object().ID()); m_item_to_spawn = weapon_ammo->cNameSect(); m_ammo_in_box_to_spawn = weapon_ammo->m_boxSize; @@ -159,7 +159,7 @@ void CObjectHandler::weapon_bones(int& b0, int& b1, int& b2) const return; } - THROW3(weapon->can_be_strapped(), "Cannot strap weapon", *weapon->cName()); + THROW3(weapon->can_be_strapped(), "Cannot strap weapon", weapon->cName().c_str()); if (weapon->ID() != m_strap_object_id) { @@ -196,7 +196,7 @@ void CObjectHandler::actualize_strap_mode(CWeapon* weapon) const return; } - THROW3(weapon->can_be_strapped(), "Cannot strap weapon", *weapon->cName()); + THROW3(weapon->can_be_strapped(), "Cannot strap weapon", weapon->cName().c_str()); // Msg ( "[%6d][%s] actualizing: weapon_strapped = true", Device.dwTimeGlobal, // planner().object().cName().c_str() ); weapon->strapped_mode(true); diff --git a/src/xrGame/object_handler_planner.cpp b/src/xrGame/object_handler_planner.cpp index 2e854b5be80..3c41c718b24 100644 --- a/src/xrGame/object_handler_planner.cpp +++ b/src/xrGame/object_handler_planner.cpp @@ -70,7 +70,7 @@ void CObjectHandlerPlanner::set_goal(MonsterSpace::EObjectAction object_action, if (m_use_log) { Msg("%6d : Active item %s", Device.dwTimeGlobal, object().inventory().ActiveItem() ? - *object().inventory().ActiveItem()->object().cName() : + object().inventory().ActiveItem()->object().cName().c_str() : "no active items"); Msg("%6d : Goal %s", Device.dwTimeGlobal, property2string(condition_id)); } @@ -121,7 +121,7 @@ LPCSTR CObjectHandlerPlanner::action2string(const _action_id_type& id) pstr S = m_temp_string; if (action_object_id(id) != 0xffff) if (Level().Objects.net_Find(action_object_id(id))) - xr_strcpy(m_temp_string, *Level().Objects.net_Find(action_object_id(id))->cName()); + xr_strcpy(m_temp_string, Level().Objects.net_Find(action_object_id(id))->cName().c_str()); else xr_strcpy(m_temp_string, "no_items"); else @@ -313,7 +313,7 @@ LPCSTR CObjectHandlerPlanner::property2string(const _condition_type& id) { if (action_object_id(id) != 0xffff) if (Level().Objects.net_Find(action_object_id(id))) - xr_strcpy(m_temp_string, *Level().Objects.net_Find(action_object_id(id))->cName()); + xr_strcpy(m_temp_string, Level().Objects.net_Find(action_object_id(id))->cName().c_str()); else xr_strcpy(m_temp_string, "no_items"); else diff --git a/src/xrGame/patrol_path_manager.cpp b/src/xrGame/patrol_path_manager.cpp index c1283d80be2..103ee8c2c10 100644 --- a/src/xrGame/patrol_path_manager.cpp +++ b/src/xrGame/patrol_path_manager.cpp @@ -32,20 +32,20 @@ static void show_restrictions(LPCSTR restrictions) bool show_restrictions(CRestrictedObject* object) { Msg("DEFAULT OUT RESTRICTIONS :"); - show_restrictions(*Level().space_restriction_manager().default_out_restrictions() ? - *Level().space_restriction_manager().default_out_restrictions() : + show_restrictions(Level().space_restriction_manager().default_out_restrictions().c_str() ? + Level().space_restriction_manager().default_out_restrictions().c_str() : ""); Msg("DEFAULT IN RESTRICTIONS :"); - show_restrictions(*Level().space_restriction_manager().default_in_restrictions() ? - *Level().space_restriction_manager().default_in_restrictions() : + show_restrictions(Level().space_restriction_manager().default_in_restrictions().c_str() ? + Level().space_restriction_manager().default_in_restrictions().c_str() : ""); Msg("OUT RESTRICTIONS :"); - show_restrictions(*object->out_restrictions() ? *object->out_restrictions() : ""); + show_restrictions(object->out_restrictions().c_str() ? object->out_restrictions().c_str() : ""); Msg("IN RESTRICTIONS :"); - show_restrictions(*object->in_restrictions() ? *object->in_restrictions() : ""); + show_restrictions(object->in_restrictions().c_str() ? object->in_restrictions().c_str() : ""); return (false); } @@ -106,26 +106,26 @@ void CPatrolPathManager::select_point(const Fvector& position, u32& dest_vertex_ case ePatrolStartTypeFirst: { vertex = m_path->vertex(0); - VERIFY3(accessible(vertex) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(accessible(vertex) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); break; } case ePatrolStartTypeLast: { vertex = m_path->vertex(m_path->vertices().size() - 1); - VERIFY3(accessible(vertex) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(accessible(vertex) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); break; } case ePatrolStartTypeNearest: { vertex = m_path->point(position, CAccessabilityEvaluator(this)); - VERIFY3(accessible(vertex) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(accessible(vertex) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); break; } case ePatrolStartTypePoint: { - VERIFY3(m_path->vertex(m_start_point_index), *m_path_name, *m_game_object->cName()); + VERIFY3(m_path->vertex(m_start_point_index), m_path_name.c_str(), m_game_object->cName().c_str()); vertex = m_path->vertex(m_start_point_index); - VERIFY3(accessible(vertex) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(accessible(vertex) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); break; } case ePatrolStartTypeNext: @@ -149,7 +149,7 @@ void CPatrolPathManager::select_point(const Fvector& position, u32& dest_vertex_ if (!vertex) vertex = m_path->point(position, CAccessabilityEvaluator(this)); - VERIFY3(accessible(vertex) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(accessible(vertex) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); break; } default: NODEFAULT; @@ -158,15 +158,15 @@ void CPatrolPathManager::select_point(const Fvector& position, u32& dest_vertex_ { // ugly HACK, just because Plecha asked... VERIFY2(vertex || show_restrictions(m_object), - make_string("any vertex in patrol path [%s] is inaccessible for object [%s]", *m_path_name, - *m_game_object->cName())); + make_string("any vertex in patrol path [%s] is inaccessible for object [%s]", m_path_name.c_str(), + m_game_object->cName().c_str())); dest_vertex_id = m_game_object->ai_location().level_vertex_id(); return; } R_ASSERT2(ai().level_graph().valid_vertex_id(vertex->data().level_vertex_id()), - make_string("patrol path[%s], point on path [%s],object [%s]", *m_path_name, *vertex->data().name(), - *m_game_object->cName())); + make_string("patrol path[%s], point on path [%s],object [%s]", m_path_name.c_str(), vertex->data().name().c_str(), + m_game_object->cName().c_str())); if (!m_path->vertex(m_prev_point_index)) m_prev_point_index = vertex->vertex_id(); @@ -196,7 +196,7 @@ void CPatrolPathManager::select_point(const Fvector& position, u32& dest_vertex_ } #endif } - VERIFY3(m_path->vertex(m_curr_point_index) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(m_path->vertex(m_curr_point_index) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); m_game_object->callback(GameObject::ePatrolPathInPoint)( m_game_object->lua_game_object(), u32(ScriptEntity::eActionTypeMovement), m_curr_point_index); @@ -279,13 +279,13 @@ void CPatrolPathManager::select_point(const Fvector& position, u32& dest_vertex_ } } - VERIFY3(m_path->vertex(target) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(m_path->vertex(target) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); m_prev_point_index = m_curr_point_index; m_curr_point_index = target; dest_vertex_id = m_path->vertex(m_curr_point_index)->data().level_vertex_id(); m_dest_position = m_path->vertex(m_curr_point_index)->data().position(); - VERIFY3(accessible(m_dest_position) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(accessible(m_dest_position) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); m_actuality = true; m_completed = false; } @@ -343,7 +343,7 @@ shared_str CPatrolPathManager::path_name() const if (!m_path) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "Path not specified (object %s)!", *m_game_object->cName()); + LuaMessageType::Error, "Path not specified (object %s)!", m_game_object->cName().c_str()); return (""); } VERIFY(m_path); @@ -355,14 +355,14 @@ void CPatrolPathManager::set_previous_point(int point_index) if (!m_path) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "Path not specified (object %s)!", *m_game_object->cName()); + LuaMessageType::Error, "Path not specified (object %s)!", m_game_object->cName().c_str()); return; } if (!m_path->vertex(point_index)) { GEnv.ScriptEngine->script_log(LuaMessageType::Error, "Start point violates path bounds %s (object %s)!", - *m_path_name, *m_game_object->cName()); + m_path_name.c_str(), m_game_object->cName().c_str()); return; } VERIFY(m_path); @@ -375,13 +375,13 @@ void CPatrolPathManager::set_start_point(int point_index) if (!m_path) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "Path not specified (object %s)!", *m_game_object->cName()); + LuaMessageType::Error, "Path not specified (object %s)!", m_game_object->cName().c_str()); return; } if (!m_path->vertex(point_index)) { GEnv.ScriptEngine->script_log(LuaMessageType::Error, "Start point violates path bounds %s (object %s)!", - *m_path_name, *m_game_object->cName()); + m_path_name.c_str(), m_game_object->cName().c_str()); return; } VERIFY(m_path); diff --git a/src/xrGame/physics_game.cpp b/src/xrGame/physics_game.cpp index 51c324f059b..ffc727b01d7 100644 --- a/src/xrGame/physics_game.cpp +++ b/src/xrGame/physics_game.cpp @@ -254,7 +254,7 @@ void TContactShotMark(CDB::TRI* T, dContactGeom* c) { SGameMtl* static_mtl = GMLib.GetMaterialByIdx(T->material); VERIFY(static_mtl); - LPCSTR ps_name = *mtl_pair->CollideParticles[::Random.randI(0, mtl_pair->CollideParticles.size())]; + LPCSTR ps_name = mtl_pair->CollideParticles[::Random.randI(0, mtl_pair->CollideParticles.size())].c_str(); play_particles(vel_cret, data, c, b_invert_normal, static_mtl, ps_name); } } diff --git a/src/xrGame/purchase_list.cpp b/src/xrGame/purchase_list.cpp index cce4ab77cd0..1242c363e77 100644 --- a/src/xrGame/purchase_list.cpp +++ b/src/xrGame/purchase_list.cpp @@ -48,8 +48,8 @@ void CPurchaseList::process(CInifile& ini_file, LPCSTR section, CInventoryOwner& void CPurchaseList::process( const CGameObject& owner, const shared_str& name, const u32& count, const float& probability) { - VERIFY3(count, "Invalid count for section in the purchase list", *name); - VERIFY3(!fis_zero(probability, EPS_S), "Invalid probability for section in the purchase list", *name); + VERIFY3(count, "Invalid count for section in the purchase list", name.c_str()); + VERIFY3(!fis_zero(probability, EPS_S), "Invalid probability for section in the purchase list", name.c_str()); const Fvector& position = owner.Position(); const u32& level_vertex_id = owner.ai_location().level_vertex_id(); @@ -62,7 +62,7 @@ void CPurchaseList::process( continue; ++j; - Level().spawn_item(*name, position, level_vertex_id, id, false); + Level().spawn_item(name.c_str(), position, level_vertex_id, id, false); } VERIFY3(m_deficits.find(name) == m_deficits.end(), "Duplicate section in the purchase list", name.c_str()); diff --git a/src/xrGame/restricted_object.cpp b/src/xrGame/restricted_object.cpp index 66796fe71ef..599ebbaab53 100644 --- a/src/xrGame/restricted_object.cpp +++ b/src/xrGame/restricted_object.cpp @@ -71,8 +71,8 @@ BOOL CRestrictedObject::net_Spawn(CSE_Abstract* data) string4096 temp0; string4096 temp1; - xr_strcpy(temp0, *monster->m_out_space_restrictors); - xr_strcpy(temp1, *monster->m_in_space_restrictors); + xr_strcpy(temp0, monster->m_out_space_restrictors.c_str()); + xr_strcpy(temp1, monster->m_in_space_restrictors.c_str()); if (ai().get_alife()) { @@ -266,7 +266,7 @@ IC void CRestrictedObject::construct_restriction_string(pstr temp_restrictions, for (; I != E; ++I) { IGameObject* object = Level().Objects.net_Find(*I); - if (!object || !!strstr(*current_restrictions, *object->cName()) == value) + if (!object || !!strstr(current_restrictions.c_str(), object->cName().c_str()) == value) continue; p(this, object->ID()); @@ -274,7 +274,7 @@ IC void CRestrictedObject::construct_restriction_string(pstr temp_restrictions, if (count) xr_strcat(temp_restrictions, temp_restrictions_size, ","); - xr_strcat(temp_restrictions, temp_restrictions_size, *object->cName()); + xr_strcat(temp_restrictions, temp_restrictions_size, object->cName().c_str()); count++; } @@ -355,7 +355,7 @@ void CRestrictedObject::add_restrictions(const shared_str& out_restrictions, con START_PROFILE("Restricted Object/Add Restrictions"); - Level().space_restriction_manager().add_restrictions(object().ID(), *out_restrictions, *in_restrictions); + Level().space_restriction_manager().add_restrictions(object().ID(), out_restrictions.c_str(), in_restrictions.c_str()); actual(false); @@ -369,7 +369,7 @@ void CRestrictedObject::remove_restrictions(const shared_str& out_restrictions, START_PROFILE("Restricted Object/Remove Restrictions"); - Level().space_restriction_manager().remove_restrictions(object().ID(), *out_restrictions, *in_restrictions); + Level().space_restriction_manager().remove_restrictions(object().ID(), out_restrictions.c_str(), in_restrictions.c_str()); actual(false); diff --git a/src/xrGame/script_entity.cpp b/src/xrGame/script_entity.cpp index f2e0aca6caa..f4819df3938 100644 --- a/src/xrGame/script_entity.cpp +++ b/src/xrGame/script_entity.cpp @@ -92,7 +92,7 @@ void CScriptEntity::reinit() void CScriptEntity::SetScriptControl(const bool bScriptControl, shared_str caSciptName) { if (!(((m_bScriptControl && !bScriptControl) || (!m_bScriptControl && bScriptControl)) && - (bScriptControl || (xr_strlen(*m_caScriptName) && !xr_strcmp(caSciptName, m_caScriptName))))) + (bScriptControl || (xr_strlen(m_caScriptName.c_str()) && !xr_strcmp(caSciptName, m_caScriptName))))) { GEnv.ScriptEngine->script_log( LuaMessageType::Error, "Invalid sequence of taking an entity under script control"); @@ -124,7 +124,7 @@ void CScriptEntity::SetScriptControl(const bool bScriptControl, shared_str caSci } bool CScriptEntity::GetScriptControl() const { return (m_bScriptControl); } -LPCSTR CScriptEntity::GetScriptControlName() const { return (*m_caScriptName); } +LPCSTR CScriptEntity::GetScriptControlName() const { return (m_caScriptName.c_str()); } bool CScriptEntity::CheckObjectVisibility(const CGameObject* tpObject) { if (!m_monster) @@ -145,7 +145,7 @@ bool CScriptEntity::CheckTypeVisibility(const char* section_name) for (; I != E; ++I) { VERIFY((*I).m_object); - if (!xr_strcmp(section_name, *(*I).m_object->cNameSect())) + if (!xr_strcmp(section_name, (*I).m_object->cNameSect().c_str())) return (true); } return (false); @@ -272,7 +272,7 @@ void CScriptEntity::ProcessScripts() #ifdef DEBUG if (empty_queue) GEnv.ScriptEngine->script_log( - LuaMessageType::Info, "Object %s has an empty script queue!", *object().cName()); + LuaMessageType::Info, "Object %s has an empty script queue!", object().cName().c_str()); #endif return; } @@ -345,7 +345,7 @@ bool CScriptEntity::bfAssignAnimation(CScriptEntityAction* tpEntityAction) return (true); IKinematicsAnimated& tVisualObject = *(smart_cast(object().Visual())); - m_tpNextAnimation = tVisualObject.ID_Cycle_Safe(*GetCurrentAction()->m_tAnimationAction.m_caAnimationToPlay); + m_tpNextAnimation = tVisualObject.ID_Cycle_Safe(GetCurrentAction()->m_tAnimationAction.m_caAnimationToPlay.c_str()); m_use_animation_movement_controller = GetCurrentAction()->m_tAnimationAction.m_use_animation_movement_controller; return (true); } @@ -401,7 +401,7 @@ bool CScriptEntity::bfAssignSound(CScriptEntityAction* tpEntityAction) if (xr_strlen(l_tSoundAction.m_caSoundToPlay)) { m_current_sound = xr_new(); - m_current_sound->create(*l_tSoundAction.m_caSoundToPlay, st_Effect, l_tSoundAction.m_sound_type); + m_current_sound->create(l_tSoundAction.m_caSoundToPlay.c_str(), st_Effect, l_tSoundAction.m_sound_type); } else l_tSoundAction.m_bCompleted = true; @@ -420,7 +420,7 @@ bool CScriptEntity::bfAssignParticles(CScriptEntityAction* tpEntityAction) { if (!l_tParticleAction.m_bStartedToPlay) { - const Fmatrix& l_tMatrix = GetUpdatedMatrix(*l_tParticleAction.m_caBoneName, + const Fmatrix& l_tMatrix = GetUpdatedMatrix(l_tParticleAction.m_caBoneName.c_str(), l_tParticleAction.m_tParticlePosition, l_tParticleAction.m_tParticleAngles); Fvector zero_vel = {0.f, 0.f, 0.f}; l_tParticleAction.m_tpParticleSystem->UpdateParent(l_tMatrix, zero_vel); @@ -513,7 +513,7 @@ bool CScriptEntity::bfAssignMovement(CScriptEntityAction* tpEntityAction) string256 S; xr_sprintf(S, "Cannot find corresponding level vertex for the specified position [%f][%f][%f] for monster %s", - VPUSH(l_tMovementAction.m_tDestinationPosition), *m_monster->cName()); + VPUSH(l_tMovementAction.m_tDestinationPosition), m_monster->cName().c_str()); THROW2(ai().level_graph().valid_vertex_id(vertex_id), S); } #endif @@ -568,13 +568,13 @@ LPCSTR CScriptEntity::GetPatrolPathName() if (!GetScriptControl()) { GEnv.ScriptEngine->script_log(LuaMessageType::Error, - "Object %s is not under script control while you are trying to get patrol path name!", *m_object->cName()); + "Object %s is not under script control while you are trying to get patrol path name!", m_object->cName().c_str()); return ""; } #endif if (m_tpActionQueue.empty()) return (""); - return (*m_tpActionQueue.back()->m_tMovementAction.m_path_name); + return (m_tpActionQueue.back()->m_tMovementAction.m_path_name.c_str()); } bool CScriptEntity::net_Spawn(CSE_Abstract* DC) @@ -627,7 +627,7 @@ bool CScriptEntity::bfScriptAnimation() #endif m_tpScriptAnimation = m_tpNextAnimation; IKinematicsAnimated* skeleton_animated = smart_cast(object().Visual()); - LPCSTR animation_id = *GetCurrentAction()->m_tAnimationAction.m_caAnimationToPlay; + LPCSTR animation_id = GetCurrentAction()->m_tAnimationAction.m_caAnimationToPlay.c_str(); MotionID animation = skeleton_animated->ID_Cycle(animation_id); CBlend* result = 0; for (u16 i = 0; i < MAX_PARTS; ++i) diff --git a/src/xrGame/script_game_object.cpp b/src/xrGame/script_game_object.cpp index 56124459bd3..d2de0e012e7 100644 --- a/src/xrGame/script_game_object.cpp +++ b/src/xrGame/script_game_object.cpp @@ -214,7 +214,7 @@ LPCSTR CScriptGameObject::WhoHitName() CEntityAlive* entity_alive = smart_cast(&object()); if (entity_alive) return entity_alive->conditions().GetWhoHitLastTime() ? - (*entity_alive->conditions().GetWhoHitLastTime()->cName()) : + (entity_alive->conditions().GetWhoHitLastTime()->cName().c_str()) : NULL; else { @@ -229,7 +229,7 @@ LPCSTR CScriptGameObject::WhoHitSectionName() CEntityAlive* entity_alive = smart_cast(&object()); if (entity_alive) return entity_alive->conditions().GetWhoHitLastTime() ? - (*entity_alive->conditions().GetWhoHitLastTime()->cNameSect()) : + (entity_alive->conditions().GetWhoHitLastTime()->cNameSect().c_str()) : NULL; else { diff --git a/src/xrGame/script_game_object3.cpp b/src/xrGame/script_game_object3.cpp index 27482d5032a..2f9d100555f 100644 --- a/src/xrGame/script_game_object3.cpp +++ b/src/xrGame/script_game_object3.cpp @@ -351,7 +351,7 @@ LPCSTR CScriptGameObject::GetPatrolPathName() return (script_monster->GetPatrolPathName()); } else - return (*stalker->movement().patrol().path_name()); + return (stalker->movement().patrol().path_name().c_str()); } void CScriptGameObject::add_animation(LPCSTR animation, bool hand_usage, bool use_movement_controller) @@ -572,7 +572,7 @@ void CScriptGameObject::set_desired_position(const Fvector* desired_position) GEnv.ScriptEngine->script_log(LuaMessageType::Error, "CAI_Stalker : cannot access class member movement!"); else { - THROW2(desired_position || stalker->movement().restrictions().accessible(*desired_position), *stalker->cName()); + THROW2(desired_position || stalker->movement().restrictions().accessible(*desired_position), stalker->cName().c_str()); stalker->movement().set_desired_position(desired_position); } } @@ -1239,7 +1239,7 @@ LPCSTR CScriptGameObject::sound_prefix() const return (0); } - return (*custom_monster->sound().sound_prefix()); + return (custom_monster->sound().sound_prefix().c_str()); } void CScriptGameObject::sound_prefix(LPCSTR sound_prefix) diff --git a/src/xrGame/script_game_object_inventory_owner.cpp b/src/xrGame/script_game_object_inventory_owner.cpp index 392b1d64ea2..a67ddbb4842 100644 --- a/src/xrGame/script_game_object_inventory_owner.cpp +++ b/src/xrGame/script_game_object_inventory_owner.cpp @@ -662,7 +662,7 @@ LPCSTR CScriptGameObject::ProfileName() if (!profile_id || !profile_id.size()) return NULL; else - return *profile_id; + return profile_id.c_str(); } LPCSTR CScriptGameObject::CharacterName() @@ -776,7 +776,7 @@ LPCSTR CScriptGameObject::CharacterCommunity() GEnv.ScriptEngine->script_log(LuaMessageType::Error, "CharacterCommunity available only for InventoryOwner"); return NULL; } - return *pInventoryOwner->CharacterInfo().Community().id(); + return pInventoryOwner->CharacterInfo().Community().id().c_str(); } void CScriptGameObject::SetCharacterCommunity(LPCSTR comm, int squad, int group) @@ -911,10 +911,10 @@ void construct_restriction_vector(shared_str restrictions, xr_vectorID()); @@ -972,7 +972,7 @@ LPCSTR CScriptGameObject::in_restrictions() LuaMessageType::Error, "CRestrictedObject : cannot access class member in_restrictions!"); return (""); } - return (*monster->movement().restrictions().in_restrictions()); + return (monster->movement().restrictions().in_restrictions().c_str()); } LPCSTR CScriptGameObject::out_restrictions() @@ -984,7 +984,7 @@ LPCSTR CScriptGameObject::out_restrictions() LuaMessageType::Error, "CRestrictedObject : cannot access class member out_restrictions!"); return (""); } - return (*monster->movement().restrictions().out_restrictions()); + return (monster->movement().restrictions().out_restrictions().c_str()); } LPCSTR CScriptGameObject::base_in_restrictions() @@ -996,7 +996,7 @@ LPCSTR CScriptGameObject::base_in_restrictions() LuaMessageType::Error, "CRestrictedObject : cannot access class member base_in_restrictions!"); return (""); } - return (*monster->movement().restrictions().base_in_restrictions()); + return (monster->movement().restrictions().base_in_restrictions().c_str()); } LPCSTR CScriptGameObject::base_out_restrictions() @@ -1008,7 +1008,7 @@ LPCSTR CScriptGameObject::base_out_restrictions() LuaMessageType::Error, "CRestrictedObject : cannot access class member base_out_restrictions!"); return (""); } - return (*monster->movement().restrictions().base_out_restrictions()); + return (monster->movement().restrictions().base_out_restrictions().c_str()); } bool CScriptGameObject::accessible_position(const Fvector& position) diff --git a/src/xrGame/script_game_object_use.cpp b/src/xrGame/script_game_object_use.cpp index 60347a173d7..279e7a66f57 100644 --- a/src/xrGame/script_game_object_use.cpp +++ b/src/xrGame/script_game_object_use.cpp @@ -56,9 +56,9 @@ CScriptGameObject* CScriptGameObject::Parent() const } int CScriptGameObject::clsid() const { return (object().clsid()); } -LPCSTR CScriptGameObject::Name() const { return (*object().cName()); } +LPCSTR CScriptGameObject::Name() const { return (object().cName().c_str()); } shared_str CScriptGameObject::cName() const { return (object().cName()); } -LPCSTR CScriptGameObject::Section() const { return (*object().cNameSect()); } +LPCSTR CScriptGameObject::Section() const { return (object().cNameSect().c_str()); } void CScriptGameObject::Kill(CScriptGameObject* who, bool bypass_actor_check) { CEntity* l_tpEntity = smart_cast(&object()); @@ -68,13 +68,13 @@ void CScriptGameObject::Kill(CScriptGameObject* who, bool bypass_actor_check) if (!l_tpEntity) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "%s cannot access class member Kill!", *object().cName()); + LuaMessageType::Error, "%s cannot access class member Kill!", object().cName().c_str()); return; } if (!l_tpEntity->AlreadyDie()) l_tpEntity->KillEntity(who ? who->object().ID() : object().ID(), bypass_actor_check); else - GEnv.ScriptEngine->script_log(LuaMessageType::Error, "attempt to kill dead object %s", *object().cName()); + GEnv.ScriptEngine->script_log(LuaMessageType::Error, "attempt to kill dead object %s", object().cName().c_str()); } bool CScriptGameObject::Alive() const @@ -94,7 +94,7 @@ ALife::ERelationType CScriptGameObject::GetRelationType(CScriptGameObject* who) if (!l_tpEntityAlive1) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "%s cannot access class member GetRelationType!", *object().cName()); + LuaMessageType::Error, "%s cannot access class member GetRelationType!", object().cName().c_str()); return ALife::eRelationTypeDummy; } @@ -102,7 +102,7 @@ ALife::ERelationType CScriptGameObject::GetRelationType(CScriptGameObject* who) if (!l_tpEntityAlive2) { GEnv.ScriptEngine->script_log(LuaMessageType::Error, - "%s cannot apply GetRelationType method for non-alive object!", *who->object().cName()); + "%s cannot apply GetRelationType method for non-alive object!", who->object().cName().c_str()); return ALife::eRelationTypeDummy; } @@ -197,7 +197,7 @@ void CScriptGameObject::set_const_force(const Fvector& dir, float value, u32 tim if (!shell) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "set_const_force : object %s has no physics shell!", *object().cName()); + LuaMessageType::Error, "set_const_force : object %s has no physics shell!", object().cName().c_str()); return; } //#ifdef DEBUG diff --git a/src/xrGame/script_particle_action.cpp b/src/xrGame/script_particle_action.cpp index 8ba7563346e..79b10e2df56 100644 --- a/src/xrGame/script_particle_action.cpp +++ b/src/xrGame/script_particle_action.cpp @@ -19,7 +19,7 @@ void CScriptParticleAction::SetParticle(LPCSTR caParticleToRun, bool bAutoRemove { m_caParticleToRun = caParticleToRun; m_tGoalType = eGoalTypeParticleAttached; - m_tpParticleSystem = CParticlesObject::Create(*m_caParticleToRun, BOOL(m_bAutoRemove = bAutoRemove)); + m_tpParticleSystem = CParticlesObject::Create(m_caParticleToRun.c_str(), BOOL(m_bAutoRemove = bAutoRemove)); m_bStartedToPlay = false; m_bCompleted = false; } diff --git a/src/xrGame/script_sound.cpp b/src/xrGame/script_sound.cpp index 37875e8e3f3..954a2962514 100644 --- a/src/xrGame/script_sound.cpp +++ b/src/xrGame/script_sound.cpp @@ -48,7 +48,7 @@ Fvector CScriptSound::GetPosition() const void CScriptSound::Play(CScriptGameObject* object, float delay, int flags) { - THROW3(m_sound._handle() || m_bIsNoSound, "There is no sound", *m_caSoundToPlay); + THROW3(m_sound._handle() || m_bIsNoSound, "There is no sound", m_caSoundToPlay.c_str()); // Msg ("%6d : CScriptSound::Play (%s), delay %f, flags //%d",Device.dwTimeGlobal,m_sound._handle()->file_name(),delay,flags); m_sound.play((object) ? &object->object() : NULL, flags, delay); @@ -56,7 +56,7 @@ void CScriptSound::Play(CScriptGameObject* object, float delay, int flags) void CScriptSound::PlayAtPos(CScriptGameObject* object, const Fvector& position, float delay, int flags) { - THROW3(m_sound._handle() || m_bIsNoSound, "There is no sound", *m_caSoundToPlay); + THROW3(m_sound._handle() || m_bIsNoSound, "There is no sound", m_caSoundToPlay.c_str()); // Msg ("%6d : CScriptSound::Play (%s), delay %f, flags //%d",m_sound._handle()->file_name(),delay,flags); m_sound.play_at_pos((object) ? &object->object() : NULL, position, flags, delay); @@ -65,6 +65,6 @@ void CScriptSound::PlayAtPos(CScriptGameObject* object, const Fvector& position, void CScriptSound::PlayNoFeedback( CScriptGameObject* object, u32 flags /*!< Looping */, float delay /*!< Delay */, Fvector pos, float vol) { - THROW3(m_sound._handle() || m_bIsNoSound, "There is no sound", *m_caSoundToPlay); + THROW3(m_sound._handle() || m_bIsNoSound, "There is no sound", m_caSoundToPlay.c_str()); m_sound.play_no_feedback((object) ? &object->object() : NULL, flags, delay, &pos, &vol); } diff --git a/src/xrGame/script_sound_action.cpp b/src/xrGame/script_sound_action.cpp index 859225d40b5..4c894aeccdd 100644 --- a/src/xrGame/script_sound_action.cpp +++ b/src/xrGame/script_sound_action.cpp @@ -18,7 +18,7 @@ void CScriptSoundAction::SetSound(LPCSTR caSoundToPlay) m_tGoalType = eGoalTypeSoundAttached; m_bStartedToPlay = false; string_path l_caFileName; - if (FS.exist(l_caFileName, "$game_sounds$", *m_caSoundToPlay, ".ogg")) + if (FS.exist(l_caFileName, "$game_sounds$", m_caSoundToPlay.c_str(), ".ogg")) { m_bStartedToPlay = false; m_bCompleted = false; diff --git a/src/xrGame/seniority_hierarchy_holder.cpp b/src/xrGame/seniority_hierarchy_holder.cpp index dc0b9236440..7a26dc4e342 100644 --- a/src/xrGame/seniority_hierarchy_holder.cpp +++ b/src/xrGame/seniority_hierarchy_holder.cpp @@ -14,7 +14,7 @@ CSeniorityHierarchyHolder::~CSeniorityHierarchyHolder() { delete_data(m_teams); } CTeamHierarchyHolder& CSeniorityHierarchyHolder::team(u32 team_id) { - VERIFY3(team_id < max_team_count, "Team id is invalid : ", *SeniorityHierarchy::to_string(team_id)); + VERIFY3(team_id < max_team_count, "Team id is invalid : ", SeniorityHierarchy::to_string(team_id).c_str()); if (!m_teams[team_id]) m_teams[team_id] = xr_new(this); return (*m_teams[team_id]); diff --git a/src/xrGame/smart_cover_description.cpp b/src/xrGame/smart_cover_description.cpp index acf17502f46..d80b9e99cf6 100644 --- a/src/xrGame/smart_cover_description.cpp +++ b/src/xrGame/smart_cover_description.cpp @@ -90,7 +90,7 @@ void description::load_loopholes(shared_str const& table_id) { string256 temp; xr_strcpy(temp, "smart_covers.descriptions."); - xr_strcat(temp, *table_id); + xr_strcat(temp, table_id.c_str()); xr_strcat(temp, ".loopholes"); m_table_id = table_id; @@ -146,7 +146,7 @@ void description::load_transitions(shared_str const& table_id) { string256 temp; xr_strcpy(temp, "smart_covers.descriptions."); - xr_strcat(temp, *table_id); + xr_strcat(temp, table_id.c_str()); xr_strcat(temp, ".transitions"); luabind::object transitions; diff --git a/src/xrGame/sound_player.cpp b/src/xrGame/sound_player.cpp index ea5428e079a..8d5ed470271 100644 --- a/src/xrGame/sound_player.cpp +++ b/src/xrGame/sound_player.cpp @@ -169,7 +169,7 @@ void CSoundPlayer::play( { #ifdef DEBUG Msg("- There are no sounds in sound collection \"%s\" with internal type %d (sound_script = %d)", - *sound.m_sound_prefix, internal_type, StalkerSpace::eStalkerSoundScript); + sound.m_sound_prefix.c_str(), internal_type, StalkerSpace::eStalkerSoundScript); #endif return; } @@ -239,12 +239,12 @@ CSoundPlayer::CSoundCollection::CSoundCollection(const CSoundCollectionParams& p { seed(u32(CPU::QPC() & 0xffffffff)); m_sounds.clear(); - for (int j = 0, N = _GetItemCount(*params.m_sound_prefix); j < N; ++j) + for (int j = 0, N = _GetItemCount(params.m_sound_prefix.c_str()); j < N; ++j) { string_path fn, s, temp; pstr S = (pstr)&s; - _GetItem(*params.m_sound_prefix, j, temp); - strconcat(sizeof(s), S, *params.m_sound_player_prefix, temp); + _GetItem(params.m_sound_prefix.c_str(), j, temp); + strconcat(sizeof(s), S, params.m_sound_player_prefix.c_str(), temp); if (FS.exist(fn, "$game_sounds$", S, ".ogg")) { ref_sound* temp = add(params.m_type, S); @@ -265,7 +265,7 @@ CSoundPlayer::CSoundCollection::CSoundCollection(const CSoundCollectionParams& p } #ifdef DEBUG if (m_sounds.empty()) - Msg("- There are no sounds with prefix %s", *params.m_sound_prefix); + Msg("- There are no sounds with prefix %s", params.m_sound_prefix.c_str()); #endif } diff --git a/src/xrGame/space_restriction.cpp b/src/xrGame/space_restriction.cpp index 57f61b521b3..9766a8cda88 100644 --- a/src/xrGame/space_restriction.cpp +++ b/src/xrGame/space_restriction.cpp @@ -140,12 +140,12 @@ void CSpaceRestriction::merge_in_out_restrictions() CSpaceRestriction::CBaseRestrictionPtr CSpaceRestriction::merge( CBaseRestrictionPtr bridge, const RESTRICTIONS& temp_restrictions) const { - u32 acc_length = xr_strlen(*bridge->name()) + 1; + u32 acc_length = xr_strlen(bridge->name().c_str()) + 1; { RESTRICTIONS::const_iterator I = temp_restrictions.begin(); RESTRICTIONS::const_iterator E = temp_restrictions.end(); for (; I != E; ++I) - acc_length += xr_strlen(*(*I)->name()) + 1; + acc_length += xr_strlen((*I)->name().c_str()) + 1; } pstr S = xr_alloc(acc_length); @@ -154,7 +154,7 @@ CSpaceRestriction::CBaseRestrictionPtr CSpaceRestriction::merge( RESTRICTIONS::const_iterator I = temp_restrictions.begin(); RESTRICTIONS::const_iterator E = temp_restrictions.end(); for (; I != E; ++I) - temp = strconcat(acc_length, S, *temp, ",", *(*I)->name()); + temp = strconcat(acc_length, S, temp.c_str(), ",", (*I)->name().c_str()); xr_free(S); @@ -223,7 +223,7 @@ void CSpaceRestriction::initialize() if (!m_out_space_restriction->object().correct()) { Msg("~ BAD out restrictions combination :"); - Msg("~ %s", *m_out_space_restriction->name()); + Msg("~ %s", m_out_space_restriction->name().c_str()); } } #endif diff --git a/src/xrGame/space_restriction_bridge_inline.h b/src/xrGame/space_restriction_bridge_inline.h index c8d87187923..e96639b6c9d 100644 --- a/src/xrGame/space_restriction_bridge_inline.h +++ b/src/xrGame/space_restriction_bridge_inline.h @@ -47,7 +47,7 @@ IC u32 CSpaceRestrictionBridge::accessible_nearest( } bool selected_vertex_id_is_valid = level_graph.valid_vertex_id(selected); VERIFY2(selected_vertex_id_is_valid, - make_string("vertex_id[%d], object[%s], position[%f][%f][%f]", selected, *name(), VPUSH(position))); + make_string("vertex_id[%d], object[%s], position[%f][%f][%f]", selected, name().c_str(), VPUSH(position))); if (!selected_vertex_id_is_valid) return -1; diff --git a/src/xrGame/space_restriction_composition.cpp b/src/xrGame/space_restriction_composition.cpp index 88c3ce6d004..a8bf3037e23 100644 --- a/src/xrGame/space_restriction_composition.cpp +++ b/src/xrGame/space_restriction_composition.cpp @@ -60,7 +60,7 @@ bool CSpaceRestrictionComposition::inside(const Fsphere& sphere) void CSpaceRestrictionComposition::initialize() { - u32 n = _GetItemCount(*m_space_restrictors); + u32 n = _GetItemCount(m_space_restrictors.c_str()); VERIFY(n); if (n == 1) { @@ -74,14 +74,14 @@ void CSpaceRestrictionComposition::initialize() string256 element; for (u32 i = 0; i < n; ++i) - if (!m_space_restriction_holder->restriction(_GetItem(*m_space_restrictors, i, element))->initialized()) + if (!m_space_restriction_holder->restriction(_GetItem(m_space_restrictors.c_str(), i, element))->initialized()) return; Fsphere* spheres = (Fsphere*)xr_alloca(n * sizeof(Fsphere)); for (u32 i = 0; i < n; ++i) { SpaceRestrictionHolder::CBaseRestrictionPtr restriction = - m_space_restriction_holder->restriction(_GetItem(*m_space_restrictors, i, element)); + m_space_restriction_holder->restriction(_GetItem(m_space_restrictors.c_str(), i, element)); merge(restriction); @@ -190,7 +190,7 @@ Fsphere CSpaceRestrictionComposition::sphere() const #ifdef DEBUG void CSpaceRestrictionComposition::check_restrictor_type() { - if (_GetItemCount(*m_space_restrictors) == 1) + if (_GetItemCount(m_space_restrictors.c_str()) == 1) return; if (!ai().get_alife()) @@ -201,7 +201,7 @@ void CSpaceRestrictionComposition::check_restrictor_type() return; CSpaceRestrictor* restrictor = smart_cast(object); - VERIFY3(restrictor, "you are trying to use object as a restrictor", *m_space_restrictors); + VERIFY3(restrictor, "you are trying to use object as a restrictor", m_space_restrictors.c_str()); VERIFY2(restrictor->restrictor_type() == RestrictionSpace::eRestrictorTypeNone, "you are trying to restrict yourself with restrictor with type eRestrictorTypeNone"); VERIFY2(restrictor->restrictor_type() != RestrictionSpace::eRestrictorTypeNone, diff --git a/src/xrGame/space_restriction_holder.cpp b/src/xrGame/space_restriction_holder.cpp index 7329851c6e6..4c942737858 100644 --- a/src/xrGame/space_restriction_holder.cpp +++ b/src/xrGame/space_restriction_holder.cpp @@ -36,7 +36,7 @@ shared_str CSpaceRestrictionHolder::normalize_string(shared_str space_restrictor pstr* string_current = strings; pstr temp_string = (pstr)xr_alloca((n + 1) * sizeof(char)); - LPCSTR I = *space_restrictors; + LPCSTR I = space_restrictors.c_str(); pstr i = temp_string, j = i; for (; *I; ++I, ++i) { @@ -120,9 +120,9 @@ void CSpaceRestrictionHolder::register_restrictor( temp1 = *temp; if (xr_strlen(*temp) && xr_strlen(space_restrictors)) - strconcat(sizeof(m_temp_string), m_temp_string, **temp, ",", *space_restrictors); + strconcat(sizeof(m_temp_string), m_temp_string, (*temp).c_str(), ",", space_restrictors.c_str()); else - strconcat(sizeof(m_temp_string), m_temp_string, **temp, *space_restrictors); + strconcat(sizeof(m_temp_string), m_temp_string, (*temp).c_str(), space_restrictors.c_str()); *temp = normalize_string(m_temp_string); @@ -149,9 +149,9 @@ bool try_remove_string(shared_str& search_string, const shared_str& string_to_se string256 temp; string4096 temp1; *temp1 = 0; - for (int i = 0, j = 0, n = _GetItemCount(*search_string); i < n; ++i, ++j) + for (int i = 0, j = 0, n = _GetItemCount(search_string.c_str()); i < n; ++i, ++j) { - if (xr_strcmp(string_to_search, _GetItem(*search_string, i, temp))) + if (xr_strcmp(string_to_search, _GetItem(search_string.c_str(), i, temp))) { if (j) xr_strcat(temp1, ","); diff --git a/src/xrGame/space_restriction_manager.cpp b/src/xrGame/space_restriction_manager.cpp index c803082038d..e122bbf62c6 100644 --- a/src/xrGame/space_restriction_manager.cpp +++ b/src/xrGame/space_restriction_manager.cpp @@ -30,8 +30,8 @@ CSpaceRestrictionManager::~CSpaceRestrictionManager() void show_restriction(const shared_str& restrictions) { string256 temp; - for (int i = 0, n = _GetItemCount(*restrictions); i < n; ++i) - Msg(" %s", _GetItem(*restrictions, i, temp)); + for (int i = 0, n = _GetItemCount(restrictions.c_str()); i < n; ++i) + Msg(" %s", _GetItem(restrictions.c_str(), i, temp)); } typedef intrusive_ptr CRestrictionPtr; @@ -171,7 +171,7 @@ CSpaceRestrictionManager::CRestrictionPtr CSpaceRestrictionManager::restriction( out_restrictors = normalize_string(out_restrictors); in_restrictors = normalize_string(in_restrictors); - strconcat(sizeof(m_temp), m_temp, *out_restrictors, "\x01", *in_restrictors); + strconcat(sizeof(m_temp), m_temp, out_restrictors.c_str(), "\x01", in_restrictors.c_str()); shared_str space_restrictions = m_temp; SPACE_RESTRICTIONS::const_iterator I = m_space_restrictions.find(space_restrictions); @@ -193,8 +193,8 @@ u32 CSpaceRestrictionManager::accessible_nearest(ALife::_OBJECT_ID id, const Fve bool CSpaceRestrictionManager::restriction_presented(shared_str restrictions, shared_str restriction) const { string4096 m_temp; - for (u32 i = 0, n = _GetItemCount(*restrictions); i < n; ++i) - if (!xr_strcmp(restriction, _GetItem(*restrictions, i, m_temp))) + for (u32 i = 0, n = _GetItemCount(restrictions.c_str()); i < n; ++i) + if (!xr_strcmp(restriction, _GetItem(restrictions.c_str(), i, m_temp))) return (true); return (false); } @@ -203,9 +203,9 @@ void CSpaceRestrictionManager::join_restrictions(shared_str& restrictions, share { string4096 m_temp1; string4096 m_temp2; - xr_strcpy(m_temp2, *restrictions); - for (u32 i = 0, n = _GetItemCount(*update), count = xr_strlen(m_temp2); i < n; ++i) - if (!restriction_presented(m_temp2, _GetItem(*update, i, m_temp1))) + xr_strcpy(m_temp2, restrictions.c_str()); + for (u32 i = 0, n = _GetItemCount(update.c_str()), count = xr_strlen(m_temp2); i < n; ++i) + if (!restriction_presented(m_temp2, _GetItem(update.c_str(), i, m_temp1))) { if (count) xr_strcat(m_temp2, ","); @@ -220,8 +220,8 @@ void CSpaceRestrictionManager::difference_restrictions(shared_str& restrictions, string4096 m_temp1; string4096 m_temp2; xr_strcpy(m_temp2, ""); - for (u32 i = 0, n = _GetItemCount(*restrictions), count = 0; i < n; ++i) - if (!restriction_presented(update, _GetItem(*restrictions, i, m_temp1))) + for (u32 i = 0, n = _GetItemCount(restrictions.c_str()), count = 0; i < n; ++i) + if (!restriction_presented(update, _GetItem(restrictions.c_str(), i, m_temp1))) { if (count) xr_strcat(m_temp2, ","); diff --git a/src/xrGame/space_restriction_shape.cpp b/src/xrGame/space_restriction_shape.cpp index 59b3dcb3cb0..e2b4a3b4829 100644 --- a/src/xrGame/space_restriction_shape.cpp +++ b/src/xrGame/space_restriction_shape.cpp @@ -102,7 +102,7 @@ void CSpaceRestrictionShape::build_border() process_borders(); - VERIFY3(!border().empty(), "space restrictor has no border", *m_restrictor->cName()); + VERIFY3(!border().empty(), "space restrictor has no border", m_restrictor->cName().c_str()); #ifdef DEBUG test_correctness(); diff --git a/src/xrGame/squad_hierarchy_holder.cpp b/src/xrGame/squad_hierarchy_holder.cpp index 81df4132686..3f38989435f 100644 --- a/src/xrGame/squad_hierarchy_holder.cpp +++ b/src/xrGame/squad_hierarchy_holder.cpp @@ -16,7 +16,7 @@ CSquadHierarchyHolder::~CSquadHierarchyHolder() { delete_data(m_groups); } CGroupHierarchyHolder& CSquadHierarchyHolder::group(u32 group_id) const { - VERIFY3(group_id < max_group_count, "Group id is invalid : ", *SeniorityHierarchy::to_string(group_id)); + VERIFY3(group_id < max_group_count, "Group id is invalid : ", SeniorityHierarchy::to_string(group_id).c_str()); if (!m_groups[group_id]) m_groups[group_id] = xr_new(const_cast(this)); return (*m_groups[group_id]); diff --git a/src/xrGame/stalker_animation_callbacks.cpp b/src/xrGame/stalker_animation_callbacks.cpp index c5aa132e29e..f60d4cff8cc 100644 --- a/src/xrGame/stalker_animation_callbacks.cpp +++ b/src/xrGame/stalker_animation_callbacks.cpp @@ -119,7 +119,7 @@ void CStalkerAnimationManager::assign_bone_callbacks() m_spine_params.invalidate(); #endif // #ifdef DEBUG - LPCSTR section = *object().cNameSect(); + LPCSTR section = object().cNameSect().c_str(); m_head_params.m_rotation = &object().sight().current_head_rotation(); m_head_params.m_object = &object(); @@ -159,7 +159,7 @@ void CStalkerAnimationManager::assign_bone_blend_callbacks(bool const& forward_d m_spine_params.invalidate(); #endif // #ifdef DEBUG - LPCSTR section = *object().cNameSect(); + LPCSTR section = object().cNameSect().c_str(); m_head_params.m_rotation = &object().sight().current_head_rotation(); m_head_params.m_object = &object(); @@ -200,7 +200,7 @@ void CStalkerAnimationManager::remove_bone_callbacks() m_spine_params.invalidate(); #endif // #ifdef DEBUG - LPCSTR section = *object().cNameSect(); + LPCSTR section = object().cNameSect().c_str(); int head_bone = kinematics->LL_BoneID(pSettings->r_string(section, "bone_head")); kinematics->LL_GetBoneInstance(u16(head_bone)).set_callback(bctCustom, 0, 0); diff --git a/src/xrGame/stalker_animation_global.cpp b/src/xrGame/stalker_animation_global.cpp index 37651b62d9c..c6a95ea3457 100644 --- a/src/xrGame/stalker_animation_global.cpp +++ b/src/xrGame/stalker_animation_global.cpp @@ -51,7 +51,7 @@ MotionID CStalkerAnimationManager::global_critical_hit() CWeapon* weapon = smart_cast(object().inventory().ActiveItem()); VERIFY2(weapon, make_string("current active item: %s", object().inventory().ActiveItem() ? - *object().inventory().ActiveItem()->object().cName() : + object().inventory().ActiveItem()->object().cName().c_str() : "no active item")); u32 animation_slot = weapon->animation_slot(); diff --git a/src/xrGame/stalker_animation_manager.cpp b/src/xrGame/stalker_animation_manager.cpp index 9cccf28d579..a05ced01611 100644 --- a/src/xrGame/stalker_animation_manager.cpp +++ b/src/xrGame/stalker_animation_manager.cpp @@ -90,11 +90,11 @@ void CStalkerAnimationManager::reload() assign_bone_callbacks(); #ifdef DEBUG - global().set_dbg_info(*object().cName(), "Global"); - head().set_dbg_info(*object().cName(), "Head "); - torso().set_dbg_info(*object().cName(), "Torso "); - legs().set_dbg_info(*object().cName(), "Legs "); - script().set_dbg_info(*object().cName(), "Script"); + global().set_dbg_info(object().cName().c_str(), "Global"); + head().set_dbg_info(object().cName().c_str(), "Head "); + torso().set_dbg_info(object().cName().c_str(), "Torso "); + legs().set_dbg_info(object().cName().c_str(), "Legs "); + script().set_dbg_info(object().cName().c_str(), "Script"); #endif }; @@ -111,7 +111,7 @@ void CStalkerAnimationManager::play_fx(float power_factor, int fx_index) ->LL_MotionDefName_dbg( m_data_storage->m_part_animations.A[object().movement().body_state()].m_global.A[0].A[fx_index]) .first; - Msg("%6d [%s][%s][%s][%f]", Device.dwTimeGlobal, *object().cName(), "FX", name, power_factor); + Msg("%6d [%s][%s][%s][%f]", Device.dwTimeGlobal, object().cName().c_str(), "FX", name, power_factor); } #endif m_skeleton_animated->PlayFX( diff --git a/src/xrGame/stalker_animation_manager_debug.cpp b/src/xrGame/stalker_animation_manager_debug.cpp index f41b5b5f1a8..8ae3032219c 100644 --- a/src/xrGame/stalker_animation_manager_debug.cpp +++ b/src/xrGame/stalker_animation_manager_debug.cpp @@ -101,8 +101,8 @@ void show_animations() Msg("frames starts animation animation_set"); for (i = animations; i != e; ++i) - Msg("%6d %6d %-32s %s", (*i)->second.m_frame_count, (*i)->second.m_start_count, *(*i)->first.first, - *(*i)->first.second); + Msg("%6d %6d %-32s %s", (*i)->second.m_frame_count, (*i)->second.m_start_count, (*i)->first.first.c_str(), + (*i)->first.second.c_str()); } void show_blends() @@ -130,8 +130,8 @@ void show_blends() " " " animation_set2"); for (i = blends; i != e; ++i) - Msg("%-32s %32s ->[%6d]-> %-32s %32s", *(*i)->first.second.second, *(*i)->first.second.first, (*i)->second, - *(*i)->first.first.first, *(*i)->first.first.second); + Msg("%-32s %32s ->[%6d]-> %-32s %32s", (*i)->first.second.second.c_str(), (*i)->first.second.first.c_str(), (*i)->second, + (*i)->first.first.first.c_str(), (*i)->first.first.second.c_str()); } void show_animation_stats() @@ -193,7 +193,7 @@ void CStalkerAnimationManager::add_animation_stats( const ANIMATION_ID& animation_id, const BLEND_ID* blend_id, bool just_started) { ::add_animation_stats( - animation_id.first, animation_id.second, *object().Visual()->getDebugName(), blend_id, just_started); + animation_id.first, animation_id.second, object().Visual()->getDebugName().c_str(), blend_id, just_started); } void CStalkerAnimationManager::add_animation_stats() diff --git a/src/xrGame/stalker_animation_manager_update.cpp b/src/xrGame/stalker_animation_manager_update.cpp index 6a7b4881850..e519e07663d 100644 --- a/src/xrGame/stalker_animation_manager_update.cpp +++ b/src/xrGame/stalker_animation_manager_update.cpp @@ -228,7 +228,7 @@ void CStalkerAnimationManager::update() } catch (...) { - Msg("! error in stalker with visual %s and ID %u", *object().cNameVisual(), object().ID()); + Msg("! error in stalker with visual %s and ID %u", object().cNameVisual().c_str(), object().ID()); /* avo: prevent game from crashing */ head().reset(); torso().reset(); diff --git a/src/xrGame/stalker_animation_script.cpp b/src/xrGame/stalker_animation_script.cpp index 0664bf84451..5340a65d29f 100644 --- a/src/xrGame/stalker_animation_script.cpp +++ b/src/xrGame/stalker_animation_script.cpp @@ -51,7 +51,7 @@ void CStalkerAnimationManager::add_script_animation( if (!motion) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "There is no animation %s (object %s)!", animation, *object().cName()); + LuaMessageType::Error, "There is no animation %s (object %s)!", animation, object().cName().c_str()); return; } @@ -75,7 +75,7 @@ void CStalkerAnimationManager::add_script_animation(LPCSTR animation, bool hand_ if (!motion) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "There is no animation %s (object %s)!", animation, *object().cName()); + LuaMessageType::Error, "There is no animation %s (object %s)!", animation, object().cName().c_str()); return; } diff --git a/src/xrGame/stalker_animation_torso.cpp b/src/xrGame/stalker_animation_torso.cpp index 21ebc1b65e1..8d1928b7402 100644 --- a/src/xrGame/stalker_animation_torso.cpp +++ b/src/xrGame/stalker_animation_torso.cpp @@ -78,7 +78,7 @@ MotionID CStalkerAnimationManager::no_object_animation(const EBodyState& body_st if (eMentalStateFree == movement.mental_state()) { VERIFY3(eBodyStateStand == movement.body_state(), "Cannot run FREE animations, when body state is not stand!", - *stalker.cName()); + stalker.cName().c_str()); if (standing()) return (animation[9].A[1]); @@ -147,7 +147,7 @@ MotionID CStalkerAnimationManager::unknown_object_animation(u32 slot, const EBod if (eMentalStateFree == movement.mental_state()) { VERIFY3(eBodyStateStand == movement.body_state(), "Cannot run FREE animation when body state is not stand!", - *object().cName()); + object().cName().c_str()); if (standing()) return (animation[9].A[1]); diff --git a/src/xrGame/stalker_combat_action_base.cpp b/src/xrGame/stalker_combat_action_base.cpp index 10f9d53c783..8888a090501 100644 --- a/src/xrGame/stalker_combat_action_base.cpp +++ b/src/xrGame/stalker_combat_action_base.cpp @@ -252,7 +252,7 @@ void CStalkerActionCombatBase::play_attack_sound( if (object().agent_manager().member().combat_members().empty()) Msg("! I am in combat, but there is no combat members at all (including me), " "npc[%s],team[%d],squad[%d],group[%d]", - *object().cName(), object().g_Team(), object().g_Squad(), object().g_Group()); + object().cName().c_str(), object().g_Team(), object().g_Squad(), object().g_Group()); #endif // DEBUG if (object().agent_manager().member().combat_members().size() > 1) @@ -278,7 +278,7 @@ void CStalkerActionCombatBase::play_start_search_sound( if (object().agent_manager().member().combat_members().empty()) Msg("! I am in combat, but there is no combat members at all (including me), " "npc[%s],team[%d],squad[%d],group[%d]", - *object().cName(), object().g_Team(), object().g_Squad(), object().g_Group()); + object().cName().c_str(), object().g_Team(), object().g_Squad(), object().g_Group()); #endif // DEBUG bool search_with_allies = object().agent_manager().member().combat_members().size() > 1; @@ -297,7 +297,7 @@ void CStalkerActionCombatBase::play_enemy_lost_sound( if (object().agent_manager().member().combat_members().empty()) Msg("! I am in combat, but there is no combat members at all (including me), " "npc[%s],team[%d],squad[%d],group[%d]", - *object().cName(), object().g_Team(), object().g_Squad(), object().g_Group()); + object().cName().c_str(), object().g_Team(), object().g_Squad(), object().g_Group()); #endif // DEBUG bool search_with_allies = object().agent_manager().member().combat_members().size() > 1; diff --git a/src/xrGame/stalker_movement_manager_base.cpp b/src/xrGame/stalker_movement_manager_base.cpp index 9ccabb9db5a..e43dca5bf35 100644 --- a/src/xrGame/stalker_movement_manager_base.cpp +++ b/src/xrGame/stalker_movement_manager_base.cpp @@ -192,7 +192,7 @@ void stalker_movement_manager_base::initialize() void stalker_movement_manager_base::set_desired_position(const Fvector* position) { - VERIFY2(!position || accessible(*position) || show_restrictions(&restrictions()), *object().cName()); + VERIFY2(!position || accessible(*position) || show_restrictions(&restrictions()), object().cName().c_str()); m_target.desired_position(position); } @@ -276,7 +276,7 @@ void stalker_movement_manager_base::setup_movement_params(stalker_movement_param Fvector vertex_position = ai().level_graph().vertex_position(level_path().dest_vertex_id()); VERIFY2(restrictions().accessible(vertex_position) || show_restrictions(&restrictions()), make_string("vertex_id[%d],position[%f][%f][%f],object[%s]", vertex_id, VPUSH(vertex_position), - *object().cName())); + object().cName().c_str())); detail().set_dest_position(vertex_position); } } @@ -476,7 +476,7 @@ void stalker_movement_manager_base::parse_velocity_mask(stalker_movement_params& { CStalkerCombatPlanner& planner = smart_cast(m_object->brain().current_action()); if (planner.current_action_id() != StalkerDecisionSpace::eWorldOperatorKillWoundedEnemy) - Msg("~ stalker %s is doing bad thing (action %s)", *m_object->cName(), + Msg("~ stalker %s is doing bad thing (action %s)", m_object->cName().c_str(), planner.current_action().m_action_name); } #endif // DEBUG @@ -497,7 +497,7 @@ void stalker_movement_manager_base::parse_velocity_mask(stalker_movement_params& } VERIFY2((movement_params.m_mental_state != eMentalStateFree) || movement_params.m_body_state != eBodyStateCrouch, - *object().cName()); + object().cName().c_str()); switch (point.velocity & eVelocityMovementType) { @@ -553,10 +553,10 @@ void stalker_movement_manager_base::set_nearest_accessible_position(Fvector desi VERIFY(ai().level_graph().inside(level_vertex_id, desired_position)); - VERIFY2(restrictions().accessible(level_vertex_id) || show_restrictions(&restrictions()), *object().cName()); + VERIFY2(restrictions().accessible(level_vertex_id) || show_restrictions(&restrictions()), object().cName().c_str()); set_level_dest_vertex(level_vertex_id); - VERIFY2(restrictions().accessible(desired_position) || show_restrictions(&restrictions()), *object().cName()); + VERIFY2(restrictions().accessible(desired_position) || show_restrictions(&restrictions()), object().cName().c_str()); set_desired_position(&desired_position); } diff --git a/src/xrGame/stalker_movement_manager_smart_cover.cpp b/src/xrGame/stalker_movement_manager_smart_cover.cpp index 0592edc9720..4ecc91df368 100644 --- a/src/xrGame/stalker_movement_manager_smart_cover.cpp +++ b/src/xrGame/stalker_movement_manager_smart_cover.cpp @@ -222,10 +222,10 @@ void stalker_movement_manager_smart_cover::reach_enter_location(u32 const& time_ VERIFY(ai().level_graph().inside(level_vertex_id, position)); - VERIFY2(restrictions().accessible(level_vertex_id) || show_restrictions(&restrictions()), *object().cName()); + VERIFY2(restrictions().accessible(level_vertex_id) || show_restrictions(&restrictions()), object().cName().c_str()); CMovementManager::set_level_dest_vertex(level_vertex_id); - VERIFY2(restrictions().accessible(position) || show_restrictions(&restrictions()), *object().cName()); + VERIFY2(restrictions().accessible(position) || show_restrictions(&restrictions()), object().cName().c_str()); m_current.desired_position(&position); } else diff --git a/src/xrGame/stalker_planner.cpp b/src/xrGame/stalker_planner.cpp index a9733a5411d..7cb716abbcd 100644 --- a/src/xrGame/stalker_planner.cpp +++ b/src/xrGame/stalker_planner.cpp @@ -41,7 +41,7 @@ LPCSTR CStalkerPlanner::property2string(const _condition_type& property_id) LPCSTR CStalkerPlanner::object_name() const { VERIFY(m_object); - return (*m_object->cName()); + return (m_object->cName().c_str()); } #endif diff --git a/src/xrGame/stalker_sound_data_visitor.cpp b/src/xrGame/stalker_sound_data_visitor.cpp index b8344b23966..0187c9a31c8 100644 --- a/src/xrGame/stalker_sound_data_visitor.cpp +++ b/src/xrGame/stalker_sound_data_visitor.cpp @@ -46,7 +46,7 @@ void CStalkerSoundDataVisitor::visit(CStalkerSoundData* data) if (!object().g_Alive()) return; #ifdef DEBUG - Msg("%s : Adding fiction hit by sound info from stalker %s", *object().cName(), *data->object().cName()); + Msg("%s : Adding fiction hit by sound info from stalker %s", object().cName().c_str(), data->object().cName().c_str()); #endif //#ifdef DEBUG object().memory().make_object_visible_somewhen(data->object().memory().enemy().selected()); diff --git a/src/xrGame/step_manager.cpp b/src/xrGame/step_manager.cpp index 3dcc3b7aeaf..00b766402a2 100644 --- a/src/xrGame/step_manager.cpp +++ b/src/xrGame/step_manager.cpp @@ -203,7 +203,7 @@ void CStepManager::update(bool b_hud_view) // Играть партиклы if (b_play && !mtl_pair->CollideParticles.empty()) { - LPCSTR ps_name = *mtl_pair->CollideParticles[::Random.randI(0, mtl_pair->CollideParticles.size())]; + LPCSTR ps_name = mtl_pair->CollideParticles[::Random.randI(0, mtl_pair->CollideParticles.size())].c_str(); //отыграть партиклы столкновения материалов CParticlesObject* ps = CParticlesObject::Create(ps_name, TRUE); @@ -273,16 +273,16 @@ void CStepManager::load_foot_bones(CInifile::Sect& data) { const CInifile::Item& item = *I; - u16 index = smart_cast(m_object->Visual())->LL_BoneID(*item.second); - VERIFY3(index != BI_NONE, "foot bone not found", *item.second); + u16 index = smart_cast(m_object->Visual())->LL_BoneID(item.second.c_str()); + VERIFY3(index != BI_NONE, "foot bone not found", item.second.c_str()); - if (xr_strcmp(*item.first, "front_left") == 0) + if (xr_strcmp(item.first.c_str(), "front_left") == 0) m_foot_bones[eFrontLeft] = index; - else if (xr_strcmp(*item.first, "front_right") == 0) + else if (xr_strcmp(item.first.c_str(), "front_right") == 0) m_foot_bones[eFrontRight] = index; - else if (xr_strcmp(*item.first, "back_right") == 0) + else if (xr_strcmp(item.first.c_str(), "back_right") == 0) m_foot_bones[eBackRight] = index; - else if (xr_strcmp(*item.first, "back_left") == 0) + else if (xr_strcmp(item.first.c_str(), "back_left") == 0) m_foot_bones[eBackLeft] = index; } } @@ -296,9 +296,9 @@ void CStepManager::reload_foot_bones() } else { - if (!pSettings->line_exist(*m_object->cNameSect(), "foot_bones")) + if (!pSettings->line_exist(m_object->cNameSect().c_str(), "foot_bones")) R_ASSERT2(false, "section [foot_bones] not found in monster user_data"); - load_foot_bones(pSettings->r_section(pSettings->r_string(*m_object->cNameSect(), "foot_bones"))); + load_foot_bones(pSettings->r_section(pSettings->r_string(m_object->cNameSect().c_str(), "foot_bones"))); } // проверка на соответсвие diff --git a/src/xrGame/team_hierarchy_holder.cpp b/src/xrGame/team_hierarchy_holder.cpp index e25f4fcf536..1faa6f119f8 100644 --- a/src/xrGame/team_hierarchy_holder.cpp +++ b/src/xrGame/team_hierarchy_holder.cpp @@ -15,7 +15,7 @@ CTeamHierarchyHolder::~CTeamHierarchyHolder() { delete_data(m_squads); } CSquadHierarchyHolder& CTeamHierarchyHolder::squad(u32 squad_id) const { - VERIFY3(squad_id < max_squad_count, "Squad id is invalid : ", *SeniorityHierarchy::to_string(squad_id)); + VERIFY3(squad_id < max_squad_count, "Squad id is invalid : ", SeniorityHierarchy::to_string(squad_id).c_str()); if (!m_squads[squad_id]) m_squads[squad_id] = xr_new(const_cast(this)); return (*m_squads[squad_id]); diff --git a/src/xrGame/trade_parameters_inline.h b/src/xrGame/trade_parameters_inline.h index d94d6f98b17..40cc2fc8846 100644 --- a/src/xrGame/trade_parameters_inline.h +++ b/src/xrGame/trade_parameters_inline.h @@ -67,7 +67,7 @@ IC const CTradeFactors& CTradeParameters::factors(_action_type type, const share template IC void CTradeParameters::process(_action_type type, CInifile& ini_file, const shared_str& section) { - R_ASSERT2(ini_file.section_exist(section), make_string("cannot find section %s", *section)); + R_ASSERT2(ini_file.section_exist(section), make_string("cannot find section %s", section.c_str())); CTradeActionParameters& _action = action(type); _action.clear(); diff --git a/src/xrGame/ui/ChangeWeatherDialog.cpp b/src/xrGame/ui/ChangeWeatherDialog.cpp index a3da7be7e3f..34d73578cc7 100644 --- a/src/xrGame/ui/ChangeWeatherDialog.cpp +++ b/src/xrGame/ui/ChangeWeatherDialog.cpp @@ -112,7 +112,7 @@ void ChangeWeatherDialog::InitChangeWeather(CUIXml& xmlDoc) void ChangeWeatherDialog::OnButtonClick(int i) { string1024 command; - xr_sprintf(command, "cl_votestart changeweather %s %s", *weathers[i].Name, *weathers[i].Time); + xr_sprintf(command, "cl_votestart changeweather %s %s", weathers[i].Name.c_str(), weathers[i].Time.c_str()); Console->Execute(command); HideDialog(); } diff --git a/src/xrGame/ui/ServerList.cpp b/src/xrGame/ui/ServerList.cpp index 11b5820a045..14dcb3ba485 100644 --- a/src/xrGame/ui/ServerList.cpp +++ b/src/xrGame/ui/ServerList.cpp @@ -218,14 +218,14 @@ void CServerList::AddServerDetail(const GameInfo& info) void CServerList::AddBoolED(const char* keyName, bool value) { - AddServerDetail(GameInfo(*StringTable().translate(keyName), - value ? *StringTable().translate("mp_si_enabled") : *StringTable().translate("mp_si_disabled"))); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), + value ? StringTable().translate("mp_si_enabled").c_str() : StringTable().translate("mp_si_disabled").c_str())); } void CServerList::AddBoolYN(const char* keyName, bool value) { - AddServerDetail(GameInfo(*StringTable().translate(keyName), - value ? *StringTable().translate("mp_si_yes") : *StringTable().translate("mp_si_no"))); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), + value ? StringTable().translate("mp_si_yes").c_str() : StringTable().translate("mp_si_no").c_str())); } void CServerList::AddBoolKeyED(void* s, const char* keyName, int k) { AddBoolED(keyName, browser().GetBool(s, k)); } @@ -234,7 +234,7 @@ void CServerList::AddIntKey(void* s, const char* keyName, int k) { string256 tmp; xr_sprintf(tmp, "%d", browser().GetInt(s, k)); - AddServerDetail(GameInfo(*StringTable().translate(keyName), tmp)); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), tmp)); } void CServerList::AddIntKeyN(void* s, float m, const char* keyName, const char* suffix, int k) @@ -243,10 +243,10 @@ void CServerList::AddIntKeyN(void* s, float m, const char* keyName, const char* { string256 tmp; xr_sprintf(tmp, "%d%s", int(browser().GetInt(s, k) * m), suffix); - AddServerDetail(GameInfo(*StringTable().translate(keyName), tmp)); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), tmp)); } else - AddServerDetail(GameInfo(*StringTable().translate(keyName), *StringTable().translate("mp_si_no"))); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), StringTable().translate("mp_si_no").c_str())); } void CServerList::AddTimeKey(void* s, const char* keyName, const char* format, const char* suffix, int k) @@ -254,21 +254,21 @@ void CServerList::AddTimeKey(void* s, const char* keyName, const char* format, c if (browser().GetInt(s, k)) { string256 tmp; - xr_sprintf(tmp, format, browser().GetFloat(s, k), *StringTable().translate(suffix)); - AddServerDetail(GameInfo(*StringTable().translate(keyName), tmp)); + xr_sprintf(tmp, format, browser().GetFloat(s, k), StringTable().translate(suffix).c_str()); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), tmp)); } else - AddServerDetail(GameInfo(*StringTable().translate(keyName), *StringTable().translate("mp_si_no"))); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), StringTable().translate("mp_si_no").c_str())); } void CServerList::AddString(const char* key, const char* value) { - AddServerDetail(GameInfo(*StringTable().translate(key), value)); + AddServerDetail(GameInfo(StringTable().translate(key).c_str(), value)); } void CServerList::AddStringSt(const char* key, const char* value) { - AddServerDetail(GameInfo(*StringTable().translate(key), *StringTable().translate(value))); + AddServerDetail(GameInfo(StringTable().translate(key).c_str(), StringTable().translate(value).c_str())); } void CServerList::FillUpDetailedServerInfo() diff --git a/src/xrGame/ui/TeamInfo.cpp b/src/xrGame/ui/TeamInfo.cpp index 422bd1fbab9..2718c84392e 100644 --- a/src/xrGame/ui/TeamInfo.cpp +++ b/src/xrGame/ui/TeamInfo.cpp @@ -67,9 +67,9 @@ LPCSTR CTeamInfo::GetTeam_name(int team) string32 tmp; R_ASSERT2(team == 1 || team == 2 || team == 3, xr_itoa(team, tmp, 10)); if (team == 1) - return *GetTeam1_name(); + return GetTeam1_name().c_str(); else - return *GetTeam2_name(); + return GetTeam2_name().c_str(); } LPCSTR CTeamInfo::GetTeam_color_tag(int team) @@ -108,12 +108,12 @@ LPCSTR CTeamInfo::GetTeam_color_tag(int team) { flags.set(flTeam1_col_t, true); team1_color_tag = str.c_str(); - return *team1_color_tag; + return team1_color_tag.c_str(); } else { flags.set(flTeam2_col_t, true); team2_color_tag = str.c_str(); - return *team2_color_tag; + return team2_color_tag.c_str(); } } diff --git a/src/xrGame/ui/UIActorInfo.cpp b/src/xrGame/ui/UIActorInfo.cpp index ce2673345a3..72b1ae158ba 100644 --- a/src/xrGame/ui/UIActorInfo.cpp +++ b/src/xrGame/ui/UIActorInfo.cpp @@ -219,7 +219,7 @@ void CUIActorInfoWnd::FillPointsDetail(const shared_str& id) xr_sprintf(buff, "%d.", _cntr); itm->m_text0->SetText(buff); - itm->m_text1->SetTextST(*CStringTable().translate((*it).key)); + itm->m_text1->SetTextST(CStringTable().translate((*it).key).c_str()); itm->m_text1->AdjustHeightToText(); if (0 == (*it).str_value.size()) @@ -268,7 +268,7 @@ void CUIActorInfoWnd::FillReputationDetails(CUIXml* xml, LPCSTR path) CUIActorStaticticDetail* itm = xr_new(); itm->Init(xml, path, 0); comm.set(xml->Read(_list_node, "r", i, "unknown_community")); - itm->m_text1->SetTextST(*(comm.id())); + itm->m_text1->SetTextST(comm.id().c_str()); CHARACTER_GOODWILL gw = RELATION_REGISTRY().GetCommunityGoodwill(comm.index(), Actor()->ID()); gw += CHARACTER_COMMUNITY::relation(Actor()->Community(), comm.index()); diff --git a/src/xrGame/ui/UICellCustomItems.cpp b/src/xrGame/ui/UICellCustomItems.cpp index da179462e93..0ffd9d296c2 100644 --- a/src/xrGame/ui/UICellCustomItems.cpp +++ b/src/xrGame/ui/UICellCustomItems.cpp @@ -415,7 +415,7 @@ void CUIWeaponCellItem::Update() { CreateIcon(eSilencer); RefreshOffset(); - InitAddon(GetIcon(eSilencer), *object()->GetSilencerName(), m_addon_offset[eSilencer], Heading()); + InitAddon(GetIcon(eSilencer), object()->GetSilencerName().c_str(), m_addon_offset[eSilencer], Heading()); } } else @@ -433,7 +433,7 @@ void CUIWeaponCellItem::Update() { CreateIcon(eScope); RefreshOffset(); - InitAddon(GetIcon(eScope), *object()->GetScopeName(), m_addon_offset[eScope], Heading()); + InitAddon(GetIcon(eScope), object()->GetScopeName().c_str(), m_addon_offset[eScope], Heading()); } } else @@ -452,7 +452,7 @@ void CUIWeaponCellItem::Update() CreateIcon(eLauncher); RefreshOffset(); InitAddon( - GetIcon(eLauncher), *object()->GetGrenadeLauncherName(), m_addon_offset[eLauncher], Heading()); + GetIcon(eLauncher), object()->GetGrenadeLauncherName().c_str(), m_addon_offset[eLauncher], Heading()); } } else @@ -483,15 +483,15 @@ void CUIWeaponCellItem::SetTextureColor(u32 color) void CUIWeaponCellItem::OnAfterChild(CUIDragDropListEx* parent_list) { if (is_silencer() && GetIcon(eSilencer)) - InitAddon(GetIcon(eSilencer), *object()->GetSilencerName(), m_addon_offset[eSilencer], + InitAddon(GetIcon(eSilencer), object()->GetSilencerName().c_str(), m_addon_offset[eSilencer], parent_list->GetVerticalPlacement()); if (is_scope() && GetIcon(eScope)) InitAddon( - GetIcon(eScope), *object()->GetScopeName(), m_addon_offset[eScope], parent_list->GetVerticalPlacement()); + GetIcon(eScope), object()->GetScopeName().c_str(), m_addon_offset[eScope], parent_list->GetVerticalPlacement()); if (is_launcher() && GetIcon(eLauncher)) - InitAddon(GetIcon(eLauncher), *object()->GetGrenadeLauncherName(), m_addon_offset[eLauncher], + InitAddon(GetIcon(eLauncher), object()->GetGrenadeLauncherName().c_str(), m_addon_offset[eLauncher], parent_list->GetVerticalPlacement()); } @@ -561,7 +561,7 @@ CUIDragItem* CUIWeaponCellItem::CreateDragItem() s = xr_new("Silencer"); s->SetAutoDelete(true); s->SetShader(InventoryUtilities::GetEquipmentIconsShader()); - InitAddon(s, *object()->GetSilencerName(), m_addon_offset[eSilencer], false); + InitAddon(s, object()->GetSilencerName().c_str(), m_addon_offset[eSilencer], false); s->SetTextureColor(i->wnd()->GetTextureColor()); i->wnd()->AttachChild(s); } @@ -571,7 +571,7 @@ CUIDragItem* CUIWeaponCellItem::CreateDragItem() s = xr_new("Scope"); s->SetAutoDelete(true); s->SetShader(InventoryUtilities::GetEquipmentIconsShader()); - InitAddon(s, *object()->GetScopeName(), m_addon_offset[eScope], false); + InitAddon(s, object()->GetScopeName().c_str(), m_addon_offset[eScope], false); s->SetTextureColor(i->wnd()->GetTextureColor()); i->wnd()->AttachChild(s); } @@ -581,7 +581,7 @@ CUIDragItem* CUIWeaponCellItem::CreateDragItem() s = xr_new("Grenade launcher"); s->SetAutoDelete(true); s->SetShader(InventoryUtilities::GetEquipmentIconsShader()); - InitAddon(s, *object()->GetGrenadeLauncherName(), m_addon_offset[eLauncher], false); + InitAddon(s, object()->GetGrenadeLauncherName().c_str(), m_addon_offset[eLauncher], false); s->SetTextureColor(i->wnd()->GetTextureColor()); i->wnd()->AttachChild(s); } diff --git a/src/xrGame/ui/UIColorAnimatorWrapper.cpp b/src/xrGame/ui/UIColorAnimatorWrapper.cpp index a187a2885b8..70bf6d1cd31 100644 --- a/src/xrGame/ui/UIColorAnimatorWrapper.cpp +++ b/src/xrGame/ui/UIColorAnimatorWrapper.cpp @@ -33,7 +33,7 @@ CUIColorAnimatorWrapper::CUIColorAnimatorWrapper(u32* colorToModify) ////////////////////////////////////////////////////////////////////////// CUIColorAnimatorWrapper::CUIColorAnimatorWrapper(const shared_str& animationName) - : colorAnimation(LALib.FindItem(*animationName)), animationTime(0), color(NULL), isDone(false), reverse(false), + : colorAnimation(LALib.FindItem(animationName.c_str())), animationTime(0), color(NULL), isDone(false), reverse(false), kRev(0.0f) { VERIFY(colorAnimation); @@ -45,8 +45,8 @@ void CUIColorAnimatorWrapper::SetColorAnimation(const shared_str& animationName) { if (animationName.size() != 0) { - colorAnimation = LALib.FindItem(*animationName); - R_ASSERT2(colorAnimation, *animationName); + colorAnimation = LALib.FindItem(animationName.c_str()); + R_ASSERT2(colorAnimation, animationName.c_str()); } else { diff --git a/src/xrGame/ui/UIInventoryUtilities.cpp b/src/xrGame/ui/UIInventoryUtilities.cpp index 672124bf6c3..b89d340cb3f 100644 --- a/src/xrGame/ui/UIInventoryUtilities.cpp +++ b/src/xrGame/ui/UIInventoryUtilities.cpp @@ -334,20 +334,20 @@ LPCSTR InventoryUtilities::GetTimePeriodAsString(pstr _buff, u32 buff_sz, ALife: u8 yrdiff = ((year2 - year1) * 12); if (month1 != month2 || yrdiff > 0) - cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", month2 + (yrdiff - month1), *CStringTable().translate("ui_st_months")); + cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", month2 + (yrdiff - month1), CStringTable().translate("ui_st_months").c_str()); if (!cnt && day1 != day2) - cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", day2 - day1, *StringTable().translate("ui_st_days")); + cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", day2 - day1, StringTable().translate("ui_st_days").c_str()); if (!cnt && hours1 != hours2) cnt = - xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", hours2 - hours1, *StringTable().translate("ui_st_hours")); + xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", hours2 - hours1, StringTable().translate("ui_st_hours").c_str()); if (!cnt && mins1 != mins2) - cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", mins2 - mins1, *StringTable().translate("ui_st_mins")); + cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", mins2 - mins1, StringTable().translate("ui_st_mins").c_str()); if (!cnt && secs1 != secs2) - cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", secs2 - secs1, *StringTable().translate("ui_st_secs")); + cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", secs2 - secs1, StringTable().translate("ui_st_secs").c_str()); return _buff; } diff --git a/src/xrGame/ui/UIItemInfo.cpp b/src/xrGame/ui/UIItemInfo.cpp index 9f7bd546be1..d5adfbdc300 100644 --- a/src/xrGame/ui/UIItemInfo.cpp +++ b/src/xrGame/ui/UIItemInfo.cpp @@ -276,7 +276,7 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, pItem->SetFont(m_desc_info.pDescFont); pItem->SetWidth(UIDesc->GetDesiredChildWidth()); pItem->SetTextComplexMode(true); - pItem->SetText(*pInvItem->ItemDescription()); + pItem->SetText(pInvItem->ItemDescription().c_str()); pItem->AdjustHeightToText(); UIDesc->AddWindow(pItem, true); } diff --git a/src/xrGame/ui/UIKeyBinding.cpp b/src/xrGame/ui/UIKeyBinding.cpp index f79c9e16266..11e11eb585c 100644 --- a/src/xrGame/ui/UIKeyBinding.cpp +++ b/src/xrGame/ui/UIKeyBinding.cpp @@ -80,7 +80,7 @@ void CUIKeyBinding::FillUpList(CUIXml& xml_doc_ui, LPCSTR path_ui) shared_str exe = xml_doc.ReadAttrib("command", j, "exe"); #ifdef DEBUG - if (kNOTBINDED == ActionNameToId(*exe)) + if (kNOTBINDED == ActionNameToId(exe.c_str())) { Msg("action [%s] not exist. update data", exe.c_str()); continue; @@ -187,7 +187,7 @@ bool CUIKeyBinding::IsActionExist(LPCSTR action, CUIXml& xml_doc) { // first field of list item shared_str command_id = xml_doc.ReadAttrib("command", j, "exe"); - if (0 == xr_strcmp(action, *command_id)) + if (0 == xr_strcmp(action, command_id.c_str())) { ret = true; break; diff --git a/src/xrGame/ui/UIListItemServer.cpp b/src/xrGame/ui/UIListItemServer.cpp index 66474c67410..cc1523e274d 100644 --- a/src/xrGame/ui/UIListItemServer.cpp +++ b/src/xrGame/ui/UIListItemServer.cpp @@ -132,7 +132,7 @@ void CUIListItemServer::CreateConsoleCommand( xr_string& command, LPCSTR player_name, LPCSTR player_pass, LPCSTR server_psw) { command = "start client("; - command += *m_srv_info.info.address; + command += m_srv_info.info.address.c_str(); command += "/name="; command += player_name; command += "/pass="; diff --git a/src/xrGame/ui/UIMainIngameWnd.cpp b/src/xrGame/ui/UIMainIngameWnd.cpp index eadd88bdae2..633acc52355 100644 --- a/src/xrGame/ui/UIMainIngameWnd.cpp +++ b/src/xrGame/ui/UIMainIngameWnd.cpp @@ -181,14 +181,14 @@ void CUIMainIngameWnd::Init() { // Читаем данные порогов для каждого индикатора shared_str cfgRecord = - pSettings->r_string("main_ingame_indicators_thresholds", *warningStrings[static_cast(j) - 1]); - u32 count = _GetItemCount(*cfgRecord); + pSettings->r_string("main_ingame_indicators_thresholds", warningStrings[static_cast(j) - 1].c_str()); + u32 count = _GetItemCount(cfgRecord.c_str()); char singleThreshold[8]; float f = 0; for (u32 k = 0; k < count; ++k) { - _GetItem(*cfgRecord, k, singleThreshold); + _GetItem(cfgRecord.c_str(), k, singleThreshold); sscanf(singleThreshold, "%f", &f); m_Thresholds[j].push_back(f); diff --git a/src/xrGame/ui/UIMap.cpp b/src/xrGame/ui/UIMap.cpp index 9e88f0ac3b3..a52bfde1f3f 100644 --- a/src/xrGame/ui/UIMap.cpp +++ b/src/xrGame/ui/UIMap.cpp @@ -451,7 +451,7 @@ void CUILevelMap::Init_internal(const shared_str& name, const CInifile& pLtx, co if (FALSE == fsimilar(kw, kh, EPS_L)) { - Msg(" --incorrect global rect definition for map [%s] kw=%f kh=%f", *MapName(), kw, kh); + Msg(" --incorrect global rect definition for map [%s] kw=%f kh=%f", MapName().c_str(), kw, kh); Msg(" --try x2=%f or y2=%f", m_GlobalRect.x1 + kh * BoundRect().width(), m_GlobalRect.y1 + kw * BoundRect().height()); } diff --git a/src/xrGame/ui/UIMapInfo.cpp b/src/xrGame/ui/UIMapInfo.cpp index 981659a3612..650bb7c1044 100644 --- a/src/xrGame/ui/UIMapInfo.cpp +++ b/src/xrGame/ui/UIMapInfo.cpp @@ -23,13 +23,13 @@ void CUIMapInfo::InitMapInfo(Fvector2 pos, Fvector2 size) } #define ADD_TEXT(x, y, z) \ - text = *StringTable().translate(x); \ + text = StringTable().translate(x).c_str(); \ text += ": "; \ text += txt_color_tag; \ if (ltx.line_exist("map_info", y)) \ - text += *StringTable().translate(ltx.r_string_wb("map_info", y)); \ + text += StringTable().translate(ltx.r_string_wb("map_info", y)).c_str(); \ else \ - text += *StringTable().translate(z); \ + text += StringTable().translate(z).c_str(); \ text += "%c[default]\\n"; \ st = xr_new("Text"); \ st->SetTextComplexMode(true); \ @@ -90,27 +90,27 @@ void CUIMapInfo::InitMap(LPCSTR map_name, LPCSTR map_ver) shared_str _modes = ltx.r_string_wb("map_info", "modes"); - text = *StringTable().translate("modes"); + text = StringTable().translate("modes").c_str(); text += ": "; text += txt_color_tag; bool b_ = false; if (strstr(_modes.c_str(), "st_deathmatch")) { - text += *StringTable().translate("st_deathmatch"); + text += StringTable().translate("st_deathmatch").c_str(); b_ = true; } if (strstr(_modes.c_str(), "st_team_deathmatch")) { if (b_) text += ", "; - text += *StringTable().translate("st_team_deathmatch"); + text += StringTable().translate("st_team_deathmatch").c_str(); b_ = true; } if (strstr(_modes.c_str(), "st_artefacthunt")) { if (b_) text += ", "; - text += *StringTable().translate("st_artefacthunt"); + text += StringTable().translate("st_artefacthunt").c_str(); } text += "%c[default]\\n"; @@ -140,4 +140,4 @@ void CUIMapInfo::InitMap(LPCSTR map_name, LPCSTR map_ver) } } -const char* CUIMapInfo::GetLargeDesc() { return *m_large_desc; } +const char* CUIMapInfo::GetLargeDesc() { return m_large_desc.c_str(); } diff --git a/src/xrGame/ui/UIMapList.cpp b/src/xrGame/ui/UIMapList.cpp index 036cae46c8f..39b7997d94a 100644 --- a/src/xrGame/ui/UIMapList.cpp +++ b/src/xrGame/ui/UIMapList.cpp @@ -287,7 +287,7 @@ void CUIMapList::SetServerParams(LPCSTR params) { m_srv_params = params; } void CUIMapList::AddWeather(const shared_str& WeatherType, const shared_str& WeatherTime, u32 _id) { R_ASSERT2(m_pWeatherSelector, "m_pWeatherSelector == NULL"); - m_pWeatherSelector->AddItem_(*WeatherType, 0)->SetTAG(_id); + m_pWeatherSelector->AddItem_(WeatherType.c_str(), 0)->SetTAG(_id); m_mapWeather.resize(m_mapWeather.size() + 1); m_mapWeather.back().weather_name = WeatherType; diff --git a/src/xrGame/ui/UIMapWnd.cpp b/src/xrGame/ui/UIMapWnd.cpp index 75df8e9e9cf..abebb22778e 100644 --- a/src/xrGame/ui/UIMapWnd.cpp +++ b/src/xrGame/ui/UIMapWnd.cpp @@ -211,13 +211,13 @@ bool CUIMapWnd::Init(cpcstr xml_name, cpcstr start_from, bool critical /*= true* VERIFY(l2); if (l->GlobalRect().intersected(l2->GlobalRect())) { - Msg(" --error-incorrect map definition global rect of map [%s] intersects with [%s]", *l->MapName(), - *l2->MapName()); + Msg(" --error-incorrect map definition global rect of map [%s] intersects with [%s]", l->MapName().c_str(), + l2->MapName().c_str()); } } if (FALSE == l->GlobalRect().intersected(GlobalMap()->BoundRect())) { - Msg(" --error-incorrect map definition map [%s] places outside global map", *l->MapName()); + Msg(" --error-incorrect map definition map [%s] places outside global map", l->MapName().c_str()); } } #endif diff --git a/src/xrGame/ui/UIOutfitSlot.cpp b/src/xrGame/ui/UIOutfitSlot.cpp index afb48713d17..637b67c81b2 100644 --- a/src/xrGame/ui/UIOutfitSlot.cpp +++ b/src/xrGame/ui/UIOutfitSlot.cpp @@ -36,9 +36,9 @@ void CUIOutfitDragDropList::SetOutfit(CUICellItem* itm) xr_string iconName; if (pActor) - iconName = *pActor->cNameVisual(); + iconName = pActor->cNameVisual().c_str(); else - iconName = *m_default_outfit; + iconName = m_default_outfit.c_str(); xr_string::iterator it = std::find(iconName.rbegin(), iconName.rend(), '\\').base(); diff --git a/src/xrGame/ui/UISkinSelector.cpp b/src/xrGame/ui/UISkinSelector.cpp index dc1185138d3..3067a2a6773 100644 --- a/src/xrGame/ui/UISkinSelector.cpp +++ b/src/xrGame/ui/UISkinSelector.cpp @@ -29,8 +29,8 @@ CUISkinSelectorWnd::~CUISkinSelectorWnd() void CUISkinSelectorWnd::InitSkins() { - R_ASSERT2(pSettings->section_exist(m_strSection), *m_strSection); - R_ASSERT2(pSettings->line_exist(m_strSection, "skins"), *m_strSection); + R_ASSERT2(pSettings->section_exist(m_strSection), m_strSection.c_str()); + R_ASSERT2(pSettings->line_exist(m_strSection, "skins"), m_strSection.c_str()); LPCSTR lst = pSettings->r_string(m_strSection, "skins"); string256 singleItem; diff --git a/src/xrGame/ui/UIStatsPlayerInfo.cpp b/src/xrGame/ui/UIStatsPlayerInfo.cpp index ec0ea7ca87f..97a43e947da 100644 --- a/src/xrGame/ui/UIStatsPlayerInfo.cpp +++ b/src/xrGame/ui/UIStatsPlayerInfo.cpp @@ -70,7 +70,7 @@ void CUIStatsPlayerInfo::Update() xr_vector& field_info = *m_field_info; for (u32 i = 0; i < m_fields.size(); i++) - m_fields[i]->TextItemControl()->SetText(GetInfoByID(*field_info[i].name)); + m_fields[i]->TextItemControl()->SetText(GetInfoByID(field_info[i].name.c_str())); m_pPlayerInfo = NULL; } @@ -145,7 +145,7 @@ const char* CUIStatsPlayerInfo::GetInfoByID(const char* id) else if (0 == xr_strcmp(id, "status")) { if (m_pPlayerInfo->testFlag(GAME_PLAYER_FLAG_READY)) - xr_strcpy(ans, *StringTable().translate("st_mp_ready")); + xr_strcpy(ans, StringTable().translate("st_mp_ready").c_str()); else xr_strcpy(ans, ""); } diff --git a/src/xrGame/ui/UIStatsPlayerList.cpp b/src/xrGame/ui/UIStatsPlayerList.cpp index 06d72e00391..384b7f40259 100644 --- a/src/xrGame/ui/UIStatsPlayerList.cpp +++ b/src/xrGame/ui/UIStatsPlayerList.cpp @@ -134,7 +134,7 @@ void CUIStatsPlayerList::InitHeader(CUIXml& xml_doc, LPCSTR path) st->SetText(""); else { - st->SetTextST(GetST_entry(*m_field_info[i].name)); + st->SetTextST(GetST_entry(m_field_info[i].name.c_str())); } if (m_h.f) @@ -234,16 +234,16 @@ void CUIStatsPlayerList::Update() { game_cl_ArtefactHunt* game = static_cast(&Game()); pl_artefacts = game->teams[m_CurTeam - 1].score; - xr_sprintf(teaminfo, "%s: %u, %s: %u, %s: %d", *StringTable().translate("mp_artefacts_upcase"), pl_artefacts, - *StringTable().translate("mp_players"), pl_count, *StringTable().translate("mp_frags_upcase"), pl_frags); + xr_sprintf(teaminfo, "%s: %u, %s: %u, %s: %d", StringTable().translate("mp_artefacts_upcase").c_str(), pl_artefacts, + StringTable().translate("mp_players").c_str(), pl_count, StringTable().translate("mp_frags_upcase").c_str(), pl_frags); m_header_text->SetText(teaminfo); } else if (GameID() == eGameIDTeamDeathmatch && !m_bSpectator) { game_cl_TeamDeathmatch* game = static_cast(&Game()); pl_frags = game->teams[m_CurTeam - 1].score; - xr_sprintf(teaminfo, "%s: %d, %s: %u", *StringTable().translate("mp_frags_upcase"), pl_frags, - *StringTable().translate("mp_players"), pl_count); + xr_sprintf(teaminfo, "%s: %d, %s: %u", StringTable().translate("mp_frags_upcase").c_str(), pl_frags, + StringTable().translate("mp_players").c_str(), pl_count); m_header_text->SetText(teaminfo); } diff --git a/src/xrGame/ui/UITalkWnd.cpp b/src/xrGame/ui/UITalkWnd.cpp index ce57eb257d1..69b277e88c0 100644 --- a/src/xrGame/ui/UITalkWnd.cpp +++ b/src/xrGame/ui/UITalkWnd.cpp @@ -316,7 +316,7 @@ void CUITalkWnd::AddAnswer(const shared_str& text, LPCSTR SpeakerName) PlaySnd(text.c_str()); bool i_am = (0 == xr_strcmp(SpeakerName, m_pOurInvOwner->Name())); // XXX: not reliable when both persons have same names - UITalkDialogWnd->AddAnswer(SpeakerName, *StringTable().translate(text), i_am); + UITalkDialogWnd->AddAnswer(SpeakerName, StringTable().translate(text).c_str(), i_am); } void CUITalkWnd::SwitchToTrade() diff --git a/src/xrGame/vision_client.cpp b/src/xrGame/vision_client.cpp index 88c4aef6685..eaa59705027 100644 --- a/src/xrGame/vision_client.cpp +++ b/src/xrGame/vision_client.cpp @@ -98,7 +98,7 @@ void vision_client::shedule_Update(u32 dt) shared_str vision_client::shedule_Name() const { string256 temp; - xr_sprintf(temp, "vision_client[%s]", *object().cName()); + xr_sprintf(temp, "vision_client[%s]", object().cName().c_str()); return (temp); } diff --git a/src/xrGame/xrGameSpyServer.cpp b/src/xrGame/xrGameSpyServer.cpp index 8a213df56b4..6d80d85799c 100644 --- a/src/xrGame/xrGameSpyServer.cpp +++ b/src/xrGame/xrGameSpyServer.cpp @@ -63,7 +63,7 @@ xrGameSpyServer::EConnect xrGameSpyServer::Connect(shared_str& session_name, Gam if (res != ErrNoError) return res; - if (0 == *(game->get_option_s(*session_name, "hname", NULL))) + if (0 == *(game->get_option_s(session_name.c_str(), "hname", NULL))) { #ifdef XR_PLATFORM_WINDOWS // XXX: add Linux implementation string1024 CompName; @@ -73,20 +73,20 @@ xrGameSpyServer::EConnect xrGameSpyServer::Connect(shared_str& session_name, Gam #endif } else - HostName = game->get_option_s(*session_name, "hname", NULL); + HostName = game->get_option_s(session_name.c_str(), "hname", NULL); - if (0 != *(game->get_option_s(*session_name, "psw", NULL))) - Password._set(game->get_option_s(*session_name, "psw", NULL)); + if (0 != *(game->get_option_s(session_name.c_str(), "psw", NULL))) + Password._set(game->get_option_s(session_name.c_str(), "psw", NULL)); string4096 tMapName = ""; - const char* SName = *session_name; - strncpy_s(tMapName, *session_name, strchr(SName, '/') - SName); + const char* SName = session_name.c_str(); + strncpy_s(tMapName, session_name.c_str(), strchr(SName, '/') - SName); MapName = tMapName; // = (session_name); - m_iReportToMasterServer = game->get_option_i(*session_name, "public", 0); - m_iMaxPlayers = game->get_option_i(*session_name, "maxplayers", 32); + m_iReportToMasterServer = game->get_option_i(session_name.c_str(), "public", 0); + m_iMaxPlayers = game->get_option_i(session_name.c_str(), "maxplayers", 32); // m_bCheckCDKey = game->get_option_i (*session_name,"cdkey",0) != 0; - m_bCheckCDKey = game->get_option_i(*session_name, "public", 0) != 0; + m_bCheckCDKey = game->get_option_i(session_name.c_str(), "public", 0) != 0; //--------------------------------------------// if (game->Type() != eGameIDSingle) { @@ -95,11 +95,11 @@ xrGameSpyServer::EConnect xrGameSpyServer::Connect(shared_str& session_name, Gam shared_str result_string; if (!GSA.CheckAvailableServices(result_string)) { - Msg(*result_string); + Msg(session_name.c_str()); }; //------ Init of QR2 SDK ------------- - iGameSpyBasePort = game->get_option_i(*session_name, "portgs", -1); + iGameSpyBasePort = game->get_option_i(session_name.c_str(), "portgs", -1); QR2_Init(iGameSpyBasePort); //------ Init of CDKey SDK ----------- diff --git a/src/xrGame/xrServer_Connect.cpp b/src/xrGame/xrServer_Connect.cpp index 50f7bbd5a43..9ceb9721f15 100644 --- a/src/xrGame/xrServer_Connect.cpp +++ b/src/xrGame/xrServer_Connect.cpp @@ -36,16 +36,16 @@ LPCSTR xrServer::get_map_download_url(LPCSTR level_name, LPCSTR level_version) xrServer::EConnect xrServer::Connect(shared_str& session_name, GameDescriptionData& game_descr) { #ifdef DEBUG - Msg("* sv_Connect: %s", *session_name); + Msg("* sv_Connect: %s", session_name.c_str()); #endif // Parse options and create game - if (0 == strchr(*session_name, '/')) + if (0 == strchr(session_name.c_str(), '/')) return ErrConnect; string1024 options; R_ASSERT2(xr_strlen(session_name) <= sizeof(options), "session_name too BIIIGGG!!!"); - xr_strcpy(options, strchr(*session_name, '/') + 1); + xr_strcpy(options, strchr(session_name.c_str(), '/') + 1); // Parse game type string1024 type; @@ -83,7 +83,7 @@ xrServer::EConnect xrServer::Connect(shared_str& session_name, GameDescriptionDa game->Create(session_name); - return IPureServer::Connect(*session_name, game_descr); + return IPureServer::Connect(session_name.c_str(), game_descr); } IClient* xrServer::new_client(SClientConnectData* cl_data) diff --git a/src/xrGame/xrServer_process_event.cpp b/src/xrGame/xrServer_process_event.cpp index 64e0037dca5..e7e43a099fd 100644 --- a/src/xrGame/xrServer_process_event.cpp +++ b/src/xrGame/xrServer_process_event.cpp @@ -202,7 +202,7 @@ void xrServer::Process_event(NET_Packet& P, ClientID sender) #ifndef MASTER_GOLD if (game->Type() != eGameIDSingle) - Msg("* [%2d] is [%s:%s]", id_dest, *e_dest->s_name, e_dest->name_replace()); + Msg("* [%2d] is [%s:%s]", id_dest, e_dest->s_name.c_str(), e_dest->name_replace()); #endif // #ifndef MASTER_GOLD CSE_Abstract* e_src = game->get_entity_from_eid(id_src); // кто убил @@ -221,7 +221,7 @@ void xrServer::Process_event(NET_Packet& P, ClientID sender) // R_ASSERT2 (e_dest && e_src, "Killer or/and being killed are offline or not exist at all :("); #ifndef MASTER_GOLD if (game->Type() != eGameIDSingle) - Msg("* [%2d] is [%s:%s]", id_src, *e_src->s_name, e_src->name_replace()); + Msg("* [%2d] is [%s:%s]", id_src, e_src->s_name.c_str(), e_src->name_replace()); #endif // #ifndef MASTER_GOLD game->on_death(e_dest, e_src); diff --git a/src/xrGame/xrServer_process_event_activate.cpp b/src/xrGame/xrServer_process_event_activate.cpp index 185167ffdad..b5060ce02be 100644 --- a/src/xrGame/xrServer_process_event_activate.cpp +++ b/src/xrGame/xrServer_process_event_activate.cpp @@ -28,7 +28,7 @@ void xrServer::Process_event_activate( { #ifndef MASTER_GOLD Msg("~ ERROR: can't activate independant object. entity[%s:%d], parent[%s:%d], section[%s]", - e_entity->name_replace(), id_entity, e_parent->name_replace(), id_parent, *e_entity->s_name); + e_entity->name_replace(), id_entity, e_parent->name_replace(), id_parent, e_entity->s_name.c_str()); #endif // #ifndef MASTER_GOLD return; } diff --git a/src/xrGame/xrServer_process_spawn.cpp b/src/xrGame/xrServer_process_spawn.cpp index a4cf8657944..3f1b973a615 100644 --- a/src/xrGame/xrServer_process_spawn.cpp +++ b/src/xrGame/xrServer_process_spawn.cpp @@ -32,7 +32,7 @@ CSE_Abstract* xrServer::Process_spawn( !E->m_gameType.MatchType((u16)game->Type()) || !E->match_configuration() || !game->OnPreCreate(E)) { #ifndef MASTER_GOLD - Msg("- SERVER: Entity [%s] incompatible with current game type.", *E->s_name); + Msg("- SERVER: Entity [%s] incompatible with current game type.", E->s_name.c_str()); #endif // #ifndef MASTER_GOLD F_entity_Destroy(E); return NULL; @@ -76,7 +76,7 @@ CSE_Abstract* xrServer::Process_spawn( if (E->RespawnTime && (0xffff == E->ID_Phantom)) { // Create phantom - CSE_Abstract* Phantom = entity_Create(*E->s_name); + CSE_Abstract* Phantom = entity_Create(E->s_name.c_str()); R_ASSERT(Phantom); Phantom->Spawn_Read(P); Phantom->ID = PerformIDgen(0xffff); diff --git a/src/xrGame/xr_time.cpp b/src/xrGame/xr_time.cpp index 56fca449c6a..39ebb80bd5c 100644 --- a/src/xrGame/xr_time.cpp +++ b/src/xrGame/xr_time.cpp @@ -17,11 +17,11 @@ u32 get_time() { return u32(__game_time() & u32(-1)); } xrTime get_time_struct() { return xrTime(__game_time()); } LPCSTR xrTime::dateToString(int mode) { - return *InventoryUtilities::GetDateAsString(m_time, (InventoryUtilities::EDatePrecision)mode); + return InventoryUtilities::GetDateAsString(m_time, (InventoryUtilities::EDatePrecision)mode).c_str(); } LPCSTR xrTime::timeToString(int mode) { - return *InventoryUtilities::GetTimeAsString(m_time, (InventoryUtilities::ETimePrecision)mode); + return InventoryUtilities::GetTimeAsString(m_time, (InventoryUtilities::ETimePrecision)mode).c_str(); } void xrTime::add(const xrTime& other) { m_time += other.m_time; } diff --git a/src/xrGame/zone_effector.cpp b/src/xrGame/zone_effector.cpp index 21fde1167a9..90523af7ebe 100644 --- a/src/xrGame/zone_effector.cpp +++ b/src/xrGame/zone_effector.cpp @@ -41,7 +41,7 @@ void CZoneEffector::Activate() m_pp_effector->SetType(EEffectorPPType(u32(u64(this) & u32(-1)))); m_pp_effector->SetCyclic(true); m_pp_effector->SetFactorFunc(GET_KOEFF_FUNC(this, &CZoneEffector::GetFactor)); - m_pp_effector->Load(*m_pp_fname); + m_pp_effector->Load(m_pp_fname.c_str()); m_pActor->Cameras().AddPPEffector(m_pp_effector); } diff --git a/src/xrNetServer/empty/NET_Client.cpp b/src/xrNetServer/empty/NET_Client.cpp index c898d533ea1..d514fd58e1f 100644 --- a/src/xrNetServer/empty/NET_Client.cpp +++ b/src/xrNetServer/empty/NET_Client.cpp @@ -406,7 +406,7 @@ bool IPureClient::Connect(pcstr options) net_csEnumeration->Enter(); // real connect for (u32 I = 0; I < net_Hosts.size(); I++) - Msg("* HOST #%d: %s\n", I + 1, *net_Hosts[I].dpSessionName); + Msg("* HOST #%d: %s\n", I + 1, net_Hosts[I].dpSessionName.c_str()); if (res != S_OK) return false; diff --git a/src/xrNetServer/empty/NET_Client.h b/src/xrNetServer/empty/NET_Client.h index 2f3b20c09cb..407441afac8 100644 --- a/src/xrNetServer/empty/NET_Client.h +++ b/src/xrNetServer/empty/NET_Client.h @@ -105,7 +105,7 @@ class XRNETSERVER_API IPureClient : MultipacketReciever, { return m_game_description; } - pcstr net_SessionName() { return *net_Hosts.front().dpSessionName; } + pcstr net_SessionName() { return net_Hosts.front().dpSessionName.c_str(); } // receive void StartProcessQueue() { diff --git a/src/xrPhysics/PHScriptCall.cpp b/src/xrPhysics/PHScriptCall.cpp index 9f97add8ccb..b10496852c6 100644 --- a/src/xrPhysics/PHScriptCall.cpp +++ b/src/xrPhysics/PHScriptCall.cpp @@ -66,7 +66,7 @@ bool CPHScriptObjectAction::compare(const CPHScriptObjectAction* v) const } void CPHScriptObjectAction::run() { - luabind::call_member(*m_lua_object, *m_method_name); + luabind::call_member(*m_lua_object, m_method_name.c_str()); b_obsolete = true; } @@ -90,7 +90,7 @@ bool CPHScriptObjectCondition::compare(const CPHScriptObjectCondition* v) const return m_method_name == v->m_method_name && compare_safe(*m_lua_object, *(v->m_lua_object)); } -bool CPHScriptObjectCondition::is_true() { return luabind::call_member(*m_lua_object, *m_method_name); } +bool CPHScriptObjectCondition::is_true() { return luabind::call_member(*m_lua_object, m_method_name.c_str()); } bool CPHScriptObjectCondition::obsolete() const { return false; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// CPHScriptObjectActionN::CPHScriptObjectActionN(const luabind::object& object, const luabind::functor& functor) diff --git a/src/xrServerEntities/PropertiesListTypes.h b/src/xrServerEntities/PropertiesListTypes.h index e113ddce11f..cb1ad436061 100644 --- a/src/xrServerEntities/PropertiesListTypes.h +++ b/src/xrServerEntities/PropertiesListTypes.h @@ -765,7 +765,7 @@ class RTokenValue : public CustomValue, public RTokenValueCustom else for (u32 k = 0; k < token_count; k++) if ((T)token[k].id == this->GetValue()) - return *token[k].name; + return token[k].name.c_str(); return draw_val; } }; diff --git a/src/xrServerEntities/object_factory_script.cpp b/src/xrServerEntities/object_factory_script.cpp index 8ba708028c9..011e9490806 100644 --- a/src/xrServerEntities/object_factory_script.cpp +++ b/src/xrServerEntities/object_factory_script.cpp @@ -75,7 +75,7 @@ void CObjectFactory::register_script() const const_iterator I = clsids().begin(), B = I; const_iterator E = clsids().end(); for (; I != E; ++I) - instance.enum_("_clsid")[luabind::value(*(*I)->script_clsid(), int(I - B))]; + instance.enum_("_clsid")[luabind::value((*I)->script_clsid().c_str(), int(I - B))]; luabind::module(GEnv.ScriptEngine->lua())[instance]; } diff --git a/src/xrServerEntities/script_net_packet_script.cpp b/src/xrServerEntities/script_net_packet_script.cpp index 2cbf48a1b6d..5524bd877ac 100644 --- a/src/xrServerEntities/script_net_packet_script.cpp +++ b/src/xrServerEntities/script_net_packet_script.cpp @@ -84,7 +84,7 @@ void CScriptNetPacket::script_register(lua_State* luaState) { shared_str temp; self->r_stringZ(temp); - return (*temp); + return (temp.c_str()); }) .def("r_matrix", &NET_Packet::r_matrix) .def("r_clientID", +[](NET_Packet* self) diff --git a/src/xrServerEntities/xrServer_Objects_ALife_Monsters_script.cpp b/src/xrServerEntities/xrServer_Objects_ALife_Monsters_script.cpp index cc119da67f4..4e2137645b2 100644 --- a/src/xrServerEntities/xrServer_Objects_ALife_Monsters_script.cpp +++ b/src/xrServerEntities/xrServer_Objects_ALife_Monsters_script.cpp @@ -38,7 +38,7 @@ void CSE_ALifeTraderAbstract::script_register(lua_State* luaState) selected_char.Load(ta->m_SpecificCharacter); ta->m_icon_name = selected_char.IconName(); } - return *ta->m_icon_name; + return ta->m_icon_name.c_str(); }) ]; } diff --git a/src/xrServerEntities/xrServer_Objects_Alife_Smartcovers.cpp b/src/xrServerEntities/xrServer_Objects_Alife_Smartcovers.cpp index 757eb479386..6413f4397c8 100644 --- a/src/xrServerEntities/xrServer_Objects_Alife_Smartcovers.cpp +++ b/src/xrServerEntities/xrServer_Objects_Alife_Smartcovers.cpp @@ -29,7 +29,7 @@ BOOL is_combat_cover(shared_str const& table_id) string256 temp; xr_strcpy(temp, "smart_covers.descriptions."); - xr_strcat(temp, *table_id); + xr_strcat(temp, table_id.c_str()); luabind::object table, value; bool result = GEnv.ScriptEngine->function_object(temp, table, LUA_TTABLE); @@ -129,20 +129,20 @@ void CSE_SmartCover::UPDATE_Write(NET_Packet& tNetPacket) { inherited1::UPDATE_W #ifndef MASTER_GOLD void CSE_SmartCover::FillProps(LPCSTR pref, PropItemVec& items) { - PHelper().CreateFloat(items, PrepareKey(pref, *s_name, "hold position time"), &m_hold_position_time, 0.f, 60.f); - RListValue* value = PHelper().CreateRList(items, PrepareKey(pref, *s_name, "description"), &m_description, + PHelper().CreateFloat(items, PrepareKey(pref, s_name.c_str(), "hold position time"), &m_hold_position_time, 0.f, 60.f); + RListValue* value = PHelper().CreateRList(items, PrepareKey(pref, s_name.c_str(), "description"), &m_description, &*fp_data.smart_covers.begin(), fp_data.smart_covers.size()); value->OnChangeEvent.bind(this, &CSE_SmartCover::OnChangeDescription); PHelper().CreateFloat( - items, PrepareKey(pref, *s_name, "enter min enemy distance"), &m_enter_min_enemy_distance, 0.f, 100.f); + items, PrepareKey(pref, s_name.c_str(), "enter min enemy distance"), &m_enter_min_enemy_distance, 0.f, 100.f); PHelper().CreateFloat( - items, PrepareKey(pref, *s_name, "exit min enemy distance"), &m_exit_min_enemy_distance, 0.f, 100.f); + items, PrepareKey(pref, s_name.c_str(), "exit min enemy distance"), &m_exit_min_enemy_distance, 0.f, 100.f); if (is_combat_cover(m_description)) { - PHelper().CreateBOOL(items, PrepareKey(pref, *s_name, "is combat cover"), &m_is_combat_cover); - PHelper().CreateBOOL(items, PrepareKey(pref, *s_name, "can fire"), &m_can_fire); + PHelper().CreateBOOL(items, PrepareKey(pref, s_name.c_str(), "is combat cover"), &m_is_combat_cover); + PHelper().CreateBOOL(items, PrepareKey(pref, s_name.c_str(), "can fire"), &m_can_fire); } } #endif // #ifndef MASTER_GOLD From 9cbe3899fbc5c3d191ce79c2cfdf9dbdc08c85e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=8B=D0=B1=D0=B0=D0=B5=D0=B2=20=D0=98=D0=BB=D1=8C?= =?UTF-8?q?=D1=8F?= <61088459+Aardes-Roiven@users.noreply.github.com> Date: Thu, 25 Dec 2025 14:06:05 +0300 Subject: [PATCH 100/261] Remove shared_str::operator* (#1948) (#2019) --- src/Layers/xrRender/ModelPool.cpp | 2 +- src/xrCore/xrstring.h | 4 +- src/xrGame/Actor.cpp | 10 ++-- src/xrGame/ActorCondition.cpp | 2 +- src/xrGame/Actor_Network.cpp | 20 +++---- src/xrGame/ContextMenu.cpp | 4 +- src/xrGame/CustomMonster.cpp | 2 +- src/xrGame/DestroyablePhysicsObject.cpp | 2 +- src/xrGame/GameObject.cpp | 52 +++++++++---------- src/xrGame/GameTask.cpp | 12 ++--- src/xrGame/GametaskManager.cpp | 2 +- src/xrGame/HangingLamp.cpp | 16 +++--- src/xrGame/Helicopter.cpp | 10 ++-- src/xrGame/Helicopter2.cpp | 12 ++--- src/xrGame/InfoPortion.cpp | 2 +- src/xrGame/Inventory.cpp | 8 +-- src/xrGame/InventoryOwner.cpp | 4 +- src/xrGame/Level.cpp | 4 +- src/xrGame/Level_network.cpp | 14 ++--- src/xrGame/Level_network_spawn.cpp | 8 +-- src/xrGame/PhraseDialog.cpp | 14 ++--- src/xrGame/PhraseScript.cpp | 28 +++++----- src/xrGame/PhysicObject.cpp | 4 +- src/xrGame/WeaponMagazined.cpp | 14 ++--- src/xrGame/actor_communication.cpp | 6 +-- src/xrGame/agent_enemy_manager.cpp | 4 +- .../basemonster/base_monster_debug.cpp | 30 +++++------ .../basemonster/base_monster_script.cpp | 2 +- src/xrGame/ai/monsters/monster_home.cpp | 12 +++-- src/xrGame/ai/stalker/ai_stalker.cpp | 12 ++--- src/xrGame/ai/stalker/ai_stalker_cover.cpp | 2 +- src/xrGame/ai/stalker/ai_stalker_debug.cpp | 46 ++++++++-------- src/xrGame/ai/trader/ai_trader.cpp | 2 +- src/xrGame/alife_simulator_base.cpp | 10 ++-- src/xrGame/alife_simulator_script.cpp | 14 ++--- src/xrGame/animation_script_callback.cpp | 2 +- src/xrGame/console_commands.cpp | 6 +-- src/xrGame/ef_storage_script.cpp | 4 +- src/xrGame/enemy_manager.cpp | 4 +- src/xrGame/fs_registrator_script.cpp | 2 +- src/xrGame/game_cl_base.cpp | 6 +-- .../game_cl_base_weapon_usage_statistic.cpp | 18 +++---- ...me_cl_base_weapon_usage_statistic_save.cpp | 2 +- src/xrGame/game_cl_capture_the_artefact.cpp | 2 +- src/xrGame/game_cl_deathmatch.cpp | 22 ++++---- src/xrGame/game_cl_deathmatch_buywnd.cpp | 10 ++-- src/xrGame/game_cl_teamdeathmatch.cpp | 10 ++-- src/xrGame/game_sv_artefacthunt.cpp | 10 ++-- src/xrGame/game_sv_capture_the_artefact.cpp | 36 ++++++------- src/xrGame/game_sv_deathmatch.cpp | 26 +++++----- src/xrGame/game_sv_mp.cpp | 12 ++--- src/xrGame/game_sv_single.cpp | 10 ++-- src/xrGame/game_sv_teamdeathmatch.cpp | 10 ++-- src/xrGame/inventory_item_upgrade.cpp | 2 +- src/xrGame/inventory_owner_info.cpp | 2 +- src/xrGame/item_manager.cpp | 8 +-- src/xrGame/level_debug.cpp | 4 +- src/xrGame/level_script.cpp | 4 +- src/xrGame/level_sounds.cpp | 2 +- src/xrGame/map_location.cpp | 16 +++--- src/xrGame/map_manager.cpp | 4 +- src/xrGame/material_manager.cpp | 4 +- src/xrGame/movement_manager.cpp | 2 +- src/xrGame/moving_objects.cpp | 6 +-- .../moving_objects_dynamic_collision.cpp | 10 ++-- src/xrGame/object_handler.cpp | 12 ++--- src/xrGame/object_handler_planner.cpp | 6 +-- src/xrGame/patrol_path_manager.cpp | 48 ++++++++--------- src/xrGame/physics_game.cpp | 2 +- src/xrGame/purchase_list.cpp | 6 +-- src/xrGame/restricted_object.cpp | 12 ++--- src/xrGame/script_entity.cpp | 22 ++++---- src/xrGame/script_game_object.cpp | 4 +- src/xrGame/script_game_object3.cpp | 6 +-- .../script_game_object_inventory_owner.cpp | 16 +++--- src/xrGame/script_game_object_use.cpp | 14 ++--- src/xrGame/script_particle_action.cpp | 2 +- src/xrGame/script_sound.cpp | 6 +-- src/xrGame/script_sound_action.cpp | 2 +- src/xrGame/seniority_hierarchy_holder.cpp | 2 +- src/xrGame/smart_cover_description.cpp | 4 +- src/xrGame/sound_player.cpp | 10 ++-- src/xrGame/space_restriction.cpp | 8 +-- src/xrGame/space_restriction_bridge_inline.h | 2 +- src/xrGame/space_restriction_composition.cpp | 10 ++-- src/xrGame/space_restriction_holder.cpp | 10 ++-- src/xrGame/space_restriction_manager.cpp | 20 +++---- src/xrGame/space_restriction_shape.cpp | 2 +- src/xrGame/squad_hierarchy_holder.cpp | 2 +- src/xrGame/stalker_animation_callbacks.cpp | 6 +-- src/xrGame/stalker_animation_global.cpp | 2 +- src/xrGame/stalker_animation_manager.cpp | 12 ++--- .../stalker_animation_manager_debug.cpp | 10 ++-- .../stalker_animation_manager_update.cpp | 2 +- src/xrGame/stalker_animation_script.cpp | 4 +- src/xrGame/stalker_animation_torso.cpp | 4 +- src/xrGame/stalker_combat_action_base.cpp | 6 +-- src/xrGame/stalker_movement_manager_base.cpp | 12 ++--- .../stalker_movement_manager_smart_cover.cpp | 4 +- src/xrGame/stalker_planner.cpp | 2 +- src/xrGame/stalker_sound_data_visitor.cpp | 2 +- src/xrGame/step_manager.cpp | 18 +++---- src/xrGame/team_hierarchy_holder.cpp | 2 +- src/xrGame/trade_parameters_inline.h | 2 +- src/xrGame/ui/ChangeWeatherDialog.cpp | 2 +- src/xrGame/ui/ServerList.cpp | 24 ++++----- src/xrGame/ui/TeamInfo.cpp | 8 +-- src/xrGame/ui/UIActorInfo.cpp | 4 +- src/xrGame/ui/UICellCustomItems.cpp | 18 +++---- src/xrGame/ui/UIColorAnimatorWrapper.cpp | 6 +-- src/xrGame/ui/UIInventoryUtilities.cpp | 10 ++-- src/xrGame/ui/UIItemInfo.cpp | 2 +- src/xrGame/ui/UIKeyBinding.cpp | 4 +- src/xrGame/ui/UIListItemServer.cpp | 2 +- src/xrGame/ui/UIMainIngameWnd.cpp | 6 +-- src/xrGame/ui/UIMap.cpp | 2 +- src/xrGame/ui/UIMapInfo.cpp | 16 +++--- src/xrGame/ui/UIMapList.cpp | 2 +- src/xrGame/ui/UIMapWnd.cpp | 6 +-- src/xrGame/ui/UIOutfitSlot.cpp | 4 +- src/xrGame/ui/UISkinSelector.cpp | 4 +- src/xrGame/ui/UIStatsPlayerInfo.cpp | 4 +- src/xrGame/ui/UIStatsPlayerList.cpp | 10 ++-- src/xrGame/ui/UITalkWnd.cpp | 2 +- src/xrGame/vision_client.cpp | 2 +- src/xrGame/xrGameSpyServer.cpp | 22 ++++---- src/xrGame/xrServer_Connect.cpp | 8 +-- src/xrGame/xrServer_process_event.cpp | 4 +- .../xrServer_process_event_activate.cpp | 2 +- src/xrGame/xrServer_process_spawn.cpp | 4 +- src/xrGame/xr_time.cpp | 4 +- src/xrGame/zone_effector.cpp | 2 +- src/xrNetServer/empty/NET_Client.cpp | 2 +- src/xrNetServer/empty/NET_Client.h | 2 +- src/xrPhysics/PHScriptCall.cpp | 4 +- src/xrServerEntities/PropertiesListTypes.h | 2 +- .../object_factory_script.cpp | 2 +- .../script_net_packet_script.cpp | 2 +- ...xrServer_Objects_ALife_Monsters_script.cpp | 2 +- .../xrServer_Objects_Alife_Smartcovers.cpp | 14 ++--- 140 files changed, 588 insertions(+), 588 deletions(-) diff --git a/src/Layers/xrRender/ModelPool.cpp b/src/Layers/xrRender/ModelPool.cpp index 678236a5492..753bb678e01 100644 --- a/src/Layers/xrRender/ModelPool.cpp +++ b/src/Layers/xrRender/ModelPool.cpp @@ -194,7 +194,7 @@ void CModelPool::Destroy() REGISTRY_IT it = Registry.begin(); dxRender_Visual* V = (dxRender_Visual*)it->first; #ifdef _DEBUG - Msg("ModelPool: Destroy object: '%s'", *V->dbg_name); + Msg("ModelPool: Destroy object: '%s'", V->dbg_name.c_str()); #endif DeleteInternal(V, TRUE); } diff --git a/src/xrCore/xrstring.h b/src/xrCore/xrstring.h index 29e0824ce51..c53123b8e31 100644 --- a/src/xrCore/xrstring.h +++ b/src/xrCore/xrstring.h @@ -138,9 +138,6 @@ class shared_str _set(nullptr); return *this; } - // XXX tamlin: Remove operator*(). It may be convenient, but it's dangerous. Use - [[nodiscard]] - pcstr operator*() const { return p_ ? p_->value : nullptr; } [[nodiscard]] bool operator!() const { return p_ == nullptr; } @@ -258,3 +255,4 @@ IC void xr_strlwr(shared_str& src) } #pragma pack(pop) +//// diff --git a/src/xrGame/Actor.cpp b/src/xrGame/Actor.cpp index a975c8f43ce..899af0eddfd 100644 --- a/src/xrGame/Actor.cpp +++ b/src/xrGame/Actor.cpp @@ -416,10 +416,10 @@ void CActor::Load(LPCSTR section) } } - sndDie[0].create(strconcat(buf, *cName(), "\\die0"), st_Effect, SOUND_TYPE_MONSTER_DYING); - sndDie[1].create(strconcat(buf, *cName(), "\\die1"), st_Effect, SOUND_TYPE_MONSTER_DYING); - sndDie[2].create(strconcat(buf, *cName(), "\\die2"), st_Effect, SOUND_TYPE_MONSTER_DYING); - sndDie[3].create(strconcat(buf, *cName(), "\\die3"), st_Effect, SOUND_TYPE_MONSTER_DYING); + sndDie[0].create(strconcat(buf, cName().c_str(), "\\die0"), st_Effect, SOUND_TYPE_MONSTER_DYING); + sndDie[1].create(strconcat(buf, cName().c_str(), "\\die1"), st_Effect, SOUND_TYPE_MONSTER_DYING); + sndDie[2].create(strconcat(buf, cName().c_str(), "\\die2"), st_Effect, SOUND_TYPE_MONSTER_DYING); + sndDie[3].create(strconcat(buf, cName().c_str(), "\\die3"), st_Effect, SOUND_TYPE_MONSTER_DYING); m_HeavyBreathSnd.create( pSettings->r_string(section, "heavy_breath_snd"), st_Effect, SOUND_TYPE_MONSTER_INJURING); @@ -2064,7 +2064,7 @@ void CActor::OnDifficultyChanged() conditions().LoadImmunities(tmp, pSettings); // hit probability strconcat(sizeof(tmp), tmp, "hit_probability_", diff_name); - m_hit_probability = pSettings->r_float(*cNameSect(), tmp); + m_hit_probability = pSettings->r_float(cNameSect().c_str(), tmp); // two hits death parameters strconcat(sizeof(tmp), tmp, "actor_thd_", diff_name); conditions().LoadTwoHitsDeathParams(tmp); diff --git a/src/xrGame/ActorCondition.cpp b/src/xrGame/ActorCondition.cpp index d35cee23df8..c6a7af5d9b5 100644 --- a/src/xrGame/ActorCondition.cpp +++ b/src/xrGame/ActorCondition.cpp @@ -248,7 +248,7 @@ void CActorCondition::UpdateCondition() { CEffectorPP* ppe = object().Cameras().GetPPEffector((EEffectorPPType)effPsyHealth); - strconcat(sizeof(pp_sect_name), pp_sect_name, "effector_psy_health", "_", *ln); + strconcat(sizeof(pp_sect_name), pp_sect_name, "effector_psy_health", "_", ln.c_str()); if (!pSettings->section_exist(pp_sect_name)) xr_strcpy(pp_sect_name, "effector_psy_health"); diff --git a/src/xrGame/Actor_Network.cpp b/src/xrGame/Actor_Network.cpp index 6c8187dd9ca..e8fc54927d6 100644 --- a/src/xrGame/Actor_Network.cpp +++ b/src/xrGame/Actor_Network.cpp @@ -873,14 +873,14 @@ void CActor::OnChangeVisual() SetCallbacks(); m_anims->Create(V); m_vehicle_anims->Create(V); - CDamageManager::reload(*cNameSect(), "damage", pSettings); + CDamageManager::reload(cNameSect().c_str(), "damage", pSettings); //------------------------------------------------------------------------------- m_head = smart_cast(Visual())->LL_BoneID("bip01_head"); m_eye_left = smart_cast(Visual())->LL_BoneID("eye_left"); m_eye_right = smart_cast(Visual())->LL_BoneID("eye_right"); - m_r_hand = smart_cast(Visual())->LL_BoneID(pSettings->r_string(*cNameSect(), "weapon_bone0")); - m_l_finger1 = smart_cast(Visual())->LL_BoneID(pSettings->r_string(*cNameSect(), "weapon_bone1")); - m_r_finger2 = smart_cast(Visual())->LL_BoneID(pSettings->r_string(*cNameSect(), "weapon_bone2")); + m_r_hand = smart_cast(Visual())->LL_BoneID(pSettings->r_string(cNameSect().c_str(), "weapon_bone0")); + m_l_finger1 = smart_cast(Visual())->LL_BoneID(pSettings->r_string(cNameSect().c_str(), "weapon_bone1")); + m_r_finger2 = smart_cast(Visual())->LL_BoneID(pSettings->r_string(cNameSect().c_str(), "weapon_bone2")); //------------------------------------------------------------------------------- m_neck = smart_cast(Visual())->LL_BoneID("bip01_neck"); m_l_clavicle = smart_cast(Visual())->LL_BoneID("bip01_l_clavicle"); @@ -1270,7 +1270,7 @@ void CActor::make_Interpolation() if (!pSyncObj) return; pSyncObj->set_State(PredictedState); //, PredictedState.enabled); - VERIFY2(_valid(renderable.xform), *cName()); + VERIFY2(_valid(renderable.xform), cName().c_str()); } else { @@ -1282,7 +1282,7 @@ void CActor::make_Interpolation() Fvector NewPos; NewPos.lerp(IStart.Pos, IEnd.Pos, factor); - VERIFY2(_valid(renderable.xform), *cName()); + VERIFY2(_valid(renderable.xform), cName().c_str()); // r_model_yaw = angle_lerp (IStart.o_model,IEnd.o_model, factor); unaffected_r_torso.yaw = angle_lerp(IStart.o_torso.yaw, IEnd.o_torso.yaw, factor); @@ -1916,8 +1916,8 @@ void CActor::OnCriticalHitHealthLoss() #ifdef DEBUG IGameObject* pLastHitter = Level().Objects.net_Find(m_iLastHitterID); - Msg("%s killed by hit from %s %s", *cName(), (pLastHitter ? *(pLastHitter->cName()) : ""), - ((pLastHittingWeapon && pLastHittingWeapon != pLastHitter) ? *(pLastHittingWeapon->cName()) : "")); + Msg("%s killed by hit from %s %s", cName().c_str(), (pLastHitter ? pLastHitter->cName().c_str() : ""), + ((pLastHittingWeapon && pLastHittingWeapon != pLastHitter) ? pLastHittingWeapon->cName().c_str() : "")); #endif //------------------------------------------------------------------- if (m_iLastHitterID != u16(-1)) @@ -2023,7 +2023,7 @@ void CActor::OnCriticalWoundHealthLoss() if (GameID() == eGameIDSingle || !OnServer()) return; #ifdef DEBUG - Msg("--- %s is bleed out", *cName()); + Msg("--- %s is bleed out", cName().c_str()); #endif // #ifdef DEBUG //------------------------------- NET_Packet P; @@ -2044,7 +2044,7 @@ void CActor::OnCriticalRadiationHealthLoss() if (GameID() == eGameIDSingle || !OnServer()) return; //------------------------------- - Msg("%s killed by radiation", *cName()); + Msg("%s killed by radiation", cName().c_str()); NET_Packet P; u_EventGen(P, GE_GAME_EVENT, ID()); P.w_u16(GAME_EVENT_PLAYER_KILLED); diff --git a/src/xrGame/ContextMenu.cpp b/src/xrGame/ContextMenu.cpp index 514bbcd0ecd..fcfa4009f91 100644 --- a/src/xrGame/ContextMenu.cpp +++ b/src/xrGame/ContextMenu.cpp @@ -22,9 +22,9 @@ void CContextMenu::Load(CInifile* INI, LPCSTR SECT) char Event[128], Param[128]; Event[0] = 0; Param[0] = 0; - sscanf(*I->second, "%[^,],%s", Event, Param); + sscanf(I->second.c_str(), "%[^,],%s", Event, Param); MenuItem Item; - Item.Name = xr_strdup(*I->first); + Item.Name = xr_strdup(I->first.c_str()); Item.Event = Engine.Event.Create(Event); Item.Param = xr_strdup(Param); Items.push_back(Item); diff --git a/src/xrGame/CustomMonster.cpp b/src/xrGame/CustomMonster.cpp index 8edb4abb4bf..b3621c08401 100644 --- a/src/xrGame/CustomMonster.cpp +++ b/src/xrGame/CustomMonster.cpp @@ -700,7 +700,7 @@ void CCustomMonster::Die(IGameObject* who) bool CCustomMonster::net_Spawn(CSE_Abstract* DC) { - memory().reload(*cNameSect()); + memory().reload(cNameSect().c_str()); memory().reinit(); if (!movement().net_Spawn(DC) || !inherited::net_Spawn(DC) || !CScriptEntity::net_Spawn(DC)) diff --git a/src/xrGame/DestroyablePhysicsObject.cpp b/src/xrGame/DestroyablePhysicsObject.cpp index f5d0a00d228..49a10a9a715 100644 --- a/src/xrGame/DestroyablePhysicsObject.cpp +++ b/src/xrGame/DestroyablePhysicsObject.cpp @@ -110,7 +110,7 @@ void CDestroyablePhysicsObject::Destroy() { m_destroy_sound.play_at_pos(this, Position()); } - if (*m_destroy_particles) + if (m_destroy_particles.c_str()) { // Fvector dir;dir.set(0,1,0); Fmatrix m; diff --git a/src/xrGame/GameObject.cpp b/src/xrGame/GameObject.cpp index 3892ee1f4f5..026802b4ff6 100644 --- a/src/xrGame/GameObject.cpp +++ b/src/xrGame/GameObject.cpp @@ -119,15 +119,15 @@ void CGameObject::cNameSect_set(shared_str N) { NameSection = N; } void CGameObject::cNameVisual_set(shared_str N) { // check if equal - if (*N && *NameVisual) + if (N.c_str() && NameVisual.c_str()) if (N == NameVisual) return; // replace model - if (*N && N[0]) + if (N.c_str() && N[0]) { IRenderVisual* old_v = renderable.visual; NameVisual = N; - renderable.visual = GEnv.Render->model_Create(*N); + renderable.visual = GEnv.Render->model_Create(N.c_str()); IKinematics* old_k = old_v ? old_v->dcast_PKinematics() : NULL; IKinematics* new_k = renderable.visual->dcast_PKinematics(); /* @@ -154,7 +154,7 @@ void CGameObject::cNameVisual_set(shared_str N) // flagging void CGameObject::processing_activate() { - VERIFY3(255 != Props.bActiveCounter, "Invalid sequence of processing enable/disable calls: overflow", *cName()); + VERIFY3(255 != Props.bActiveCounter, "Invalid sequence of processing enable/disable calls: overflow", cName().c_str()); Props.bActiveCounter++; if (!(Props.bActiveCounter - 1)) g_pGameLevel->Objects.o_activate(this); @@ -162,7 +162,7 @@ void CGameObject::processing_activate() void CGameObject::processing_deactivate() { - VERIFY3(Props.bActiveCounter, "Invalid sequence of processing enable/disable calls: underflow", *cName()); + VERIFY3(Props.bActiveCounter, "Invalid sequence of processing enable/disable calls: underflow", cName().c_str()); Props.bActiveCounter--; if (!Props.bActiveCounter) g_pGameLevel->Objects.o_sleep(this); @@ -201,19 +201,19 @@ void CGameObject::setVisible(bool _visible) void CGameObject::Center(Fvector& C) const { - VERIFY2(renderable.visual, *cName()); + VERIFY2(renderable.visual, cName().c_str()); renderable.xform.transform_tiny(C, renderable.visual->getVisData().sphere.P); } float CGameObject::Radius() const { - VERIFY2(renderable.visual, *cName()); + VERIFY2(renderable.visual, cName().c_str()); return renderable.visual->getVisData().sphere.R; } const Fbox& CGameObject::BoundingBox() const { - VERIFY2(renderable.visual, *cName()); + VERIFY2(renderable.visual, cName().c_str()); return renderable.visual->getVisData().box; } @@ -274,7 +274,7 @@ void CGameObject::net_Destroy() { #ifdef DEBUG if (psAI_Flags.test(aiDestroy)) - Msg("Destroying client object [%d][%s][%x]", ID(), *cName(), this); + Msg("Destroying client object [%d][%s][%x]", ID(), cName().c_str(), this); #endif VERIFY(m_spawned); @@ -471,7 +471,7 @@ bool CGameObject::net_Spawn(CSE_Abstract* DC) XFORM().setXYZ(E->o_Angle); Position().set(E->o_Position); #ifdef DEBUG - if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), *cName()) == 0) + if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), cName().c_str()) == 0) { Msg("CGameObject::net_Spawn obj %s Position set from CSE_Abstract %f,%f,%f", PH_DBG_ObjectTrackName(), Position().x, Position().y, Position().z); @@ -484,7 +484,7 @@ bool CGameObject::net_Spawn(CSE_Abstract* DC) { #pragma warning(push) #pragma warning(disable : 4238) - IReader reader((void*)(*(O->m_ini_string)), O->m_ini_string.size()); + IReader reader((void*)((O->m_ini_string).c_str()), O->m_ini_string.size()); m_ini_file = xr_new(&reader, FS.get_path("$game_config$")->m_Path); #pragma warning(pop) } @@ -517,15 +517,15 @@ bool CGameObject::net_Spawn(CSE_Abstract* DC) spatial.type = (spatial.type | STYPE_VISIBLEFORAI) ^ STYPE_VISIBLEFORAI; } - reload(*cNameSect()); + reload(cNameSect().c_str()); if (!GEnv.isDedicatedServer) - scriptBinder.reload(*cNameSect()); + scriptBinder.reload(cNameSect().c_str()); reinit(); if (!GEnv.isDedicatedServer) scriptBinder.reinit(); #ifdef DEBUG - if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), *cName()) == 0) + if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), cName().c_str()) == 0) { Msg("CGameObject::net_Spawn obj %s After Script Binder reinit %f,%f,%f", PH_DBG_ObjectTrackName(), Position().x, Position().y, Position().z); @@ -588,7 +588,7 @@ bool CGameObject::net_Spawn(CSE_Abstract* DC) { if (pSettings->line_exist(cNameSect(), "cform")) { - VERIFY3(*NameVisual, "Model isn't assigned for object, but cform requisted", *cName()); + VERIFY3(NameVisual.c_str(), "Model isn't assigned for object, but cform requisted", cName().c_str()); CForm = xr_new(this); } } @@ -604,7 +604,7 @@ bool CGameObject::net_Spawn(CSE_Abstract* DC) m_bObjectRemoved = false; spawn_supplies(); #ifdef DEBUG - if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), *cName()) == 0) + if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), cName().c_str()) == 0) { Msg("CGameObject::net_Spawn obj %s Before CScriptBinder::net_Spawn %f,%f,%f", PH_DBG_ObjectTrackName(), Position().x, Position().y, Position().z); @@ -615,7 +615,7 @@ bool CGameObject::net_Spawn(CSE_Abstract* DC) #endif #ifdef DEBUG - if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), *cName()) == 0) + if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), cName().c_str()) == 0) { Msg("CGameObject::net_Spawn obj %s Before CScriptBinder::net_Spawn %f,%f,%f", PH_DBG_ObjectTrackName(), Position().x, Position().y, Position().z); @@ -634,7 +634,7 @@ void CGameObject::net_Save(NET_Packet& net_packet) #ifdef DEBUG if (psAI_Flags.test(aiSerialize)) { - Msg(">> **** Save script object [%s] *****", *cName()); + Msg(">> **** Save script object [%s] *****", cName().c_str()); Msg(">> Before save :: packet position = [%u]", net_packet.w_tell()); } @@ -663,7 +663,7 @@ void CGameObject::net_Load(IReader& ireader) #ifdef DEBUG if (psAI_Flags.test(aiSerialize)) { - Msg(">> **** Load script object [%s] *****", *cName()); + Msg(">> **** Load script object [%s] *****", cName().c_str()); Msg(">> Before load :: reader position = [%i]", ireader.tell()); } @@ -680,7 +680,7 @@ void CGameObject::net_Load(IReader& ireader) #endif // ---------------------------------------------------------- #ifdef DEBUG - if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), *cName()) == 0) + if (ph_dbg_draw_mask1.test(ph_m1_DbgTrackObject) && xr_stricmp(PH_DBG_ObjectTrackName(), cName().c_str()) == 0) { Msg("CGameObject::net_Load obj %s (loaded) %f,%f,%f", PH_DBG_ObjectTrackName(), Position().x, Position().y, Position().z); @@ -1201,7 +1201,7 @@ void CGameObject::shedule_Update(u32 dt) bool CGameObject::net_SaveRelevant() { return scriptBinder.net_SaveRelevant(); } //игровое имя объекта -LPCSTR CGameObject::Name() const { return (*cName()); } +LPCSTR CGameObject::Name() const { return (cName().c_str()); } u32 CGameObject::ef_creature_type() const { string16 temp; @@ -1358,14 +1358,14 @@ void CGameObject::UpdateCL() // IGameObject::UpdateCL(); // consistency check #ifdef DEBUG - VERIFY2(_valid(renderable.xform), *cName()); + VERIFY2(_valid(renderable.xform), cName().c_str()); if (Device.dwFrame == dbg_update_cl) - xrDebug::Fatal(DEBUG_INFO, "'UpdateCL' called twice per frame for %s", *cName()); + xrDebug::Fatal(DEBUG_INFO, "'UpdateCL' called twice per frame for %s", cName().c_str()); dbg_update_cl = Device.dwFrame; if (Parent && spatial.node_ptr) - xrDebug::Fatal(DEBUG_INFO, "Object %s has parent but is still registered inside spatial DB", *cName()); + xrDebug::Fatal(DEBUG_INFO, "Object %s has parent but is still registered inside spatial DB", cName().c_str()); if (!CForm && (spatial.type & STYPE_COLLIDEABLE)) - xrDebug::Fatal(DEBUG_INFO, "Object %s registered as 'collidable' but has no collidable model", *cName()); + xrDebug::Fatal(DEBUG_INFO, "Object %s registered as 'collidable' but has no collidable model", cName().c_str()); #endif spatial_update(base_spu_epsP * 5, base_spu_epsR * 5); // crow @@ -1522,7 +1522,7 @@ bool CGameObject::use(IGameObject* obj) return true; } -LPCSTR CGameObject::tip_text() { return *m_sTipText; } +LPCSTR CGameObject::tip_text() { return m_sTipText.c_str(); } void CGameObject::set_tip_text(LPCSTR new_text) { m_sTipText = new_text; } void CGameObject::set_tip_text_default() { m_sTipText = nullptr; } bool CGameObject::nonscript_usable() { return m_bNonscriptUsable; } diff --git a/src/xrGame/GameTask.cpp b/src/xrGame/GameTask.cpp index 967fe321c9c..8441bb9f76c 100644 --- a/src/xrGame/GameTask.cpp +++ b/src/xrGame/GameTask.cpp @@ -84,7 +84,7 @@ void CGameTask::Load(const TASK_ID& id) return; } - const XML_NODE task_node = gameTaskXml.NavigateToNodeWithAttribute("game_task", "id", *id); + const XML_NODE task_node = gameTaskXml.NavigateToNodeWithAttribute("game_task", "id", id.c_str()); THROW3(task_node, "game task id = ", id.c_str()); gameTaskXml.SetLocalRoot(task_node); @@ -128,11 +128,11 @@ void CGameTask::Load(const TASK_ID& id) { objective.m_icon_texture_name = gameTaskXml.Read(gameTaskXml.GetLocalRoot(), "icon", 0, nullptr); if (objective.m_icon_texture_name.size() && - 0 != xr_stricmp(*objective.m_icon_texture_name, "ui\\ui_icons_task")) + 0 != xr_stricmp(objective.m_icon_texture_name.c_str(), "ui\\ui_icons_task")) { - objective.m_icon_rect = CUITextureMaster::GetTextureRect(*objective.m_icon_texture_name); + objective.m_icon_rect = CUITextureMaster::GetTextureRect(objective.m_icon_texture_name.c_str()); objective.m_icon_rect.rb.sub(objective.m_icon_rect.rb, objective.m_icon_rect.lt); - objective.m_icon_texture_name = CUITextureMaster::GetTextureFileName(*objective.m_icon_texture_name); + objective.m_icon_texture_name = CUITextureMaster::GetTextureFileName(objective.m_icon_texture_name.c_str()); } else if (objective.m_icon_texture_name.size()) { @@ -673,9 +673,9 @@ void SScriptTaskHelper::init_functors(xr_vector& v_src, task_state_f for (u32 idx = 0; it != it_e; ++it, ++idx) { - const bool functor_exists = GEnv.ScriptEngine->functor(*(*it), v_dest[idx]); + const bool functor_exists = GEnv.ScriptEngine->functor(it->c_str(), v_dest[idx]); if (!functor_exists) - Log("Cannot find script function described in task objective ", *(*it)); + Log("Cannot find script function described in task objective ", it->c_str()); } } diff --git a/src/xrGame/GametaskManager.cpp b/src/xrGame/GametaskManager.cpp index db04ae4a1da..2bce9e42e19 100644 --- a/src/xrGame/GametaskManager.cpp +++ b/src/xrGame/GametaskManager.cpp @@ -176,7 +176,7 @@ void CGameTaskManager::SetTaskState(const TASK_ID& id, ETaskState state, TASK_OB CGameTask* t = HasGameTask(id, objectiveSpecified); if (NULL == t) { - Msg("! actor does not has task [%s]%s", *id, objectiveSpecified ? "" : " or it is completed"); + Msg("! actor does not has task [%s]%s", id.c_str(), objectiveSpecified ? "" : " or it is completed"); return; } SetTaskState(t, state, objective_id); diff --git a/src/xrGame/HangingLamp.cpp b/src/xrGame/HangingLamp.cpp index fa0102810fa..72b2c5091d2 100644 --- a/src/xrGame/HangingLamp.cpp +++ b/src/xrGame/HangingLamp.cpp @@ -83,9 +83,9 @@ bool CHangingLamp::net_Spawn(CSE_Abstract* DC) { IKinematics* K = smart_cast(Visual()); R_ASSERT(Visual() && smart_cast(Visual())); - light_bone = K->LL_BoneID(*lamp->light_main_bone); + light_bone = K->LL_BoneID(lamp->light_main_bone.c_str()); VERIFY(light_bone != BI_NONE); - ambient_bone = K->LL_BoneID(*lamp->light_ambient_bone); + ambient_bone = K->LL_BoneID(lamp->light_ambient_bone.c_str()); VERIFY(ambient_bone != BI_NONE); CForm = xr_new(this); } @@ -103,7 +103,7 @@ bool CHangingLamp::net_Spawn(CSE_Abstract* DC) light_render->set_virtual_size(lamp->m_virtual_size); light_render->set_color(clr); light_render->set_cone(lamp->spot_cone_angle); - light_render->set_texture(*lamp->light_texture); + light_render->set_texture(lamp->light_texture.c_str()); light_render->set_volumetric_quality(lamp->m_volumetric_quality); light_render->set_volumetric_intensity(lamp->m_volumetric_intensity); @@ -112,7 +112,7 @@ bool CHangingLamp::net_Spawn(CSE_Abstract* DC) if (lamp->glow_texture.size()) { glow_render = GEnv.Render->glow_create(); - glow_render->set_texture(*lamp->glow_texture); + glow_render->set_texture(lamp->glow_texture.c_str()); glow_render->set_color(clr); glow_render->set_radius(lamp->glow_radius); } @@ -126,12 +126,12 @@ bool CHangingLamp::net_Spawn(CSE_Abstract* DC) clr.mul_rgb(ambient_power); light_ambient->set_range(lamp->m_ambient_radius); light_ambient->set_color(clr); - light_ambient->set_texture(*lamp->m_ambient_texture); + light_ambient->set_texture(lamp->m_ambient_texture.c_str()); } fHealth = lamp->m_health; - lanim = LALib.FindItem(*lamp->color_animator); + lanim = LALib.FindItem(lamp->color_animator.c_str()); CPHSkeleton::Spawn(e); if (smart_cast(Visual())) @@ -143,7 +143,7 @@ bool CHangingLamp::net_Spawn(CSE_Abstract* DC) //.intepolate_pos } if (lamp->flags.is(CSE_ALifeObjectHangingLamp::flPhysic) && !Visual()) - Msg("! WARNING: lamp, obj name [%s],flag physics set, but has no visual", *cName()); + Msg("! WARNING: lamp, obj name [%s],flag physics set, but has no visual", cName().c_str()); //. if (lamp->flags.is(CSE_ALifeObjectHangingLamp::flPhysic)&&Visual()&&!guid_physic_bone) fHealth=0.f; if (Alive() && m_bState) TurnOn(); @@ -358,7 +358,7 @@ void CHangingLamp::CreateBody(CSE_ALifeObjectHangingLamp* lamp) m_pPhysicsShell = P_create_Shell(); bone_map.clear(); - LPCSTR fixed_bones = *lamp->fixed_bones; + LPCSTR fixed_bones = lamp->fixed_bones.c_str(); if (fixed_bones) { int count = _GetItemCount(fixed_bones); diff --git a/src/xrGame/Helicopter.cpp b/src/xrGame/Helicopter.cpp index b4d52c0583e..bb4153c7da5 100644 --- a/src/xrGame/Helicopter.cpp +++ b/src/xrGame/Helicopter.cpp @@ -94,7 +94,7 @@ void CHelicopter::Load(LPCSTR section) UseFireTrail(m_enemy.bUseFireTrail); // temp force reloar disp params m_sAmmoType = pSettings->r_string(section, "ammo_class"); - m_CurrentAmmo.Load(*m_sAmmoType, 0); + m_CurrentAmmo.Load(m_sAmmoType.c_str(), 0); m_sRocketSection = pSettings->r_string(section, "rocket_class"); @@ -143,7 +143,7 @@ bool CHelicopter::net_Spawn(CSE_Abstract* DC) CPHSkeleton::Spawn((CSE_Abstract*)(DC)); for (u32 i = 0; i < 4; ++i) - CRocketLauncher::SpawnRocket(*m_sRocketSection, smart_cast(this)); + CRocketLauncher::SpawnRocket(m_sRocketSection.c_str(), smart_cast(this)); // assigning m_animator here CSE_Abstract* abstract = (CSE_Abstract*)(DC); @@ -206,11 +206,11 @@ bool CHelicopter::net_Spawn(CSE_Abstract* DC) IKinematicsAnimated* A = smart_cast(Visual()); if (A) { - A->PlayCycle(*heli->startup_animation); + A->PlayCycle(heli->startup_animation.c_str()); K->CalculateBones(TRUE); } - m_engineSound.create(*heli->engine_sound, st_Effect, sg_SourceType); + m_engineSound.create(heli->engine_sound.c_str(), st_Effect, sg_SourceType); m_engineSound.play_at_pos(0, XFORM().c, sm_Looped); if (m_bLightShotEnabled) @@ -440,7 +440,7 @@ void CHelicopter::shedule_Update(u32 time_delta) if (state() != CHelicopter::eDead) { for (u32 i = getRocketCount(); i < 4; ++i) - CRocketLauncher::SpawnRocket(*m_sRocketSection, this); + CRocketLauncher::SpawnRocket(m_sRocketSection.c_str(), this); } if (m_ready_explode) ExplodeHelicopter(); diff --git a/src/xrGame/Helicopter2.cpp b/src/xrGame/Helicopter2.cpp index ca39bc59377..cf28e97801e 100644 --- a/src/xrGame/Helicopter2.cpp +++ b/src/xrGame/Helicopter2.cpp @@ -46,7 +46,7 @@ void CHelicopter::StartFlame() { if (m_pParticle) return; - m_pParticle = CParticlesObject::Create(*m_smoke_particle, FALSE); + m_pParticle = CParticlesObject::Create(m_smoke_particle.c_str(), FALSE); Fvector zero_vector; zero_vector.set(0.f, 0.f, 0.f); @@ -250,7 +250,7 @@ void CHelicopter::DieHelicopter() m_engineSound.stop(); - m_brokenSound.create(pSettings->r_string(*cNameSect(), "broken_snd"), st_Effect, sg_SourceType); + m_brokenSound.create(pSettings->r_string(cNameSect().c_str(), "broken_snd"), st_Effect, sg_SourceType); m_brokenSound.play_at_pos(0, XFORM().c, sm_Looped); IKinematics* K = smart_cast(Visual()); @@ -260,9 +260,9 @@ void CHelicopter::DieHelicopter() LPCSTR bone; u16 bone_id; - for (u32 i = 0, n = _GetItemCount(*m_death_bones_to_hide); i < n; ++i) + for (u32 i = 0, n = _GetItemCount(m_death_bones_to_hide.c_str()); i < n; ++i) { - bone = _GetItem(*m_death_bones_to_hide, i, I); + bone = _GetItem(m_death_bones_to_hide.c_str(), i, I); bone_id = K->LL_BoneID(bone); K->LL_SetBoneVisible(bone_id, FALSE, TRUE); } @@ -352,12 +352,12 @@ void CHelicopter::UseFireTrail(bool val) m_enemy.bUseFireTrail = val; if (val) { - fireDispersionBase = pSettings->r_float(*cNameSect(), "fire_dispersion_null"); + fireDispersionBase = pSettings->r_float(cNameSect().c_str(), "fire_dispersion_null"); fireDispersionBase = deg2rad(fireDispersionBase); } else { - fireDispersionBase = pSettings->r_float(*cNameSect(), "fire_dispersion_base"); + fireDispersionBase = pSettings->r_float(cNameSect().c_str(), "fire_dispersion_base"); fireDispersionBase = deg2rad(fireDispersionBase); } } diff --git a/src/xrGame/InfoPortion.cpp b/src/xrGame/InfoPortion.cpp index 17da1414e36..820fd35ca49 100644 --- a/src/xrGame/InfoPortion.cpp +++ b/src/xrGame/InfoPortion.cpp @@ -41,7 +41,7 @@ void CInfoPortion::load_shared(LPCSTR) //loading from XML const XML_NODE pNode = pXML->NavigateToNode(tag_name, item_data->pos_in_file); - THROW3(pNode, "info_portion id=", *item_data->id); + THROW3(pNode, "info_portion id=", item_data->id.c_str()); //список названий диалогов const int dialogs_num = pXML->GetNodesNum(pNode, "dialog"); diff --git a/src/xrGame/Inventory.cpp b/src/xrGame/Inventory.cpp index ff39a1707e8..033a4119455 100644 --- a/src/xrGame/Inventory.cpp +++ b/src/xrGame/Inventory.cpp @@ -150,7 +150,7 @@ void CInventory::Take(CGameObject* pObj, bool bNotActivate, bool strict_placemen pIItem->m_ItemCurrPlace.type = eItemPlaceUndefined; #ifdef DEBUG if (!result) - Msg("cant put in belt item %s", *pIItem->object().cName()); + Msg("cant put in belt item %s", pIItem->object().cName().c_str()); #endif break; @@ -160,7 +160,7 @@ void CInventory::Take(CGameObject* pObj, bool bNotActivate, bool strict_placemen pIItem->m_ItemCurrPlace.type = eItemPlaceUndefined; #ifdef DEBUG if (!result) - Msg("cant put in ruck item %s", *pIItem->object().cName()); + Msg("cant put in ruck item %s", pIItem->object().cName().c_str()); #endif break; @@ -170,7 +170,7 @@ void CInventory::Take(CGameObject* pObj, bool bNotActivate, bool strict_placemen pIItem->m_ItemCurrPlace.type = eItemPlaceUndefined; #ifdef DEBUG if (!result) - Msg("cant slot in slot item %s", *pIItem->object().cName()); + Msg("cant slot in slot item %s", pIItem->object().cName().c_str()); #endif break; } @@ -1268,7 +1268,7 @@ bool CInventory::CanTakeItem(CInventoryItem* inventory_item) const for (it = m_all.begin(); it != m_all.end(); ++it) if ((*it)->object().ID() == inventory_item->object().ID()) break; - VERIFY3(it == m_all.end(), "item already exists in inventory", *inventory_item->object().cName()); + VERIFY3(it == m_all.end(), "item already exists in inventory", inventory_item->object().cName().c_str()); CActor* pActor = smart_cast(m_pOwner); //актер всегда может взять вещь diff --git a/src/xrGame/InventoryOwner.cpp b/src/xrGame/InventoryOwner.cpp index 4694577a72e..b080fd3cad4 100644 --- a/src/xrGame/InventoryOwner.cpp +++ b/src/xrGame/InventoryOwner.cpp @@ -138,8 +138,8 @@ bool CInventoryOwner::net_Spawn(CSE_Abstract* DC) CharacterInfo().m_SpecificCharacter.Load("mp_actor"); CharacterInfo().InitSpecificCharacter("mp_actor"); CharacterInfo().m_SpecificCharacter.data()->m_sGameName = - (E->name_replace()[0]) ? E->name_replace() : *pThis->cName(); - m_game_name = (E->name_replace()[0]) ? E->name_replace() : *pThis->cName(); + (E->name_replace()[0]) ? E->name_replace() : pThis->cName().c_str(); + m_game_name = (E->name_replace()[0]) ? E->name_replace() : pThis->cName().c_str(); } if (!pThis->Local()) diff --git a/src/xrGame/Level.cpp b/src/xrGame/Level.cpp index 08857070f7d..fef6a22ec36 100644 --- a/src/xrGame/Level.cpp +++ b/src/xrGame/Level.cpp @@ -390,7 +390,7 @@ void CLevel::MakeReconnect() char const* client_options = nullptr; if (m_caServerOptions.c_str()) { - server_options = xr_strdup(*m_caServerOptions); + server_options = xr_strdup(m_caServerOptions.c_str()); } else { @@ -398,7 +398,7 @@ void CLevel::MakeReconnect() } if (m_caClientOptions.c_str()) { - client_options = xr_strdup(*m_caClientOptions); + client_options = xr_strdup(m_caClientOptions.c_str()); } else { diff --git a/src/xrGame/Level_network.cpp b/src/xrGame/Level_network.cpp index c6f8664726f..500ae3def86 100644 --- a/src/xrGame/Level_network.cpp +++ b/src/xrGame/Level_network.cpp @@ -252,7 +252,7 @@ u32 CLevel::Objects_net_Save(NET_Packet* _Packet, u32 start, u32 max_object_size if (size >= 65536) { xrDebug::Fatal(DEBUG_INFO, "Object [%s][%d] exceed network-data limit\n size=%d, Pend=%d, Pstart=%d", - *P->cName(), P->ID(), size, Packet.w_tell(), position); + P->cName().c_str(), P->ID(), size, Packet.w_tell(), position); } #endif Packet.w_chunk_close16(position); @@ -545,7 +545,7 @@ void CLevel::ClearAllObjects() ParentFound = true; //------------------------------------------------------------- #ifdef DEBUG - Msg("Rejection of %s[%d] from %s[%d]", *(pObj->cNameSect()), pObj->ID(), *(pObj->H_Parent()->cNameSect()), + Msg("Rejection of %s[%d] from %s[%d]", pObj->cNameSect().c_str(), pObj->ID(), pObj->H_Parent()->cNameSect().c_str(), pObj->H_Parent()->ID()); #endif }; @@ -583,7 +583,7 @@ void CLevel::ClearAllObjects() ParentFound = true; //------------------------------------------------------------- #ifdef DEBUG - Msg("Destruction of %s[%d]", *(pObj->cNameSect()), pObj->ID()); + Msg("Destruction of %s[%d]", pObj->cNameSect().c_str(), pObj->ID()); #endif }; ProcessGameEvents(); @@ -623,10 +623,10 @@ void CLevel::net_OnChangeSelfName(NET_Packet* P) return; string64 NewName; P->r_stringZ(NewName); - if (!strstr(*m_caClientOptions, "/name=")) + if (!strstr(m_caClientOptions.c_str(), "/name=")) { string1024 tmpstr; - xr_strcpy(tmpstr, *m_caClientOptions); + xr_strcpy(tmpstr, m_caClientOptions.c_str()); xr_strcat(tmpstr, "/name="); xr_strcat(tmpstr, NewName); m_caClientOptions = tmpstr; @@ -634,10 +634,10 @@ void CLevel::net_OnChangeSelfName(NET_Packet* P) else { string1024 tmpstr; - xr_strcpy(tmpstr, *m_caClientOptions); + xr_strcpy(tmpstr, m_caClientOptions.c_str()); *(strstr(tmpstr, "name=") + 5) = 0; xr_strcat(tmpstr, NewName); - pcstr ptmp = strchr(strstr(*m_caClientOptions, "name="), '/'); + pcstr ptmp = strchr(strstr(m_caClientOptions.c_str(), "name="), '/'); if (ptmp) xr_strcat(tmpstr, ptmp); m_caClientOptions = tmpstr; diff --git a/src/xrGame/Level_network_spawn.cpp b/src/xrGame/Level_network_spawn.cpp index c6ebc36599e..9627dd57d2d 100644 --- a/src/xrGame/Level_network_spawn.cpp +++ b/src/xrGame/Level_network_spawn.cpp @@ -18,8 +18,8 @@ void CLevel::cl_Process_Spawn(NET_Packet& P) P.r_stringZ(s_name); // Create DC (xrSE) - CSE_Abstract* E = F_entity_Create(*s_name); - R_ASSERT2(E, *s_name); + CSE_Abstract* E = F_entity_Create(s_name.c_str()); + R_ASSERT2(E, s_name.c_str()); E->Spawn_Read(P); if (E->s_flags.is(M_SPAWN_UPDATE)) @@ -102,7 +102,7 @@ void CLevel::g_sv_Spawn(CSE_Abstract* E) // Client spawn // T.Start (); - IGameObject* O = Objects.Create(*E->s_name); + IGameObject* O = Objects.Create(E->s_name.c_str()); // Msg ("--spawn--CREATE: %f ms",1000.f*T.GetAsync()); // T.Start (); @@ -112,7 +112,7 @@ void CLevel::g_sv_Spawn(CSE_Abstract* E) if (!GEnv.isDedicatedServer) client_spawn_manager().clear(O->ID()); Objects.Destroy(O); - Msg("! Failed to spawn entity '%s'", *E->s_name); + Msg("! Failed to spawn entity '%s'", E->s_name.c_str()); } else { diff --git a/src/xrGame/PhraseDialog.cpp b/src/xrGame/PhraseDialog.cpp index c697c0e301c..4606c701446 100644 --- a/src/xrGame/PhraseDialog.cpp +++ b/src/xrGame/PhraseDialog.cpp @@ -92,7 +92,7 @@ bool CPhraseDialog::SayPhrase(DIALOG_SHARED_PTR& phrase_dialog, const shared_str //вызвать скриптовую присоединенную функцию //активируется после сказанной фразы //первый параметр - тот кто говорит фразу, второй - тот кто слушает - last_phrase->GetScriptHelper()->Action(pSpeakerGO1, pSpeakerGO2, *phrase_dialog->m_DialogId, phrase_id.c_str()); + last_phrase->GetScriptHelper()->Action(pSpeakerGO1, pSpeakerGO2, phrase_dialog->m_DialogId.c_str(), phrase_id.c_str()); //больше нет фраз, чтоб говорить phrase_dialog->m_PhraseVector.clear(); @@ -111,7 +111,7 @@ bool CPhraseDialog::SayPhrase(DIALOG_SHARED_PTR& phrase_dialog, const shared_str THROW(next_phrase_vertex); shared_str next_phrase_id = next_phrase_vertex->vertex_id(); if (next_phrase_vertex->data()->GetScriptHelper()->Precondition( - pSpeakerGO2, pSpeakerGO1, *phrase_dialog->m_DialogId, phrase_id.c_str(), next_phrase_id.c_str())) + pSpeakerGO2, pSpeakerGO1, phrase_dialog->m_DialogId.c_str(), phrase_id.c_str(), next_phrase_id.c_str())) { phrase_dialog->m_PhraseVector.push_back(next_phrase_vertex->data()); #ifdef DEBUG @@ -127,7 +127,7 @@ bool CPhraseDialog::SayPhrase(DIALOG_SHARED_PTR& phrase_dialog, const shared_str } R_ASSERT2(!phrase_dialog->m_PhraseVector.empty(), - make_string("No available phrase to say, dialog[%s]", *phrase_dialog->m_DialogId)); + make_string("No available phrase to say, dialog[%s]", phrase_dialog->m_DialogId.c_str())); //упорядочить списко по убыванию благосклонности std::sort(phrase_dialog->m_PhraseVector.begin(), phrase_dialog->m_PhraseVector.end(), PhraseGoodwillPred); @@ -184,7 +184,7 @@ LPCSTR CPhraseDialog::GetPhraseText(const shared_str& phrase_id, bool current_sp ph->GetText(), pSpeakerGO1, pSpeakerGO2, m_DialogId.c_str(), phrase_id.c_str()); } -LPCSTR CPhraseDialog::DialogCaption() { return data()->m_sCaption.size() ? *data()->m_sCaption : GetPhraseText("0"); } +LPCSTR CPhraseDialog::DialogCaption() { return data()->m_sCaption.size() ? data()->m_sCaption.c_str() : GetPhraseText("0"); } int CPhraseDialog::Priority() { return data()->m_iPriority; } void CPhraseDialog::Load(shared_str dialog_id) { @@ -204,7 +204,7 @@ void CPhraseDialog::load_shared(LPCSTR) // loading from XML XML_NODE dialog_node = pXML->NavigateToNode(id_to_index::tag_name, item_data.pos_in_file); - THROW3(dialog_node, "dialog id=", *item_data.id); + THROW3(dialog_node, "dialog id=", item_data.id.c_str()); pXML->SetLocalRoot(dialog_node); @@ -232,13 +232,13 @@ void CPhraseDialog::load_shared(LPCSTR) } [[maybe_unused]] int phrase_num = pXML->GetNodesNum(phrase_list_node, "phrase"); - THROW3(phrase_num, "dialog %s has no phrases at all", *item_data.id); + THROW3(phrase_num, "dialog %s has no phrases at all", item_data.id.c_str()); pXML->SetLocalRoot(phrase_list_node); #ifdef DEBUG // debug & mixed LPCSTR wrong_phrase_id = pXML->CheckUniqueAttrib(phrase_list_node, "phrase", "id"); - THROW3(wrong_phrase_id == NULL, *item_data.id, wrong_phrase_id); + THROW3(wrong_phrase_id == NULL, item_data.id.c_str(), wrong_phrase_id); #endif //ищем стартовую фразу diff --git a/src/xrGame/PhraseScript.cpp b/src/xrGame/PhraseScript.cpp index db2da6be5e1..8d7480a1ded 100644 --- a/src/xrGame/PhraseScript.cpp +++ b/src/xrGame/PhraseScript.cpp @@ -45,7 +45,7 @@ bool CDialogScriptHelper::CheckInfo(const CInventoryOwner* pOwner) const { #ifdef DEBUG if (psAI_Flags.test(aiDialogs)) - Msg("----rejected: [%s] has info %s", pOwner->Name(), *m_HasInfo[i]); + Msg("----rejected: [%s] has info %s", pOwner->Name(), m_HasInfo[i].c_str()); #endif return false; } @@ -57,7 +57,7 @@ bool CDialogScriptHelper::CheckInfo(const CInventoryOwner* pOwner) const { #ifdef DEBUG if (psAI_Flags.test(aiDialogs)) - Msg("----rejected: [%s] dont has info %s", pOwner->Name(), *m_DontHasInfo[i]); + Msg("----rejected: [%s] dont has info %s", pOwner->Name(), m_DontHasInfo[i].c_str()); #endif return false; } @@ -107,9 +107,9 @@ bool CDialogScriptHelper::Precondition(const CGameObject* pSpeakerGO, LPCSTR dia for (u32 i = 0; i < Preconditions().size(); ++i) { luabind::functor lua_function; - THROW(*Preconditions()[i]); - [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(*Preconditions()[i], lua_function); - THROW3(functor_exists, "Cannot find precondition", *Preconditions()[i]); + THROW(Preconditions()[i].c_str()); + [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(Preconditions()[i].c_str(), lua_function); + THROW3(functor_exists, "Cannot find precondition", Preconditions()[i].c_str()); predicate_result = lua_function(pSpeakerGO->lua_game_object()); if (!predicate_result) { @@ -128,9 +128,9 @@ void CDialogScriptHelper::Action(const CGameObject* pSpeakerGO, LPCSTR dialog_id for (u32 i = 0; i < Actions().size(); ++i) { luabind::functor lua_function; - THROW(*Actions()[i]); - [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(*Actions()[i], lua_function); - THROW3(functor_exists, "Cannot find phrase dialog script function", *Actions()[i]); + THROW(Actions()[i].c_str()); + [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(Actions()[i].c_str(), lua_function); + THROW3(functor_exists, "Cannot find phrase dialog script function", Actions()[i].c_str()); lua_function(pSpeakerGO->lua_game_object(), dialog_id); } TransferInfo(smart_cast(pSpeakerGO)); @@ -152,9 +152,9 @@ bool CDialogScriptHelper::Precondition(const CGameObject* pSpeakerGO1, const CGa for (u32 i = 0; i < Preconditions().size(); ++i) { luabind::functor lua_function; - THROW(*Preconditions()[i]); - [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(*Preconditions()[i], lua_function); - THROW3(functor_exists, "Cannot find phrase precondition", *Preconditions()[i]); + THROW(Preconditions()[i].c_str()); + [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(Preconditions()[i].c_str(), lua_function); + THROW3(functor_exists, "Cannot find phrase precondition", Preconditions()[i].c_str()); predicate_result = lua_function( pSpeakerGO1->lua_game_object(), pSpeakerGO2->lua_game_object(), dialog_id, phrase_id, next_phrase_id); if (!predicate_result) @@ -177,9 +177,9 @@ void CDialogScriptHelper::Action( for (u32 i = 0; i < Actions().size(); ++i) { luabind::functor lua_function; - THROW(*Actions()[i]); - [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(*Actions()[i], lua_function); - THROW3(functor_exists, "Cannot find phrase dialog script function", *Actions()[i]); + THROW(Actions()[i].c_str()); + [[maybe_unused]] bool functor_exists = GEnv.ScriptEngine->functor(Actions()[i].c_str(), lua_function); + THROW3(functor_exists, "Cannot find phrase dialog script function", Actions()[i].c_str()); try { lua_function(pSpeakerGO1->lua_game_object(), pSpeakerGO2->lua_game_object(), dialog_id, phrase_id); diff --git a/src/xrGame/PhysicObject.cpp b/src/xrGame/PhysicObject.cpp index d7f8e08e27e..94fa6a0704c 100644 --- a/src/xrGame/PhysicObject.cpp +++ b/src/xrGame/PhysicObject.cpp @@ -194,7 +194,7 @@ void CPhysicObject::RunStartupAnim(CSE_Abstract* D) { CSE_Visual* visual = smart_cast(D); R_ASSERT(visual); - R_ASSERT2(*visual->startup_animation, "no startup animation"); + R_ASSERT2(visual->startup_animation.c_str(), "no startup animation"); VERIFY2((!!PKinematicsAnimated->LL_MotionID(visual->startup_animation.c_str()).valid()), (make_string(" animation %s not faund ", visual->startup_animation.c_str()) + @@ -296,7 +296,7 @@ void CPhysicObject::CreateSkeleton(CSE_ALifeObjectPhysic* po) return; if (!Visual()) return; - LPCSTR fixed_bones = *po->fixed_bones; + LPCSTR fixed_bones = po->fixed_bones.c_str(); m_pPhysicsShell = P_build_Shell(this, !po->_flags.test(CSE_PHSkeleton::flActive), fixed_bones); ApplySpawnIniToPhysicShell(&po->spawn_ini(), m_pPhysicsShell, fixed_bones[0] != '\0'); ApplySpawnIniToPhysicShell( diff --git a/src/xrGame/WeaponMagazined.cpp b/src/xrGame/WeaponMagazined.cpp index 61cfcf36730..19c01ca8839 100644 --- a/src/xrGame/WeaponMagazined.cpp +++ b/src/xrGame/WeaponMagazined.cpp @@ -285,7 +285,7 @@ void CWeaponMagazined::UnloadMagazine(bool spawn_ammo) xr_map::iterator l_it; for (l_it = l_ammo.begin(); l_ammo.end() != l_it; ++l_it) { - if (!xr_strcmp(*l_cartridge.m_ammoSect, l_it->first)) + if (!xr_strcmp(l_cartridge.m_ammoSect.c_str(), l_it->first)) { ++(l_it->second); break; @@ -293,7 +293,7 @@ void CWeaponMagazined::UnloadMagazine(bool spawn_ammo) } if (l_it == l_ammo.end()) - l_ammo[*l_cartridge.m_ammoSect] = 1; + l_ammo[l_cartridge.m_ammoSect.c_str()] = 1; m_magazine.pop_back(); --iAmmoElapsed; } @@ -383,7 +383,7 @@ void CWeaponMagazined::ReloadMagazine() //разрядить магазин, если загружаем патронами другого типа if (!m_bLockType && !m_magazine.empty() && - (!m_pCurrentAmmo || xr_strcmp(m_pCurrentAmmo->cNameSect(), *m_magazine.back().m_ammoSect))) + (!m_pCurrentAmmo || xr_strcmp(m_pCurrentAmmo->cNameSect(), m_magazine.back().m_ammoSect.c_str()))) UnloadMagazine(); VERIFY((u32)iAmmoElapsed == m_magazine.size()); @@ -700,8 +700,8 @@ void CWeaponMagazined::switch2_Fire() #ifdef DEBUG if (ii != io->inventory().ActiveItem()) - Msg("! not an active item, item %s, owner %s, active item %s", *cName(), *H_Parent()->cName(), - io->inventory().ActiveItem() ? *io->inventory().ActiveItem()->object().cName() : "no_active_item"); + Msg("! not an active item, item %s, owner %s, active item %s", cName().c_str(), H_Parent()->cName().c_str(), + io->inventory().ActiveItem() ? io->inventory().ActiveItem()->object().cName().c_str() : "no_active_item"); if (!(io && (ii == io->inventory().ActiveItem()))) { @@ -1076,7 +1076,7 @@ void CWeaponMagazined::InitAddons() m_sSndShotCurrent = "sndSilencerShot"; //подсветка от выстрела - LoadLights(*cNameSect(), "silencer_"); + LoadLights(cNameSect().c_str(), "silencer_"); } else { @@ -1085,7 +1085,7 @@ void CWeaponMagazined::InitAddons() m_sSndShotCurrent = "sndShot"; //подсветка от выстрела - LoadLights(*cNameSect(), ""); + LoadLights(cNameSect().c_str(), ""); } if (silencer) diff --git a/src/xrGame/actor_communication.cpp b/src/xrGame/actor_communication.cpp index 7f4e403afe1..f1ad8e41ae7 100644 --- a/src/xrGame/actor_communication.cpp +++ b/src/xrGame/actor_communication.cpp @@ -61,8 +61,8 @@ void CActor::AddEncyclopediaArticle(const CInfoPortion* info_portion) const article_vector.emplace_back(*it, Level().GetGameTime(), article.data()->articleType); LPCSTR g, n; int _atype = article.data()->articleType; - g = *(article.data()->group); - n = *(article.data()->name); + g = (article.data()->group).c_str(); + n = (article.data()->name).c_str(); callback(GameObject::eArticleInfo)(lua_game_object(), g, n, _atype); /* XXX: Shadow of Chernobyl encyclopedia, return this code @@ -144,7 +144,7 @@ bool CActor::OnReceiveInfo(shared_str info_id) const AddEncyclopediaArticle(&info_portion); AddGameTask(&info_portion); - callback(GameObject::eInventoryInfo)(lua_game_object(), *info_id); + callback(GameObject::eInventoryInfo)(lua_game_object(), info_id.c_str()); if (!CurrentGameUI()) return false; diff --git a/src/xrGame/agent_enemy_manager.cpp b/src/xrGame/agent_enemy_manager.cpp index d42d43f9e1c..18bf90a7320 100644 --- a/src/xrGame/agent_enemy_manager.cpp +++ b/src/xrGame/agent_enemy_manager.cpp @@ -531,7 +531,7 @@ void CAgentEnemyManager::assign_wounded() iterator I = m_enemies.begin(); iterator E = m_enemies.end(); for (; I != E; ++I) - Msg(" [%s][0x%08x][0x%08x][%.2f]", *(*I).m_object->cName(), (*I).m_mask.get(), + Msg(" [%s][0x%08x][0x%08x][%.2f]", (*I).m_object->cName().c_str(), (*I).m_mask.get(), (*I).m_distribute_mask.get(), (*I).m_probability); } Msg("combat members(%d):", object().member().combat_members().size()); @@ -540,7 +540,7 @@ void CAgentEnemyManager::assign_wounded() const_iterator I = object().member().combat_members().begin(); const_iterator E = object().member().combat_members().end(); for (; I != E; ++I) - Msg(" [%s][0x%08x][0x%08x]", *(*I)->object().cName(), object().member().mask(&(*I)->object()), + Msg(" [%s][0x%08x][0x%08x]", (*I)->object().cName().c_str(), object().member().mask(&(*I)->object()), (*I)->selected_enemy()); } } diff --git a/src/xrGame/ai/monsters/basemonster/base_monster_debug.cpp b/src/xrGame/ai/monsters/basemonster/base_monster_debug.cpp index 19f50ab4d4a..3460cf21b18 100644 --- a/src/xrGame/ai/monsters/basemonster/base_monster_debug.cpp +++ b/src/xrGame/ai/monsters/basemonster/base_monster_debug.cpp @@ -56,7 +56,7 @@ CBaseMonster::SDebugInfo CBaseMonster::show_debug_info() DBG().text(this).clear(); DBG().text(this).add_item("---------------------------------------", x, y += delta_y, delimiter_color); - xr_sprintf(text, "-- Monster : [%s] Current Time = [%u]", *cName(), Device.dwTimeGlobal); + xr_sprintf(text, "-- Monster : [%s] Current Time = [%u]", cName().c_str(), Device.dwTimeGlobal); DBG().text(this).add_item(text, x, y += delta_y, color); DBG().text(this).add_item("----------- PROPERTIES ------------", x, y += delta_y, delimiter_color); @@ -70,7 +70,7 @@ CBaseMonster::SDebugInfo CBaseMonster::show_debug_info() if (EnemyMan.get_enemy()) { - xr_sprintf(text, "Current Enemy = [%s]", *EnemyMan.get_enemy()->cName()); + xr_sprintf(text, "Current Enemy = [%s]", EnemyMan.get_enemy()->cName().c_str()); } else xr_sprintf(text, "Current Enemy = [NONE]"); @@ -85,7 +85,7 @@ CBaseMonster::SDebugInfo CBaseMonster::show_debug_info() if (CorpseMan.get_corpse()) { - xr_sprintf(text, "Current Corpse = [%s] Satiety = [%.2f]", *CorpseMan.get_corpse()->cName(), GetSatiety()); + xr_sprintf(text, "Current Corpse = [%s] Satiety = [%.2f]", CorpseMan.get_corpse()->cName().c_str(), GetSatiety()); } else xr_sprintf(text, "Current Corpse = [NONE] Satiety = [%.2f]", GetSatiety()); @@ -144,7 +144,7 @@ CBaseMonster::SDebugInfo CBaseMonster::show_debug_info() { if (HitMemory.get_last_hit_object()) { - xr_sprintf(text, "Hit Info: object=[%s] time=[%u]", *(HitMemory.get_last_hit_object()->cName()), + xr_sprintf(text, "Hit Info: object=[%s] time=[%u]", HitMemory.get_last_hit_object()->cName().c_str(), HitMemory.get_last_hit_time()); } else @@ -173,7 +173,7 @@ CBaseMonster::SDebugInfo CBaseMonster::show_debug_info() if (EnemyMan.get_enemy()) { - xr_sprintf(text, "Current Enemy = [%s]", *EnemyMan.get_enemy()->cName()); + xr_sprintf(text, "Current Enemy = [%s]", EnemyMan.get_enemy()->cName().c_str()); } else xr_sprintf(text, "Current Enemy = [NONE]"); @@ -303,7 +303,7 @@ void CBaseMonster::debug_fsm() DBG().object_info(this, this).remove_item(u32(1)); DBG().object_info(this, this).remove_item(u32(2)); - DBG().object_info(this, this).add_item(*cName(), color_xrgb(255, 0, 0), 0); + DBG().object_info(this, this).add_item(cName().c_str(), color_xrgb(255, 0, 0), 0); DBG().object_info(this, this).add_item(st, color_xrgb(255, 0, 0), 1); xr_sprintf(st, "Team[%u]Squad[%u]Group[%u]", g_Team(), g_Squad(), g_Group()); @@ -392,7 +392,7 @@ void add_debug_info(debug::text_tree& root_s, const CEntity* pE) { if (pE) { - root_s.add_line(*pE->cName()); + root_s.add_line(pE->cName().c_str()); root_s.add_line("ID", pE->ID()); } else @@ -537,7 +537,7 @@ void CBaseMonster::add_debug_info(debug::text_tree& root_s) ::detail::add_debug_info(general_s, this); TextTree& current_visual_s = general_s.add_line("Current_Visual"); - current_visual_s.add_line(*cNameVisual()); + current_visual_s.add_line(cNameVisual().c_str()); general_s.add_line("Health", conditions().GetHealth()); general_s.add_line("Morale", Morale.get_morale()); @@ -615,7 +615,7 @@ void CBaseMonster::add_debug_info(debug::text_tree& root_s) //----------------------------------------------- TextTree& corpse_s = general_s.find_or_add("Corpse_Man"); - corpse_s.add_line("Current_Corpse", CorpseMan.get_corpse() ? *CorpseMan.get_corpse()->cName() : "none"); + corpse_s.add_line("Current_Corpse", CorpseMan.get_corpse() ? CorpseMan.get_corpse()->cName().c_str() : "none"); corpse_s.add_line("Satiety", make_xrstr("%.2f", GetSatiety())); //----------------------------------------------- @@ -748,7 +748,7 @@ void CBaseMonster::add_debug_info(debug::text_tree& root_s) TextTree& controller_s = root_s.find_or_add("Controllers"); TextTree& animation_s = controller_s.find_or_add("Animations"); - TextTree& current_animation_s = animation_s.add_line(*anim().cur_anim_info().name); + TextTree& current_animation_s = animation_s.add_line(anim().cur_anim_info().name.c_str()); CBlend* p_blend = control().animation().current_blend(); if (!p_blend) @@ -798,7 +798,7 @@ void CBaseMonster::add_debug_info(debug::text_tree& root_s) movement_s.add_line("Path_Type", pc_path_type); if (movement().path_type() == MovementManager::ePathTypePatrolPath) { - movement_s.add_line("Path_Name", *movement().patrol().path_name()); + movement_s.add_line("Path_Name", movement().patrol().path_name().c_str()); movement_s.add_line("Completed", movement().patrol().completed()); movement_s.add_line("Current_Point", movement().patrol().get_current_point_index()); @@ -872,13 +872,13 @@ void CBaseMonster::add_debug_info(debug::text_tree& root_s) movement().restrictions().base_in_restrictions().size()) { ::detail::add_debug_info_restrictions( - restrictions_s.add_line("out"), *movement().restrictions().out_restrictions()); + restrictions_s.add_line("out"), movement().restrictions().out_restrictions().c_str()); ::detail::add_debug_info_restrictions( - restrictions_s.add_line("in"), *movement().restrictions().in_restrictions()); + restrictions_s.add_line("in"), movement().restrictions().in_restrictions().c_str()); ::detail::add_debug_info_restrictions( - restrictions_s.add_line("base_out"), *movement().restrictions().base_out_restrictions()); + restrictions_s.add_line("base_out"), movement().restrictions().base_out_restrictions().c_str()); ::detail::add_debug_info_restrictions( - restrictions_s.add_line("base_in"), *movement().restrictions().base_in_restrictions()); + restrictions_s.add_line("base_in"), movement().restrictions().base_in_restrictions().c_str()); restrictions_s.add_line( "Actor_Accessible?", actor ? movement().restrictions().accessible(actor->Position()) : false); diff --git a/src/xrGame/ai/monsters/basemonster/base_monster_script.cpp b/src/xrGame/ai/monsters/basemonster/base_monster_script.cpp index 241fbf1088e..904ec594772 100644 --- a/src/xrGame/ai/monsters/basemonster/base_monster_script.cpp +++ b/src/xrGame/ai/monsters/basemonster/base_monster_script.cpp @@ -498,7 +498,7 @@ void CBaseMonster::ProcessScripts() { DBG().object_info(this, this).remove_item(u32(0)); DBG().object_info(this, this).remove_item(u32(1)); - DBG().object_info(this, this).add_item(*cName(), color_xrgb(255, 0, 0), 0); + DBG().object_info(this, this).add_item(cName().c_str(), color_xrgb(255, 0, 0), 0); DBG().object_info(this, this).add_item("Under script", color_xrgb(255, 0, 0), 1); } else diff --git a/src/xrGame/ai/monsters/monster_home.cpp b/src/xrGame/ai/monsters/monster_home.cpp index 0eb9b7f00b8..35f05d5f800 100644 --- a/src/xrGame/ai/monsters/monster_home.cpp +++ b/src/xrGame/ai/monsters/monster_home.cpp @@ -19,15 +19,17 @@ void check_path(const CBaseMonster* monster, const CPatrolPath* path) ai().level_graph().level_id(), make_string("invalid patrol path [%s] as home specified for monster [%s]\nmonster is on level %s\npatrol path " "is on level %s", - *path->m_name, *monster->cName(), - *ai().game_graph() + path->m_name.c_str(), monster->cName().c_str(), + ai().game_graph() .header() .level(ai().game_graph().vertex(monster->ai_location().game_vertex_id())->level_id()) - .name(), - *ai().game_graph() + .name() + .c_str(), + ai().game_graph() .header() .level(ai().game_graph().vertex(path->vertices().begin()->second->data().game_vertex_id())->level_id()) - .name())); + .name() + .c_str())); } #else // DEBUG #define check_path(a, b) diff --git a/src/xrGame/ai/stalker/ai_stalker.cpp b/src/xrGame/ai/stalker/ai_stalker.cpp index 4f3c82aa31b..b4a84029aac 100644 --- a/src/xrGame/ai/stalker/ai_stalker.cpp +++ b/src/xrGame/ai/stalker/ai_stalker.cpp @@ -111,7 +111,7 @@ void CAI_Stalker::reinit() //загрузка спецевической звуковой схемы для сталкера согласно m_SpecificCharacter sound().sound_prefix(SpecificCharacter().sound_voice_prefix()); - LoadSounds(*cNameSect()); + LoadSounds(cNameSect().c_str()); m_pPhysics_support->in_Init(); @@ -308,11 +308,11 @@ void CAI_Stalker::reload(LPCSTR section) m_can_select_weapon = true; - LPCSTR queue_sect = READ_IF_EXISTS(pSettings, r_string, *cNameSect(), "fire_queue_section", nullptr); + LPCSTR queue_sect = READ_IF_EXISTS(pSettings, r_string, cNameSect().c_str(), "fire_queue_section", nullptr); if (!queue_sect || xr_strcmp(queue_sect, "") != 0 || !pSettings->section_exist(queue_sect)) { - queue_sect = *cNameSect(); + queue_sect = cNameSect().c_str(); } const auto tryToRead = [&](pcstr lineToRead, u32 defaultValue) -> u32 @@ -610,7 +610,7 @@ bool CAI_Stalker::net_Spawn(CSE_Abstract* DC) if (SpecificCharacter().terrain_sect().size()) { - movement().locations().Load(*SpecificCharacter().terrain_sect()); + movement().locations().Load(SpecificCharacter().terrain_sect().c_str()); } sight().update(); @@ -818,7 +818,7 @@ void CAI_Stalker::UpdateCL() { START_PROFILE("stalker") START_PROFILE("stalker/client_update") - VERIFY2(PPhysicsShell() || getEnabled(), *cName()); + VERIFY2(PPhysicsShell() || getEnabled(), cName().c_str()); if (g_Alive()) { @@ -907,7 +907,7 @@ void CAI_Stalker::shedule_Update(u32 DT) { START_PROFILE("stalker") START_PROFILE("stalker/schedule_update") - VERIFY2(getEnabled() || PPhysicsShell(), *cName()); + VERIFY2(getEnabled() || PPhysicsShell(), cName().c_str()); if (!CObjectHandler::planner().initialized()) { diff --git a/src/xrGame/ai/stalker/ai_stalker_cover.cpp b/src/xrGame/ai/stalker/ai_stalker_cover.cpp index 91870b525ab..aa572fea2ca 100644 --- a/src/xrGame/ai/stalker/ai_stalker_cover.cpp +++ b/src/xrGame/ai/stalker/ai_stalker_cover.cpp @@ -269,7 +269,7 @@ void CAI_Stalker::on_restrictions_change() inherited::on_restrictions_change(); m_best_cover_actual = false; #ifdef _DEBUG - Msg("* [%6d][%s] on_restrictions_change", Device.dwTimeGlobal, *cName()); + Msg("* [%6d][%s] on_restrictions_change", Device.dwTimeGlobal, cName().c_str()); #endif } diff --git a/src/xrGame/ai/stalker/ai_stalker_debug.cpp b/src/xrGame/ai/stalker/ai_stalker_debug.cpp index 861ee17491d..311a9dc31e4 100644 --- a/src/xrGame/ai/stalker/ai_stalker_debug.cpp +++ b/src/xrGame/ai/stalker/ai_stalker_debug.cpp @@ -208,8 +208,8 @@ void draw_restrictions(const shared_str& restrictions, LPCSTR start_indent, LPCS { DBG_OutText("%s%s%s", start_indent, indent, header); string256 temp; - for (u32 i = 0, n = _GetItemCount(*restrictions); i < n; ++i) - DBG_OutText("%s%s%s%s", start_indent, indent, indent, _GetItem(*restrictions, i, temp)); + for (u32 i = 0, n = _GetItemCount(restrictions.c_str()); i < n; ++i) + DBG_OutText("%s%s%s%s", start_indent, indent, indent, _GetItem(restrictions.c_str(), i, temp)); } LPCSTR movement_type(const MonsterSpace::EMovementType& movement_type) @@ -267,8 +267,8 @@ void CAI_Stalker::debug_text() DBG_TextOutSet(0, up_indent); // memory DBG_OutText("memory"); - DBG_OutText("%sname : %s", indent, *cName()); - DBG_OutText("%ssection : %s", indent, *cNameSect()); + DBG_OutText("%sname : %s", indent, cName().c_str()); + DBG_OutText("%ssection : %s", indent, cNameSect().c_str()); DBG_OutText("%sid : %d", indent, ID()); DBG_OutText("%shealth : %f", indent, conditions().health()); DBG_OutText("%swounded : %c", indent, wounded() ? '+' : '-'); @@ -304,7 +304,7 @@ void CAI_Stalker::debug_text() DBG_OutText("%s%s%stype", indent, indent, indent); DBG_OutText("%s%s%spower : %f", indent, indent, indent, memory().sound().sound()->m_power); DBG_OutText("%s%s%sobject : %s", indent, indent, indent, - memory().sound().sound()->m_object ? *memory().sound().sound()->m_object->cName() : "unknown"); + memory().sound().sound()->m_object ? memory().sound().sound()->m_object->cName().c_str() : "unknown"); if (g_Alive() && memory().sound().sound()->m_object) DBG_OutText("%s%s%svisible : %s", indent, indent, indent, memory().visual().visible_now(memory().sound().sound()->m_object) ? "+" : "-"); @@ -316,14 +316,14 @@ void CAI_Stalker::debug_text() ALife::_OBJECT_ID object_id = memory().hit().last_hit_object_id(); DBG_OutText("%s%slast hit object id : %d", indent, indent, object_id); IGameObject* object = (object_id == ALife::_OBJECT_ID(-1)) ? 0 : Level().Objects.net_Find(object_id); - DBG_OutText("%s%slast hit object name : %s", indent, indent, object ? *object->cName() : ""); + DBG_OutText("%s%slast hit object name : %s", indent, indent, object ? object->cName().c_str() : ""); #ifdef USE_SELECTED_HIT if (memory().hit().hit()) { DBG_OutText("%s%sselected", indent, indent); DBG_OutText("%s%s%spower : %f", indent, indent, indent, memory().hit().hit()->m_amount); DBG_OutText("%s%s%sobject : %s", indent, indent, indent, - memory().hit().hit()->m_object ? *memory().hit().hit()->m_object->cName() : "unknown"); + memory().hit().hit()->m_object ? memory().hit().hit()->m_object->cName().c_str() : "unknown"); if (g_Alive() && memory().hit().hit()->m_object) DBG_OutText("%s%s%svisible : %s", indent, indent, indent, memory().visual().visible_now(memory().hit().hit()->m_object) ? "+" : "-"); @@ -348,7 +348,7 @@ void CAI_Stalker::debug_text() CEnemyManager::OBJECTS::const_iterator I = memory().enemy().objects().begin(); CEnemyManager::OBJECTS::const_iterator E = memory().enemy().objects().end(); for (; I != E; ++I) - DBG_OutText("%s%s%s%s : %s", indent, indent, indent, *(*I)->cName(), + DBG_OutText("%s%s%s%s : %s", indent, indent, indent, (*I)->cName().c_str(), memory().visual().visible_now(*I) ? "visible" : "invisible"); } @@ -372,7 +372,7 @@ void CAI_Stalker::debug_text() DBG_OutText("%s%s%svisible : %s %f", indent, indent, indent, memory().visual().visible_now(memory().enemy().selected()) ? "+" : "-", fuzzy); } - DBG_OutText("%s%s%sobject : %s", indent, indent, indent, *memory().enemy().selected()->cName()); + DBG_OutText("%s%s%sobject : %s", indent, indent, indent, memory().enemy().selected()->cName().c_str()); if (g_Alive()) { float interval = (1.f - panic_threshold()) * .25f, left = -1.f, right = -1.f; @@ -432,7 +432,7 @@ void CAI_Stalker::debug_text() DBG_OutText("%s%s%stime : %.3f (%.3f)", indent, indent, indent, float(memory().danger().selected()->time()) / 1000.f, float(Device.dwTimeGlobal - memory().danger().selected()->time()) / 1000.f); - DBG_OutText("%s%s%sinitiator : %s", indent, indent, indent, *memory().danger().selected()->object()->cName()); + DBG_OutText("%s%s%sinitiator : %s", indent, indent, indent, memory().danger().selected()->object()->cName().c_str()); if (g_Alive() && memory().danger().selected()->object()) DBG_OutText("%s%s%svisible : %s", indent, indent, indent, memory().visual().visible_now(memory().danger().selected()->object()) ? "+" : "-"); @@ -441,7 +441,7 @@ void CAI_Stalker::debug_text() !!memory().danger().selected()->dependent_object()->cName()) { DBG_OutText("%s%s%sdependent : %s", indent, indent, indent, - *memory().danger().selected()->dependent_object()->cName()); + memory().danger().selected()->dependent_object()->cName().c_str()); if (g_Alive()) DBG_OutText("%s%s%svisible : %s", indent, indent, indent, memory().visual().visible_now( @@ -483,12 +483,12 @@ void CAI_Stalker::debug_text() if (agent_manager().member().member(this).member_death_reaction().m_processing) DBG_OutText("%react on death : %s", indent, - *agent_manager().member().member(this).member_death_reaction().m_member->cName()); + agent_manager().member().member(this).member_death_reaction().m_member->cName().c_str()); if (agent_manager().member().member(this).grenade_reaction().m_processing) DBG_OutText("%react on grenade : %s", indent, agent_manager().member().member(this).grenade_reaction().m_game_object ? - *agent_manager().member().member(this).grenade_reaction().m_game_object->cName() : + agent_manager().member().member(this).grenade_reaction().m_game_object->cName().c_str() : "unknown"); } } @@ -498,18 +498,18 @@ void CAI_Stalker::debug_text() DBG_OutText("%sobjects", indent); DBG_OutText("%s%sobjects : %d", indent, indent, inventory().m_all.size()); DBG_OutText("%s%sactive item : %s", indent, indent, - inventory().ActiveItem() ? *inventory().ActiveItem()->object().cName() : ""); - DBG_OutText("%s%sbest weapon : %s", indent, indent, best_weapon() ? *best_weapon()->object().cName() : ""); + inventory().ActiveItem() ? inventory().ActiveItem()->object().cName().c_str() : ""); + DBG_OutText("%s%sbest weapon : %s", indent, indent, best_weapon() ? best_weapon()->object().cName().c_str() : ""); DBG_OutText( - "%s%sitem to kill : %s", indent, indent, item_to_kill() ? *m_best_item_to_kill->object().cName() : ""); + "%s%sitem to kill : %s", indent, indent, item_to_kill() ? m_best_item_to_kill->object().cName().c_str() : ""); DBG_OutText("%s%sitem can kill : %s", indent, indent, item_can_kill() ? "+" : "-"); DBG_OutText("%s%smemory item to kill : %s", indent, indent, - remember_item_to_kill() ? *m_best_found_item_to_kill->object().cName() : ""); + remember_item_to_kill() ? m_best_found_item_to_kill->object().cName().c_str() : ""); DBG_OutText( - "%s%smemory ammo : %s", indent, indent, remember_ammo() ? *m_best_found_ammo->object().cName() : ""); + "%s%smemory ammo : %s", indent, indent, remember_ammo() ? m_best_found_ammo->object().cName().c_str() : ""); DBG_OutText("%s%sinfinite ammo : %s", indent, indent, m_infinite_ammo ? "+" : "-"); DBG_OutText( - "%s%sitem to spawn : %s", indent, indent, item_to_spawn().size() ? *item_to_spawn() : "no item to spawn"); + "%s%sitem to spawn : %s", indent, indent, item_to_spawn().size() ? item_to_spawn().c_str() : "no item to spawn"); DBG_OutText("%s%sammo in box to spawn: %d", indent, indent, item_to_spawn().size() ? ammo_in_box_to_spawn() : 0); CWeaponMagazined* weapon = smart_cast(inventory().ActiveItem()); @@ -525,7 +525,7 @@ void CAI_Stalker::debug_text() { DBG_OutText("%s%sactive item", indent, indent); DBG_OutText("%s%s%sobject : %s", indent, indent, indent, - inventory().ActiveItem() ? *inventory().ActiveItem()->object().cName() : ""); + inventory().ActiveItem() ? inventory().ActiveItem()->object().cName().c_str() : ""); CWeapon* weapon = smart_cast(inventory().ActiveItem()); if (weapon) { @@ -654,7 +654,7 @@ void CAI_Stalker::debug_text() if (movement().path_type() == MovementManager::ePathTypePatrolPath) { DBG_OutText("%s%spatrol", indent, indent); - DBG_OutText("%s%s%spath : %s", indent, indent, indent, *movement().patrol().path_name()); + DBG_OutText("%s%s%spath : %s", indent, indent, indent, movement().patrol().path_name().c_str()); DBG_OutText("%s%s%scompleted : %s", indent, indent, indent, movement().patrol().completed() ? "+" : "-"); DBG_OutText("%s%s%scurrent point : %d", indent, indent, indent, movement().patrol().get_current_point_index()); if (movement().patrol().get_path() && @@ -924,7 +924,7 @@ void CAI_Stalker::debug_text() } case SightManager::eSightTypeObject: { - DBG_OutText("%s%sobject : %s", indent, indent, *sight().current_action().object().cName()); + DBG_OutText("%s%sobject : %s", indent, indent, sight().current_action().object().cName().c_str()); DBG_OutText( "%s%sposition : [%f][%f][%f]", indent, indent, VPUSH(sight().current_action().object().Position())); break; @@ -939,7 +939,7 @@ void CAI_Stalker::debug_text() } case SightManager::eSightTypeFireObject: { - DBG_OutText("%s%sobject : %s", indent, indent, *sight().current_action().object().cName()); + DBG_OutText("%s%sobject : %s", indent, indent, sight().current_action().object().cName().c_str()); DBG_OutText( "%s%sposition : [%f][%f][%f]", indent, indent, VPUSH(sight().current_action().object().Position())); DBG_OutText("%s%svisible point : %s", indent, indent, false ? "-" : "+"); diff --git a/src/xrGame/ai/trader/ai_trader.cpp b/src/xrGame/ai/trader/ai_trader.cpp index 8748e6a4e84..d0c7c4bedf1 100644 --- a/src/xrGame/ai/trader/ai_trader.cpp +++ b/src/xrGame/ai/trader/ai_trader.cpp @@ -224,7 +224,7 @@ void CAI_Trader::feel_touch_new(IGameObject* O) if (I && I->useful_for_NPC()) { - Msg("Taking item %s!", *I->object().cName()); + Msg("Taking item %s!", I->object().cName().c_str()); NET_Packet P; u_EventGen(P, GE_OWNERSHIP_TAKE, ID()); P.w_u16(u16(I->object().ID())); diff --git a/src/xrGame/alife_simulator_base.cpp b/src/xrGame/alife_simulator_base.cpp index fadb911e70b..eaed45ff50e 100644 --- a/src/xrGame/alife_simulator_base.cpp +++ b/src/xrGame/alife_simulator_base.cpp @@ -102,7 +102,7 @@ CSE_Abstract* CALifeSimulatorBase::spawn_item(LPCSTR section, const Fvector& pos abstract->m_wVersion = SPAWN_VERSION; string256 s_name_replace; - xr_strcpy(s_name_replace, *abstract->s_name); + xr_strcpy(s_name_replace, abstract->s_name.c_str()); if (abstract->ID < 1000) xr_strcat(s_name_replace, "0"); if (abstract->ID < 100) @@ -159,7 +159,7 @@ CSE_Abstract* CALifeSimulatorBase::create(CSE_ALifeGroupAbstract* tpALifeGroupAb k->m_bALifeControl = true; string256 s_name_replace; - xr_strcpy(s_name_replace, *k->s_name); + xr_strcpy(s_name_replace, k->s_name.c_str()); if (k->ID < 1000) xr_strcat(s_name_replace, "0"); if (k->ID < 100) @@ -178,8 +178,8 @@ CSE_Abstract* CALifeSimulatorBase::create(CSE_ALifeGroupAbstract* tpALifeGroupAb void CALifeSimulatorBase::create(CSE_ALifeDynamicObject*& i, CSE_ALifeDynamicObject* j, const _SPAWN_ID& tSpawnID) { - CSE_Abstract* tpSE_Abstract = F_entity_Create(*j->s_name); - R_ASSERT3(tpSE_Abstract, "Cannot find item with section", *j->s_name); + CSE_Abstract* tpSE_Abstract = F_entity_Create(j->s_name.c_str()); + R_ASSERT3(tpSE_Abstract, "Cannot find item with section", j->s_name.c_str()); i = smart_cast(tpSE_Abstract); R_ASSERT2(i, "Non-ALife object in the 'game.spawn'"); @@ -264,7 +264,7 @@ void CALifeSimulatorBase::release(CSE_Abstract* abstract, bool alife_query) #ifdef DEBUG if (psAI_Flags.test(aiALife)) { - Msg("[LSS] Releasing object [%s][%s][%d][%x]", abstract->name_replace(), *abstract->s_name, abstract->ID, + Msg("[LSS] Releasing object [%s][%s][%d][%x]", abstract->name_replace(), abstract->s_name.c_str(), abstract->ID, smart_cast(abstract)); } #endif diff --git a/src/xrGame/alife_simulator_script.cpp b/src/xrGame/alife_simulator_script.cpp index fcf26f6db78..7f4df74754c 100644 --- a/src/xrGame/alife_simulator_script.cpp +++ b/src/xrGame/alife_simulator_script.cpp @@ -92,13 +92,13 @@ void generate_story_ids(STORY_PAIRS& result, _id_type INVALID_ID, LPCSTR section { const shared_str& temp = Ini->r_string_wb(section_name, N); - R_ASSERT3(!strchr(*temp, ' '), invalid_id_description, *temp); - R_ASSERT2(xr_strcmp(*temp, INVALID_ID_STRING), invalid_id_redefinition); + R_ASSERT3(!strchr(temp.c_str(), ' '), invalid_id_description, temp.c_str()); + R_ASSERT2(xr_strcmp(temp.c_str(), INVALID_ID_STRING), invalid_id_redefinition); for (const auto& story : result) - R_ASSERT3(story.first != temp, duplicated_id_description, *temp); + R_ASSERT3(story.first != temp, duplicated_id_description, temp.c_str()); - result.emplace_back(*temp, atoi(N)); + result.emplace_back(temp.c_str(), atoi(N)); ++k; } @@ -302,7 +302,7 @@ void CALifeSimulator__release(CALifeSimulator* self, CSE_Abstract* object, bool) LPCSTR get_level_name(const CALifeSimulator* self, int level_id) { - LPCSTR result = *ai().game_graph().header().level((GameGraph::_LEVEL_ID)level_id).name(); + LPCSTR result = ai().game_graph().header().level((GameGraph::_LEVEL_ID)level_id).name().c_str(); return (result); } @@ -523,7 +523,7 @@ void CALifeSimulator::script_register(lua_State* luaState) STORY_PAIRS::const_iterator I = story_ids.begin(); STORY_PAIRS::const_iterator E = story_ids.end(); for (; I != E; ++I) - instance.enum_("_story_ids")[luabind::value(*(*I).first, (*I).second)]; + instance.enum_("_story_ids")[luabind::value((*I).first.c_str(), (*I).second)]; luabind::module(luaState)[instance]; } @@ -543,7 +543,7 @@ void CALifeSimulator::script_register(lua_State* luaState) SPAWN_STORY_PAIRS::const_iterator I = spawn_story_ids.begin(); SPAWN_STORY_PAIRS::const_iterator E = spawn_story_ids.end(); for (; I != E; ++I) - instance.enum_("_spawn_story_ids")[luabind::value(*(*I).first, (*I).second)]; + instance.enum_("_spawn_story_ids")[luabind::value((*I).first.c_str(), (*I).second)]; luabind::module(luaState)[instance]; } diff --git a/src/xrGame/animation_script_callback.cpp b/src/xrGame/animation_script_callback.cpp index e8a4c1ff60a..c18bac844ef 100644 --- a/src/xrGame/animation_script_callback.cpp +++ b/src/xrGame/animation_script_callback.cpp @@ -27,7 +27,7 @@ CBlend* PlayMotionByParts( CBlend* anim_script_callback::play_cycle(IKinematicsAnimated* sa, const shared_str& anim) { - MotionID m = sa->LL_MotionID(*anim); + MotionID m = sa->LL_MotionID(anim.c_str()); R_ASSERT(m.valid()); if (sa->LL_GetMotionDef(m)->StopAtEnd()) { diff --git a/src/xrGame/console_commands.cpp b/src/xrGame/console_commands.cpp index e0f325402bf..d50f275ea61 100644 --- a/src/xrGame/console_commands.cpp +++ b/src/xrGame/console_commands.cpp @@ -1402,13 +1402,13 @@ class CCC_ScriptCommand : public IConsole_Command l_iErrorCode = lua_pcall(GEnv.ScriptEngine->lua(), 0, 0, 0); if (l_iErrorCode) { - GEnv.ScriptEngine->print_output(GEnv.ScriptEngine->lua(), *m_script_name, l_iErrorCode); + GEnv.ScriptEngine->print_output(GEnv.ScriptEngine->lua(), m_script_name.c_str(), l_iErrorCode); GEnv.ScriptEngine->on_error(GEnv.ScriptEngine->lua()); return; } } - GEnv.ScriptEngine->print_output(GEnv.ScriptEngine->lua(), *m_script_name, l_iErrorCode); + GEnv.ScriptEngine->print_output(GEnv.ScriptEngine->lua(), m_script_name.c_str(), l_iErrorCode); } } // void Execute @@ -1757,7 +1757,7 @@ class CCC_DumpModelBones : public IConsole_Command Msg("bones for model \"%s\"", arguments); for (u16 i = 0, n = kinematics->LL_BoneCount(); i < n; ++i) - Msg("%s", *kinematics->LL_GetData(i).name); + Msg("%s", kinematics->LL_GetData(i).name.c_str()); GEnv.Render->model_Delete(visual); } diff --git a/src/xrGame/ef_storage_script.cpp b/src/xrGame/ef_storage_script.cpp index 66d1c06f3bc..ec2c0d703d7 100644 --- a/src/xrGame/ef_storage_script.cpp +++ b/src/xrGame/ef_storage_script.cpp @@ -35,7 +35,7 @@ float evaluate(CEF_Storage* ef_storage, LPCSTR function, CScriptGameObject* _0, if (_0 && !ef_storage->non_alife().member()) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "object %s is not herited from CSE_ALifeSchedulable!", *_0->cName()); + LuaMessageType::Error, "object %s is not herited from CSE_ALifeSchedulable!", _0->cName().c_str()); return (0.f); } @@ -43,7 +43,7 @@ float evaluate(CEF_Storage* ef_storage, LPCSTR function, CScriptGameObject* _0, if (_1 && !ef_storage->non_alife().enemy()) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "object %s is not herited from CSE_ALifeSchedulable!", *_1->cName()); + LuaMessageType::Error, "object %s is not herited from CSE_ALifeSchedulable!", _1->cName().c_str()); return (0.f); } diff --git a/src/xrGame/enemy_manager.cpp b/src/xrGame/enemy_manager.cpp index 6d524249382..909753b519a 100644 --- a/src/xrGame/enemy_manager.cpp +++ b/src/xrGame/enemy_manager.cpp @@ -195,7 +195,7 @@ void CEnemyManager::ignore_monster_threshold(const float& ignore_monster_thresho void CEnemyManager::restore_ignore_monster_threshold() { m_ignore_monster_threshold = - READ_IF_EXISTS(pSettings, r_float, *m_object->cNameSect(), "ignore_monster_threshold", 1.f); + READ_IF_EXISTS(pSettings, r_float, m_object->cNameSect().c_str(), "ignore_monster_threshold", 1.f); } float CEnemyManager::ignore_monster_threshold() const { return (m_ignore_monster_threshold); } @@ -206,7 +206,7 @@ void CEnemyManager::max_ignore_monster_distance(const float& max_ignore_monster_ void CEnemyManager::restore_max_ignore_monster_distance() { - m_max_ignore_distance = READ_IF_EXISTS(pSettings, r_float, *m_object->cNameSect(), "max_ignore_distance", 0.f); + m_max_ignore_distance = READ_IF_EXISTS(pSettings, r_float, m_object->cNameSect().c_str(), "max_ignore_distance", 0.f); } float CEnemyManager::max_ignore_monster_distance() const { return (m_max_ignore_distance); } diff --git a/src/xrGame/fs_registrator_script.cpp b/src/xrGame/fs_registrator_script.cpp index c47b25b6e41..9fc7799eb2f 100644 --- a/src/xrGame/fs_registrator_script.cpp +++ b/src/xrGame/fs_registrator_script.cpp @@ -15,7 +15,7 @@ LPCSTR update_path_script(CLocatorAPI* fs, LPCSTR initial, LPCSTR src) convert_path_separators(temp); #endif temp_2 = temp; - return *temp_2; + return temp_2.c_str(); } //Alundaio: Set flag to rescan all files in path diff --git a/src/xrGame/game_cl_base.cpp b/src/xrGame/game_cl_base.cpp index ac3f160a7e2..3961f4ad9f9 100644 --- a/src/xrGame/game_cl_base.cpp +++ b/src/xrGame/game_cl_base.cpp @@ -236,7 +236,7 @@ void game_cl_GameState::TranslateGameMessage(u32 msg, NET_Packet& P) players.emplace(newClientId, PS); OnNewPlayerConnected(newClientId); } - xr_sprintf(Text, "%s%s %s%s", Color_Teams[0], PS->getName(), Color_Main, *StringTable().translate("mp_connected")); + xr_sprintf(Text, "%s%s %s%s", Color_Teams[0], PS->getName(), Color_Main, StringTable().translate("mp_connected").c_str()); if (CurrentGameUI()) CurrentGameUI()->CommonMessageOut(Text); //--------------------------------------- @@ -248,7 +248,7 @@ void game_cl_GameState::TranslateGameMessage(u32 msg, NET_Packet& P) string64 PlayerName; P.r_stringZ(PlayerName); - xr_sprintf(Text, "%s%s %s%s", Color_Teams[0], PlayerName, Color_Main, *StringTable().translate("mp_disconnected")); + xr_sprintf(Text, "%s%s %s%s", Color_Teams[0], PlayerName, Color_Main, StringTable().translate("mp_disconnected").c_str()); if (CurrentGameUI()) CurrentGameUI()->CommonMessageOut(Text); //--------------------------------------- @@ -260,7 +260,7 @@ void game_cl_GameState::TranslateGameMessage(u32 msg, NET_Packet& P) string64 PlayerName; P.r_stringZ(PlayerName); - xr_sprintf(Text, "%s%s %s%s", Color_Teams[0], PlayerName, Color_Main, *StringTable().translate("mp_entered_game")); + xr_sprintf(Text, "%s%s %s%s", Color_Teams[0], PlayerName, Color_Main, StringTable().translate("mp_entered_game").c_str()); if (CurrentGameUI()) CurrentGameUI()->CommonMessageOut(Text); } diff --git a/src/xrGame/game_cl_base_weapon_usage_statistic.cpp b/src/xrGame/game_cl_base_weapon_usage_statistic.cpp index fdcedbfde11..8a156d26bb2 100644 --- a/src/xrGame/game_cl_base_weapon_usage_statistic.cpp +++ b/src/xrGame/game_cl_base_weapon_usage_statistic.cpp @@ -449,8 +449,8 @@ void WeaponUsageStatistic::RemoveBullet(ABULLETS_it& Bullet_it) if (!Bullet_it->Removed || Bullet_it->HitRefCount != Bullet_it->HitResponds) return; //------------------------------------------------------------- - auto PlayerIt = FindPlayer(*(Bullet_it->FirerName)); - auto WeaponIt = PlayerIt->FindPlayersWeapon(*(Bullet_it->WeaponName)); + auto PlayerIt = FindPlayer(Bullet_it->FirerName.c_str()); + auto WeaponIt = PlayerIt->FindPlayersWeapon(Bullet_it->WeaponName.c_str()); HITS_VEC_it HitIt; if (WeaponIt->FindHit(Bullet_it->Bullet.m_dwID, HitIt)) { @@ -504,10 +504,10 @@ void WeaponUsageStatistic::OnBullet_Fire(SBullet* pBullet, const CCartridge& car if (!pActor) return; //----------------------------------------------------------------------------------- - auto PlayerIt = FindPlayer(*object_parent->cName()); + auto PlayerIt = FindPlayer(object_parent->cName().c_str()); pBullet->m_dwID = PlayerIt->m_dwTotalShots++; PlayerIt->m_dwTotalShots_d++; - auto WeaponIt = PlayerIt->FindPlayersWeapon(*object_weapon->cNameSect()); + auto WeaponIt = PlayerIt->FindPlayersWeapon(object_weapon->cNameSect().c_str()); WeaponIt->m_dwRoundsFired = (++WeaponIt->m_dwBulletsFired) / cartridge.param_s.buckShot; WeaponIt->m_dwBulletsFired_d++; //----------------------------------------------------------------------------------- @@ -526,8 +526,8 @@ void WeaponUsageStatistic::OnBullet_Hit(SBullet* pBullet, u16 TargetID, s16 elem if (!FindBullet(pBullet->m_dwID, BulletIt)) return; //----------------------------------------------------- - auto PlayerIt = FindPlayer(*(BulletIt->FirerName)); - auto WeaponIt = PlayerIt->FindPlayersWeapon(*(BulletIt->WeaponName)); + auto PlayerIt = FindPlayer(BulletIt->FirerName.c_str()); + auto WeaponIt = PlayerIt->FindPlayersWeapon(BulletIt->WeaponName.c_str()); if (!BulletIt->HitRefCount++) { WeaponIt->m_dwHitsScored++; @@ -717,8 +717,8 @@ void WeaponUsageStatistic::On_Check_Respond(NET_Packet* P) BulletIt->HitResponds++; //--------------------------------------------------------------- - auto PlayerIt = FindPlayer(*(BulletIt->FirerName)); - auto WeaponIt = PlayerIt->FindPlayersWeapon(*(BulletIt->WeaponName)); + auto PlayerIt = FindPlayer(BulletIt->FirerName.c_str()); + auto WeaponIt = PlayerIt->FindPlayersWeapon(BulletIt->WeaponName.c_str()); (*WeaponIt).m_dwKillsScored++; (*WeaponIt).m_dwKillsScored_d++; @@ -998,7 +998,7 @@ void WeaponUsageStatistic::OnUpdateRespond(NET_Packet* P, shared_str const& send shared_str PName; P->r_stringZ(PName); - Player_Statistic& PS = *(FindPlayer(*PName)); + Player_Statistic& PS = *(FindPlayer(PName.c_str())); PS.PDigest = sender_digest; PS.PID = sender_pid; Msg("--- CL: On Update Respond from [%s]", PName.c_str()); diff --git a/src/xrGame/game_cl_base_weapon_usage_statistic_save.cpp b/src/xrGame/game_cl_base_weapon_usage_statistic_save.cpp index cbca57095ac..ca0d88658d2 100644 --- a/src/xrGame/game_cl_base_weapon_usage_statistic_save.cpp +++ b/src/xrGame/game_cl_base_weapon_usage_statistic_save.cpp @@ -86,7 +86,7 @@ void WeaponUsageStatistic::SaveData() #else time_t Time; time(&Time); - xr_sprintf(mFileName, "(%s)_(%s)_%s.wus", *(Level().name()), GameType, ctime(&Time)); + xr_sprintf(mFileName, "(%s)_(%s)_%s.wus", Level().name().c_str(), GameType, ctime(&Time)); #endif //--------------------------------------------------------- FS.update_path(mFileName, "$logs$", mFileName); diff --git a/src/xrGame/game_cl_capture_the_artefact.cpp b/src/xrGame/game_cl_capture_the_artefact.cpp index fd2831b7731..15eb265ce45 100644 --- a/src/xrGame/game_cl_capture_the_artefact.cpp +++ b/src/xrGame/game_cl_capture_the_artefact.cpp @@ -299,7 +299,7 @@ void game_cl_CaptureTheArtefact::TranslateGameMessage(u32 msg, NET_Packet& P) } else { - xr_sprintf(Text, "%s%s", Color_Main, *st.translate("mp_you_captured_artefact")); + xr_sprintf(Text, "%s%s", Color_Main, st.translate("mp_you_captured_artefact").c_str()); PlayCapturedTheArtefact(ps); diff --git a/src/xrGame/game_cl_deathmatch.cpp b/src/xrGame/game_cl_deathmatch.cpp index 3221f860816..c72fc4b2d53 100644 --- a/src/xrGame/game_cl_deathmatch.cpp +++ b/src/xrGame/game_cl_deathmatch.cpp @@ -516,11 +516,11 @@ void game_cl_Deathmatch::shedule_Update(u32 dt) ConvertTime2String(&S, TimeRemains); string1024 tmpStr = ""; if (TimeRemains > 10000) - strconcat(sizeof(tmpStr), tmpStr, *st.translate("mp_time2start"), " ", S); + strconcat(sizeof(tmpStr), tmpStr, st.translate("mp_time2start").c_str(), " ", S); else { if (TimeRemains < 1000) - strconcat(sizeof(tmpStr), tmpStr, *st.translate("mp_go"), ""); + strconcat(sizeof(tmpStr), tmpStr, st.translate("mp_go").c_str(), ""); else { static u32 dwLastTimeRemains = 10; @@ -532,7 +532,7 @@ void game_cl_Deathmatch::shedule_Update(u32 dt) } dwLastTimeRemains = dwCurTimeRemains; xr_itoa(dwCurTimeRemains, S, 10); - strconcat(sizeof(tmpStr), tmpStr, *st.translate("mp_ready"), "...", S); + strconcat(sizeof(tmpStr), tmpStr, st.translate("mp_ready").c_str(), "...", S); } }; @@ -596,7 +596,7 @@ void game_cl_Deathmatch::shedule_Update(u32 dt) string64 S; ConvertTime2String(&S, Rest); string128 FullS; - xr_sprintf(FullS, "%s : %s", *st.translate("mp_time2respawn"), S); + xr_sprintf(FullS, "%s : %s", st.translate("mp_time2respawn").c_str(), S); m_game_ui->SetForceRespawnTimeCaption(FullS); }; @@ -812,15 +812,15 @@ void game_cl_Deathmatch::OnVoteStart(NET_Packet& P) if (!xr_strcmp(CmdName, "restart")) { - xr_sprintf(NewCmd, "%s", *st.translate("mp_restart")); + xr_sprintf(NewCmd, "%s", st.translate("mp_restart").c_str()); } else if (!xr_strcmp(CmdName, "restart_fast")) { - xr_sprintf(NewCmd, "%s", *st.translate("mp_restart_fast")); + xr_sprintf(NewCmd, "%s", st.translate("mp_restart_fast").c_str()); } else if (!xr_strcmp(CmdName, "kick")) { - xr_sprintf(NewCmd, "%s %s", *st.translate("mp_kick"), CmdParams[0]); + xr_sprintf(NewCmd, "%s %s", st.translate("mp_kick").c_str(), CmdParams[0]); for (size_t i = 1; i < MAX_VOTE_PARAMS; i++) { if (xr_strlen(CmdParams[i])) @@ -832,7 +832,7 @@ void game_cl_Deathmatch::OnVoteStart(NET_Packet& P) } else if (!xr_strcmp(CmdName, "ban")) { - xr_sprintf(NewCmd, "%s %s", *st.translate("mp_ban"), CmdParams[0]); + xr_sprintf(NewCmd, "%s %s", st.translate("mp_ban").c_str(), CmdParams[0]); for (size_t i = 1; i < MAX_VOTE_PARAMS; i++) { if (xr_strlen(CmdParams[i])) @@ -844,15 +844,15 @@ void game_cl_Deathmatch::OnVoteStart(NET_Packet& P) } else if (!xr_strcmp(CmdName, "changemap")) { - xr_sprintf(NewCmd, "%s %s", *st.translate("mp_change_map"), *st.translate(CmdParams[0])); + xr_sprintf(NewCmd, "%s %s", st.translate("mp_change_map").c_str(), st.translate(CmdParams[0]).c_str()); } else if (!xr_strcmp(CmdName, "changeweather")) { - xr_sprintf(NewCmd, "%s %s", *st.translate("mp_change_weather"), *st.translate(CmdParams[0])); + xr_sprintf(NewCmd, "%s %s", st.translate("mp_change_weather").c_str(), st.translate(CmdParams[0]).c_str()); } string1024 VoteStr; - xr_sprintf(VoteStr, *st.translate("mp_voting_started"), NewCmd, Player); + xr_sprintf(VoteStr, st.translate("mp_voting_started").c_str(), NewCmd, Player); m_game_ui->SetVoteMessage(VoteStr); m_game_ui->SetVoteTimeResultMsg(""); diff --git a/src/xrGame/game_cl_deathmatch_buywnd.cpp b/src/xrGame/game_cl_deathmatch_buywnd.cpp index 0538e119eca..ccc34d0ad0f 100644 --- a/src/xrGame/game_cl_deathmatch_buywnd.cpp +++ b/src/xrGame/game_cl_deathmatch_buywnd.cpp @@ -279,7 +279,7 @@ void game_cl_Deathmatch::CheckItem(PIItem pItem, PRESET_ITEMS* pPresetItems, BOO return; u8 SlotID, ItemID; - pCurBuyMenu->GetWeaponIndexByName(*pItem->object().cNameSect(), SlotID, ItemID); + pCurBuyMenu->GetWeaponIndexByName(pItem->object().cNameSect().c_str(), SlotID, ItemID); if (SlotID == 0xff || ItemID == 0xff) return; s16 BigID = GetBuyMenuItemIndex(SlotID, ItemID); @@ -318,7 +318,7 @@ void game_cl_Deathmatch::CheckItem(PIItem pItem, PRESET_ITEMS* pPresetItems, BOO { if (pWeapon->ScopeAttachable()) { - pCurBuyMenu->GetWeaponIndexByName(*pWeapon->GetScopeName(), SlotID, ItemID); + pCurBuyMenu->GetWeaponIndexByName(pWeapon->GetScopeName().c_str(), SlotID, ItemID); if (SlotID != 0xff && ItemID != 0xff) { if (pWeapon->IsScopeAttached()) @@ -336,7 +336,7 @@ void game_cl_Deathmatch::CheckItem(PIItem pItem, PRESET_ITEMS* pPresetItems, BOO if (pWeapon->GrenadeLauncherAttachable()) { - pCurBuyMenu->GetWeaponIndexByName(*pWeapon->GetGrenadeLauncherName(), SlotID, ItemID); + pCurBuyMenu->GetWeaponIndexByName(pWeapon->GetGrenadeLauncherName().c_str(), SlotID, ItemID); if (SlotID != 0xff && ItemID != 0xff) { if (pWeapon->IsGrenadeLauncherAttached()) @@ -356,7 +356,7 @@ void game_cl_Deathmatch::CheckItem(PIItem pItem, PRESET_ITEMS* pPresetItems, BOO if (pWeapon->SilencerAttachable()) { - pCurBuyMenu->GetWeaponIndexByName(*pWeapon->GetSilencerName(), SlotID, ItemID); + pCurBuyMenu->GetWeaponIndexByName(pWeapon->GetSilencerName().c_str(), SlotID, ItemID); if (SlotID != 0xff && ItemID != 0xff) { if (pWeapon->IsSilencerAttached()) @@ -458,7 +458,7 @@ void game_cl_Deathmatch::LoadDefItemsForRank(IBuyWnd* pBuyMenu) const shared_str& ItemName = pBuyMenu->GetWeaponNameByIndex(pDefItem->SlotID, pDefItem->ItemID); if (!ItemName.size()) continue; - if (!xr_strcmp(*ItemName, "mp_wpn_knife")) + if (!xr_strcmp(ItemName.c_str(), "mp_wpn_knife")) continue; if (!pSettings->line_exist(ItemName, "ammo_class")) continue; diff --git a/src/xrGame/game_cl_teamdeathmatch.cpp b/src/xrGame/game_cl_teamdeathmatch.cpp index eb93ac084ee..5ee44867dff 100644 --- a/src/xrGame/game_cl_teamdeathmatch.cpp +++ b/src/xrGame/game_cl_teamdeathmatch.cpp @@ -122,12 +122,12 @@ void game_cl_TeamDeathmatch::TranslateGameMessage(u32 msg, NET_Packet& P) xr_sprintf(Text, "%s%s %s%s %s%s", "", // no color - PlayerName, Color_Main, *StringTable().translate("mp_joined"), CTeamInfo::GetTeam_color_tag(int(Team)), + PlayerName, Color_Main, StringTable().translate("mp_joined").c_str(), CTeamInfo::GetTeam_color_tag(int(Team)), CTeamInfo::GetTeam_name(int(Team))); if (CurrentGameUI()) CurrentGameUI()->CommonMessageOut(Text); //--------------------------------------- - Msg("%s %s %s", PlayerName, *StringTable().translate("mp_joined"), CTeamInfo::GetTeam_name(int(Team))); + Msg("%s %s %s", PlayerName, StringTable().translate("mp_joined").c_str(), CTeamInfo::GetTeam_name(int(Team))); } break; @@ -143,12 +143,12 @@ void game_cl_TeamDeathmatch::TranslateGameMessage(u32 msg, NET_Packet& P) break; xr_sprintf(Text, "%s%s %s%s %s%s", CTeamInfo::GetTeam_color_tag(int(OldTeam)), pPlayer->getName(), Color_Main, - *StringTable().translate("mp_switched_to"), CTeamInfo::GetTeam_color_tag(int(NewTeam)), + StringTable().translate("mp_switched_to").c_str(), CTeamInfo::GetTeam_color_tag(int(NewTeam)), CTeamInfo::GetTeam_name(int(NewTeam))); if (CurrentGameUI()) CurrentGameUI()->CommonMessageOut(Text); //--------------------------------------- - Msg("%s *s %s", pPlayer->getName(), *StringTable().translate("mp_switched_to"), CTeamInfo::GetTeam_name(int(NewTeam))); + Msg("%s *s %s", pPlayer->getName(), StringTable().translate("mp_switched_to").c_str(), CTeamInfo::GetTeam_name(int(NewTeam))); } break; @@ -436,7 +436,7 @@ void game_cl_TeamDeathmatch::shedule_Update(u32 dt) !local_player->testFlag(GAME_PLAYER_FLAG_VERY_VERY_DEAD)) { string1024 msg; - xr_sprintf(msg, *StringTable().translate("mp_press_to_buy"), "B"); + xr_sprintf(msg, StringTable().translate("mp_press_to_buy").c_str(), "B"); if (m_game_ui) m_game_ui->SetBuyMsgCaption(msg); m_bBuyEnabled = true; diff --git a/src/xrGame/game_sv_artefacthunt.cpp b/src/xrGame/game_sv_artefacthunt.cpp index cfaefe7ee67..61a0452e2cd 100644 --- a/src/xrGame/game_sv_artefacthunt.cpp +++ b/src/xrGame/game_sv_artefacthunt.cpp @@ -561,7 +561,7 @@ BOOL game_sv_ArtefactHunt::OnTouch(u16 eid_who, u16 eid_what, BOOL bForced) if (W) { //--------------------------------------------------------------- - if (IsBuyableItem(*e_what->s_name)) + if (IsBuyableItem(e_what->s_name.c_str())) return TRUE; //--------------------------------------------------------------- } @@ -1437,12 +1437,12 @@ void game_sv_ArtefactHunt::ReadOptions(shared_str& options) { inherited::ReadOptions(options); //------------------------------- - g_sv_ah_dwArtefactRespawnDelta = get_option_i(*options, "ardelta", g_sv_ah_dwArtefactRespawnDelta); // sec - g_sv_ah_dwArtefactsNum = get_option_i(*options, "anum", g_sv_ah_dwArtefactsNum); - g_sv_ah_dwArtefactStayTime = get_option_i(*options, "astime", g_sv_ah_dwArtefactStayTime); + g_sv_ah_dwArtefactRespawnDelta = get_option_i(options.c_str(), "ardelta", g_sv_ah_dwArtefactRespawnDelta); // sec + g_sv_ah_dwArtefactsNum = get_option_i(options.c_str(), "anum", g_sv_ah_dwArtefactsNum); + g_sv_ah_dwArtefactStayTime = get_option_i(options.c_str(), "astime", g_sv_ah_dwArtefactStayTime); g_sv_dm_dwFragLimit = 0; //---------------------------------------------------------------------------- - g_sv_ah_iReinforcementTime = get_option_i(*options, "reinf", g_sv_ah_iReinforcementTime); + g_sv_ah_iReinforcementTime = get_option_i(options.c_str(), "reinf", g_sv_ah_iReinforcementTime); if (g_sv_ah_iReinforcementTime < 0) g_sv_ah_iReinforcementTime = -1; //---------------------------------------------------------------------------- diff --git a/src/xrGame/game_sv_capture_the_artefact.cpp b/src/xrGame/game_sv_capture_the_artefact.cpp index ddc95ace7f3..f99dd96c07a 100644 --- a/src/xrGame/game_sv_capture_the_artefact.cpp +++ b/src/xrGame/game_sv_capture_the_artefact.cpp @@ -1279,7 +1279,7 @@ void game_sv_CaptureTheArtefact::SpawnWeaponsForActor(CSE_Abstract* pE, game_Pla { u16 ItemID = ps->pItemList.front(); SpawnWeapon4Actor( - pA->ID, *m_strWeaponsData->GetItemName(ItemID & 0x00FF), u8((ItemID & 0xFF00) >> 0x08), ps->pItemList); + pA->ID, m_strWeaponsData->GetItemName(ItemID & 0x00FF).c_str(), u8((ItemID & 0xFF00) >> 0x08), ps->pItemList); // Game().m_WeaponUsageStatistic->OnWeaponBought(ps, *m_strWeaponsData->GetItemName(ItemID& 0x00FF)); R_ASSERT(ps->pItemList.size()); ps->pItemList.erase(ps->pItemList.begin()); @@ -2453,37 +2453,37 @@ void game_sv_CaptureTheArtefact::ReadOptions(shared_str& options) inherited::ReadOptions(options); //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - g_sv_dm_bAnomaliesEnabled = (get_option_i(*options, "ans", (isAnomaliesEnabled() ? 1 : 0)) != 0); - g_sv_dm_dwAnomalySetLengthTime = get_option_i(*options, "anslen", g_sv_dm_dwAnomalySetLengthTime); // in (min) - g_sv_dm_bPDAHunt = (get_option_i(*options, "pdahunt", (isPDAHuntEnabled() ? 1 : 0)) != 0); - g_sv_dm_bDamageBlockIndicators = (get_option_i(*options, "dmbi", (g_sv_dm_bDamageBlockIndicators ? 1 : 0)) != 0); - g_sv_dm_dwWarmUp_MaxTime = get_option_i(*options, "warmup", g_sv_dm_dwWarmUp_MaxTime); + g_sv_dm_bAnomaliesEnabled = (get_option_i(options.c_str(), "ans", (isAnomaliesEnabled() ? 1 : 0)) != 0); + g_sv_dm_dwAnomalySetLengthTime = get_option_i(options.c_str(), "anslen", g_sv_dm_dwAnomalySetLengthTime); // in (min) + g_sv_dm_bPDAHunt = (get_option_i(options.c_str(), "pdahunt", (isPDAHuntEnabled() ? 1 : 0)) != 0); + g_sv_dm_bDamageBlockIndicators = (get_option_i(options.c_str(), "dmbi", (g_sv_dm_bDamageBlockIndicators ? 1 : 0)) != 0); + g_sv_dm_dwWarmUp_MaxTime = get_option_i(options.c_str(), "warmup", g_sv_dm_dwWarmUp_MaxTime); //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //------------------------------------------------------------------------- - g_sv_tdm_bAutoTeamBalance = get_option_i(*options, "abalance", (g_sv_tdm_bAutoTeamBalance ? 1 : 0)) != 0; - g_sv_tdm_bAutoTeamSwap = get_option_i(*options, "aswap", (g_sv_tdm_bAutoTeamSwap ? 1 : 0)) != 0; - g_sv_tdm_bFriendlyIndicators = get_option_i(*options, "fi", (g_sv_tdm_bFriendlyIndicators ? 1 : 0)) != 0; - g_sv_tdm_bFriendlyNames = get_option_i(*options, "fn", (g_sv_tdm_bFriendlyNames ? 1 : 0)) != 0; + g_sv_tdm_bAutoTeamBalance = get_option_i(options.c_str(), "abalance", (g_sv_tdm_bAutoTeamBalance ? 1 : 0)) != 0; + g_sv_tdm_bAutoTeamSwap = get_option_i(options.c_str(), "aswap", (g_sv_tdm_bAutoTeamSwap ? 1 : 0)) != 0; + g_sv_tdm_bFriendlyIndicators = get_option_i(options.c_str(), "fi", (g_sv_tdm_bFriendlyIndicators ? 1 : 0)) != 0; + g_sv_tdm_bFriendlyNames = get_option_i(options.c_str(), "fn", (g_sv_tdm_bFriendlyNames ? 1 : 0)) != 0; - float fFF = get_option_f(*options, "ffire", g_sv_tdm_fFriendlyFireModifier); + float fFF = get_option_f(options.c_str(), "ffire", g_sv_tdm_fFriendlyFireModifier); g_sv_tdm_fFriendlyFireModifier = fFF; //------------------------------------------------------------------------- - g_sv_ah_dwArtefactsNum = get_option_i(*options, "anum", g_sv_ah_dwArtefactsNum); + g_sv_ah_dwArtefactsNum = get_option_i(options.c_str(), "anum", g_sv_ah_dwArtefactsNum); //---------------------------------------------------------------------------- - g_sv_ah_iReinforcementTime = get_option_i(*options, "reinf", g_sv_ah_iReinforcementTime); + g_sv_ah_iReinforcementTime = get_option_i(options.c_str(), "reinf", g_sv_ah_iReinforcementTime); if (g_sv_ah_iReinforcementTime <= 0) g_sv_ah_iReinforcementTime = 1; //---------------------------------------------------------------------------- - g_sv_cta_dwInvincibleTime = get_option_i(*options, "dmgblock", g_sv_cta_dwInvincibleTime); // in (sec) - g_sv_cta_artefactReturningTime = get_option_i(*options, "artrettime", g_sv_cta_artefactReturningTime); // in (sec) - g_sv_cta_activatedArtefactRet = get_option_i(*options, "actret", g_sv_cta_activatedArtefactRet); // in (sec) + g_sv_cta_dwInvincibleTime = get_option_i(options.c_str(), "dmgblock", g_sv_cta_dwInvincibleTime); // in (sec) + g_sv_cta_artefactReturningTime = get_option_i(options.c_str(), "artrettime", g_sv_cta_artefactReturningTime); // in (sec) + g_sv_cta_activatedArtefactRet = get_option_i(options.c_str(), "actret", g_sv_cta_activatedArtefactRet); // in (sec) m_bSpectatorMode = false; - if (!GEnv.isDedicatedServer && (get_option_i(*options, "spectr", -1) != -1)) + if (!GEnv.isDedicatedServer && (get_option_i(options.c_str(), "spectr", -1) != -1)) { m_bSpectatorMode = true; - m_dwSM_SwitchDelta = get_option_i(*options, "spectr", 0) * 1000; + m_dwSM_SwitchDelta = get_option_i(options.c_str(), "spectr", 0) * 1000; if (m_dwSM_SwitchDelta < 1000) m_dwSM_SwitchDelta = 1000; }; diff --git a/src/xrGame/game_sv_deathmatch.cpp b/src/xrGame/game_sv_deathmatch.cpp index 978f132eb9f..85cd98390c9 100644 --- a/src/xrGame/game_sv_deathmatch.cpp +++ b/src/xrGame/game_sv_deathmatch.cpp @@ -1076,7 +1076,7 @@ void game_sv_Deathmatch::SpawnWeaponsForActor(CSE_Abstract* pE, game_PlayerState Msg("--- Server: spawning item [%d] for actor [%s]", ItemID, ps->getName()); #endif // #ifdef DEBUG SpawnWeapon4Actor( - pA->ID, *m_strWeaponsData->GetItemName(ItemID & 0x00FF), u8((ItemID & 0xFF00) >> 0x08), ps->pItemList); + pA->ID, m_strWeaponsData->GetItemName(ItemID & 0x00FF).c_str(), u8((ItemID & 0xFF00) >> 0x08), ps->pItemList); // Game().m_WeaponUsageStatistic->OnWeaponBought(ps, *m_strWeaponsData->GetItemName(ItemID& 0x00FF)); R_ASSERT(ps->pItemList.size()); ps->pItemList.erase(ps->pItemList.begin()); @@ -1729,7 +1729,7 @@ BOOL game_sv_Deathmatch::OnTouch(u16 eid_who, u16 eid_what, BOOL bForced) } }; //--------------------------------------------------------------- - if (IsBuyableItem(*e_what->s_name)) + if (IsBuyableItem(e_what->s_name.c_str())) return TRUE; //--------------------------------------------------------------- }; @@ -2169,28 +2169,28 @@ void game_sv_Deathmatch::ReadOptions(shared_str& options) { inherited::ReadOptions(options); //------------------------------- - g_sv_dm_dwForceRespawn = get_option_i(*options, "frcrspwn", g_sv_dm_dwForceRespawn); - g_sv_dm_dwFragLimit = get_option_i(*options, "fraglimit", g_sv_dm_dwFragLimit); - g_sv_dm_dwTimeLimit = get_option_i(*options, "timelimit", g_sv_dm_dwTimeLimit); // in (min) - g_sv_dm_dwDamageBlockTime = get_option_i(*options, "dmgblock", g_sv_dm_dwDamageBlockTime); // in (sec) + g_sv_dm_dwForceRespawn = get_option_i(options.c_str(), "frcrspwn", g_sv_dm_dwForceRespawn); + g_sv_dm_dwFragLimit = get_option_i(options.c_str(), "fraglimit", g_sv_dm_dwFragLimit); + g_sv_dm_dwTimeLimit = get_option_i(options.c_str(), "timelimit", g_sv_dm_dwTimeLimit); // in (min) + g_sv_dm_dwDamageBlockTime = get_option_i(options.c_str(), "dmgblock", g_sv_dm_dwDamageBlockTime); // in (sec) //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - g_sv_dm_bDamageBlockIndicators = (get_option_i(*options, "dmbi", (g_sv_dm_bDamageBlockIndicators ? 1 : 0)) != 0); + g_sv_dm_bDamageBlockIndicators = (get_option_i(options.c_str(), "dmbi", (g_sv_dm_bDamageBlockIndicators ? 1 : 0)) != 0); //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - g_sv_dm_bAnomaliesEnabled = (get_option_i(*options, "ans", (IsAnomaliesEnabled() ? 1 : 0)) != 0); - g_sv_dm_dwAnomalySetLengthTime = get_option_i(*options, "anslen", g_sv_dm_dwAnomalySetLengthTime); // in (min) + g_sv_dm_bAnomaliesEnabled = (get_option_i(options.c_str(), "ans", (IsAnomaliesEnabled() ? 1 : 0)) != 0); + g_sv_dm_dwAnomalySetLengthTime = get_option_i(options.c_str(), "anslen", g_sv_dm_dwAnomalySetLengthTime); // in (min) //----------------------------------------------------------------------- m_bSpectatorMode = false; - if (!GEnv.isDedicatedServer && (get_option_i(*options, "spectr", -1) != -1)) + if (!GEnv.isDedicatedServer && (get_option_i(options.c_str(), "spectr", -1) != -1)) { m_bSpectatorMode = true; - m_dwSM_SwitchDelta = get_option_i(*options, "spectr", 0) * 1000; + m_dwSM_SwitchDelta = get_option_i(options.c_str(), "spectr", 0) * 1000; if (m_dwSM_SwitchDelta < 1000) m_dwSM_SwitchDelta = 1000; }; //------------------------------------------------------------------------- - g_sv_dm_dwWarmUp_MaxTime = get_option_i(*options, "warmup", g_sv_dm_dwWarmUp_MaxTime); + g_sv_dm_dwWarmUp_MaxTime = get_option_i(options.c_str(), "warmup", g_sv_dm_dwWarmUp_MaxTime); - g_sv_dm_bPDAHunt = (get_option_i(*options, "pdahunt", (g_sv_dm_bPDAHunt ? 1 : 0)) != 0); + g_sv_dm_bPDAHunt = (get_option_i(options.c_str(), "pdahunt", (g_sv_dm_bPDAHunt ? 1 : 0)) != 0); }; [[maybe_unused]] void game_sv_Deathmatch::ConsoleCommands_Create() {} diff --git a/src/xrGame/game_sv_mp.cpp b/src/xrGame/game_sv_mp.cpp index d9be6fb1425..e3953ccbfd7 100644 --- a/src/xrGame/game_sv_mp.cpp +++ b/src/xrGame/game_sv_mp.cpp @@ -1346,7 +1346,7 @@ void game_sv_mp::SetPlayersDefItems(game_PlayerState* ps) continue; shared_str WeaponName = m_strWeaponsData->GetItemName((*pItemID) & 0x00FF); // strconcat(ItemStr, "def_item_repl_", pWpnS->WeaponName.c_str()); - strconcat(sizeof(ItemStr), ItemStr, "def_item_repl_", *WeaponName); + strconcat(sizeof(ItemStr), ItemStr, "def_item_repl_", WeaponName.c_str()); if (!pSettings->line_exist(RankStr, ItemStr)) continue; @@ -1369,7 +1369,7 @@ void game_sv_mp::SetPlayersDefItems(game_PlayerState* ps) continue; shared_str WeaponName = m_strWeaponsData->GetItemName((*pItemID) & 0x00FF); - if (!xr_strcmp(*WeaponName, "mp_wpn_knife")) + if (!xr_strcmp(WeaponName.c_str(), "mp_wpn_knife")) continue; u16 AmmoID = u16(-1); if (pSettings->line_exist(WeaponName, "ammo_class")) @@ -1773,14 +1773,14 @@ void game_sv_mp::ReadOptions(shared_str& options) inherited::ReadOptions(options); u8 SpectatorModes = SpectatorModes_Pack(); - SpectatorModes = u8(get_option_i(*options, "spectrmds", s32(SpectatorModes)) & 0x00ff); + SpectatorModes = u8(get_option_i(options.c_str(), "spectrmds", s32(SpectatorModes)) & 0x00ff); SpectatorModes_UnPack(SpectatorModes); - g_sv_dwMaxClientPing = get_option_i(*options, "maxping", g_sv_dwMaxClientPing); + g_sv_dwMaxClientPing = get_option_i(options.c_str(), "maxping", g_sv_dwMaxClientPing); string64 StartTime, TimeFactor; - xr_strcpy(StartTime, get_option_s(*options, "estime", "9:00")); - xr_strcpy(TimeFactor, get_option_s(*options, "etimef", "1")); + xr_strcpy(StartTime, get_option_s(options.c_str(), "estime", "9:00")); + xr_strcpy(TimeFactor, get_option_s(options.c_str(), "etimef", "1")); u32 hours = 0, mins = 0; sscanf(StartTime, "%d:%d", &hours, &mins); diff --git a/src/xrGame/game_sv_single.cpp b/src/xrGame/game_sv_single.cpp index c7be6bc8d7b..39412469c18 100644 --- a/src/xrGame/game_sv_single.cpp +++ b/src/xrGame/game_sv_single.cpp @@ -19,7 +19,7 @@ game_sv_Single::~game_sv_Single() { delete_data(m_alife_simulator); } void game_sv_Single::Create(shared_str& options) { inherited::Create(options); - if (strstr(*options, "/alife")) + if (strstr(options.c_str(), "/alife")) m_alife_simulator = xr_new(&server(), &options); switch_Phase(GAME_PHASE_INPROGRESS); } @@ -97,8 +97,8 @@ BOOL game_sv_Single::OnTouch(u16 eid_who, u16 eid_what, BOOL bForced) #ifdef DEBUG else if (psAI_Flags.test(aiALife)) { - Msg("Cannot attach object [%s][%s][%d] to object [%s][%s][%d]", e_what->name_replace(), *e_what->s_name, - e_what->ID, e_who->name_replace(), *e_who->s_name, e_who->ID); + Msg("Cannot attach object [%s][%s][%d] to object [%s][%s][%d]", e_what->name_replace(), e_what->s_name.c_str(), + e_what->ID, e_who->name_replace(), e_who->s_name.c_str(), e_who->ID); } #endif } @@ -146,7 +146,7 @@ void game_sv_Single::OnDetach(u16 eid_who, u16 eid_what) else if (psAI_Flags.test(aiALife)) { Msg("Cannot detach object [%s][%s][%d] from object [%s][%s][%d]", - l_tpALifeInventoryItem->base()->name_replace(), *l_tpALifeInventoryItem->base()->s_name, + l_tpALifeInventoryItem->base()->name_replace(), l_tpALifeInventoryItem->base()->s_name.c_str(), l_tpALifeInventoryItem->base()->ID, l_tpDynamicObject->base()->name_replace(), l_tpDynamicObject->base()->s_name.c_str(), l_tpDynamicObject->ID); } @@ -243,7 +243,7 @@ bool game_sv_Single::load_game(NET_Packet& net_packet, ClientID sender) return (inherited::load_game(net_packet, sender)); shared_str game_name; net_packet.r_stringZ(game_name); - return (alife().load_game(*game_name, true)); + return (alife().load_game(game_name.c_str(), true)); } void game_sv_Single::reload_game(NET_Packet& net_packet, ClientID sender) {} diff --git a/src/xrGame/game_sv_teamdeathmatch.cpp b/src/xrGame/game_sv_teamdeathmatch.cpp index 0739b1814d4..f255babd991 100644 --- a/src/xrGame/game_sv_teamdeathmatch.cpp +++ b/src/xrGame/game_sv_teamdeathmatch.cpp @@ -561,12 +561,12 @@ void game_sv_TeamDeathmatch::ReadOptions(shared_str& options) { inherited::ReadOptions(options); //------------------------------- - g_sv_tdm_bAutoTeamBalance = get_option_i(*options, "abalance", (g_sv_tdm_bAutoTeamBalance ? 1 : 0)) != 0; - g_sv_tdm_bAutoTeamSwap = get_option_i(*options, "aswap", (g_sv_tdm_bAutoTeamSwap ? 1 : 0)) != 0; - g_sv_tdm_bFriendlyIndicators = get_option_i(*options, "fi", (g_sv_tdm_bFriendlyIndicators ? 1 : 0)) != 0; - g_sv_tdm_bFriendlyNames = get_option_i(*options, "fn", (g_sv_tdm_bFriendlyNames ? 1 : 0)) != 0; + g_sv_tdm_bAutoTeamBalance = get_option_i(options.c_str(), "abalance", (g_sv_tdm_bAutoTeamBalance ? 1 : 0)) != 0; + g_sv_tdm_bAutoTeamSwap = get_option_i(options.c_str(), "aswap", (g_sv_tdm_bAutoTeamSwap ? 1 : 0)) != 0; + g_sv_tdm_bFriendlyIndicators = get_option_i(options.c_str(), "fi", (g_sv_tdm_bFriendlyIndicators ? 1 : 0)) != 0; + g_sv_tdm_bFriendlyNames = get_option_i(options.c_str(), "fn", (g_sv_tdm_bFriendlyNames ? 1 : 0)) != 0; - float fFF = get_option_f(*options, "ffire", g_sv_tdm_fFriendlyFireModifier); + float fFF = get_option_f(options.c_str(), "ffire", g_sv_tdm_fFriendlyFireModifier); g_sv_tdm_fFriendlyFireModifier = fFF; } diff --git a/src/xrGame/inventory_item_upgrade.cpp b/src/xrGame/inventory_item_upgrade.cpp index c5439654de0..0e5f7895e71 100644 --- a/src/xrGame/inventory_item_upgrade.cpp +++ b/src/xrGame/inventory_item_upgrade.cpp @@ -170,7 +170,7 @@ void CInventoryItem::net_Spawn_install_upgrades(CSE_Abstract* DC) // net_Spawn for (const auto& upgrade : saved_upgrades) { - ai().alife().inventory_upgrade_manager().upgrade_install(*this, *upgrade, true); + ai().alife().inventory_upgrade_manager().upgrade_install(*this, upgrade.c_str(), true); } } diff --git a/src/xrGame/inventory_owner_info.cpp b/src/xrGame/inventory_owner_info.cpp index 08df5e1df32..b1fbe594825 100644 --- a/src/xrGame/inventory_owner_info.cpp +++ b/src/xrGame/inventory_owner_info.cpp @@ -54,7 +54,7 @@ bool CInventoryOwner::OnReceiveInfo(shared_str info_id) const #ifdef DEBUG if (psAI_Flags.test(aiInfoPortion)) - Msg("[%s] Received Info [%s]", Name(), *info_id); + Msg("[%s] Received Info [%s]", Name(), info_id.c_str()); #endif //Запустить скриптовый callback diff --git a/src/xrGame/item_manager.cpp b/src/xrGame/item_manager.cpp index 14cd72bce3f..5244a7863b0 100644 --- a/src/xrGame/item_manager.cpp +++ b/src/xrGame/item_manager.cpp @@ -72,8 +72,8 @@ bool CItemManager::useful(const CGameObject* object) const float CItemManager::do_evaluate(const CGameObject* object) const { - VERIFY3(m_object->movement().restrictions().accessible(object->ai_location().level_vertex_id()), *m_object->cName(), - *object->cName()); + VERIFY3(m_object->movement().restrictions().accessible(object->ai_location().level_vertex_id()), m_object->cName().c_str(), + object->cName().c_str()); return (m_object->evaluate(this, object)); } @@ -94,13 +94,13 @@ void CItemManager::update() OBJECTS::const_iterator E = m_objects.end(); for (; I != E; ++I) VERIFY3(m_object->movement().restrictions().accessible((*I)->ai_location().level_vertex_id()), - *m_object->cName(), *(*I)->cName()); + m_object->cName().c_str(), (*I)->cName().c_str()); #endif // DEBUG inherited::update(); VERIFY3(!selected() || m_object->movement().restrictions().accessible(selected()->ai_location().level_vertex_id()), - *m_object->cName(), selected() ? *selected()->cName() : ""); + m_object->cName().c_str(), selected() ? selected()->cName().c_str() : ""); STOP_PROFILE } diff --git a/src/xrGame/level_debug.cpp b/src/xrGame/level_debug.cpp index 914410d7e01..bd765ea80ca 100644 --- a/src/xrGame/level_debug.cpp +++ b/src/xrGame/level_debug.cpp @@ -258,7 +258,7 @@ struct DrawInfoPredicate UI().Font().pFontMedium->SetAligment(CGameFont::alLeft); UI().Font().pFontMedium->SetColor(s.color); UI().Font().pFontMedium->OutSet(x, y -= delta_height); - UI().Font().pFontMedium->OutNext(*(s.text)); + UI().Font().pFontMedium->OutNext(s.text.c_str()); } }; @@ -285,7 +285,7 @@ struct DrawTextPredicate UI().Font().pFontMedium->SetAligment(CGameFont::alLeft); UI().Font().pFontMedium->SetColor(s.color); UI().Font().pFontMedium->OutSet(s.x, s.y); - UI().Font().pFontMedium->OutNext(*(s.text)); + UI().Font().pFontMedium->OutNext(s.text.c_str()); } }; diff --git a/src/xrGame/level_script.cpp b/src/xrGame/level_script.cpp index bc74c3e6366..204941cf8dd 100644 --- a/src/xrGame/level_script.cpp +++ b/src/xrGame/level_script.cpp @@ -94,7 +94,7 @@ CScriptGameObject* get_object_by_id(u16 id) return pGameObject->lua_game_object(); } -LPCSTR get_weather() { return *g_pGamePersistent->Environment().GetWeather(); } +LPCSTR get_weather() { return g_pGamePersistent->Environment().GetWeather().c_str(); } void set_weather(pcstr const weather_name, const bool forced) { if (!Device.editor_mode()) @@ -571,7 +571,7 @@ void stop_tutorial() g_tutorial->Stop(); } -LPCSTR translate_string(LPCSTR str) { return *StringTable().translate(str); } +LPCSTR translate_string(LPCSTR str) { return StringTable().translate(str).c_str(); } bool has_active_tutotial() { return (g_tutorial != NULL); } // Alundaio: namespace level exports extension diff --git a/src/xrGame/level_sounds.cpp b/src/xrGame/level_sounds.cpp index 1edcfe201da..0417482447a 100644 --- a/src/xrGame/level_sounds.cpp +++ b/src/xrGame/level_sounds.cpp @@ -207,7 +207,7 @@ void CLevelSoundManager::Load() for (; it != end; ++it) { m_MusicTracks.push_back(SMusicTrack()); - m_MusicTracks.back().Load(*it->first, *it->second); + m_MusicTracks.back().Load(it->first.c_str(), it->second.c_str()); } } } diff --git a/src/xrGame/map_location.cpp b/src/xrGame/map_location.cpp index 219b4a69185..596a82e6d54 100644 --- a/src/xrGame/map_location.cpp +++ b/src/xrGame/map_location.cpp @@ -234,7 +234,7 @@ void CMapLocation::InitUserSpot(const shared_str& level_name, const Fvector& pos if (ai().get_alife()) { - const CGameGraph::SLevel& level = ai().game_graph().header().level(*level_name); + const CGameGraph::SLevel& level = ai().game_graph().header().level(level_name.c_str()); float min_dist = 128; // flt_max; GameGraph::_GRAPH_ID n = ai().game_graph().header().vertex_count(); @@ -253,7 +253,7 @@ void CMapLocation::InitUserSpot(const shared_str& level_name, const Fvector& pos } if (!ai().game_graph().vertex(m_cached.m_graphID)) { - Msg("qweasdd! Cannot assign game vertex for CUserDefinedMapLocation [map=%s]", *level_name); + Msg("qweasdd! Cannot assign game vertex for CUserDefinedMapLocation [map=%s]", level_name.c_str()); R_ASSERT(ai().game_graph().vertex(m_cached.m_graphID)); } } @@ -504,7 +504,7 @@ void CMapLocation::UpdateSpot(CUICustomMap* map, CMapSpot* sp) { // Msg("%d-%s",(*it),ai().game_graph().vertex(*it)); Msg("[%d] level[%s]", (*it), - *ai().game_graph().header().level(ai().game_graph().vertex(*it)->level_id()).name()); + ai().game_graph().header().level(ai().game_graph().vertex(*it)->level_id()).name().c_str()); } Msg("- Available LevelChangers:"); xr_vector::iterator lit, lit_e; @@ -515,7 +515,7 @@ void CMapLocation::UpdateSpot(CUICustomMap* map, CMapSpot* sp) Msg("[%d]", gid); Fvector p = ai().game_graph().vertex(gid)->level_point(); Msg("lch_name=%s pos=%f %f %f", - *ai().game_graph().header().level(ai().game_graph().vertex(gid)->level_id()).name(), p.x, p.y, + ai().game_graph().header().level(ai().game_graph().vertex(gid)->level_id()).name().c_str(), p.x, p.y, p.z); } }; @@ -532,7 +532,7 @@ void CMapLocation::UpdateSpot(CUICustomMap* map, CMapSpot* sp) xr_vector::reverse_iterator it_e = map_point_path.rend(); for (; (it != it_e) && (!bDone); ++it) { - if (*ai().game_graph().header().level(ai().game_graph().vertex(*it)->level_id()).name() == + if (ai().game_graph().header().level(ai().game_graph().vertex(*it)->level_id()).name().c_str() == Level().name()) break; } @@ -771,7 +771,7 @@ Fvector2 CMapLocation::SpotSize() } CRelationMapLocation::CRelationMapLocation(const shared_str& type, u16 object_id, u16 pInvOwnerActorID) - : CMapLocation(*type, object_id), m_last_relation() + : CMapLocation(type.c_str(), object_id), m_last_relation() { m_curr_spot_name = type; m_pInvOwnerActorID = pInvOwnerActorID; @@ -829,7 +829,7 @@ bool CRelationMapLocation::Update() if (m_curr_spot_name != sname) { - LoadSpot(*sname); + LoadSpot(sname.c_str()); m_curr_spot_name = sname; } // update visibility @@ -924,6 +924,6 @@ void CRelationMapLocation::UpdateLevelMap(CUICustomMap* map) void CRelationMapLocation::Dump() { inherited::Dump(); - Msg("--CRelationMapLocation m_curr_spot_name=[%s]", *m_curr_spot_name); + Msg("--CRelationMapLocation m_curr_spot_name=[%s]", m_curr_spot_name.c_str()); } #endif diff --git a/src/xrGame/map_manager.cpp b/src/xrGame/map_manager.cpp index 132c09cd7f3..f0c7a262b05 100644 --- a/src/xrGame/map_manager.cpp +++ b/src/xrGame/map_manager.cpp @@ -49,7 +49,7 @@ void SLocationKey::load(IReader& stream) stream.r_stringZ(spot_type); stream.r_u8(); - location = xr_new(*spot_type, object_id); + location = xr_new(spot_type.c_str(), object_id); location->load(stream); } @@ -293,7 +293,7 @@ void CMapManager::Dump() auto it_e = Locations().end(); for (; it != it_e; ++it) { - Msg("spot_type=[%s] object_id=[%d]", *((*it).spot_type), (*it).object_id); + Msg("spot_type=[%s] object_id=[%d]", (*it).spot_type.c_str(), (*it).object_id); (*it).location->Dump(); } diff --git a/src/xrGame/material_manager.cpp b/src/xrGame/material_manager.cpp index cc386dbdc72..5df8bcc9c4e 100644 --- a/src/xrGame/material_manager.cpp +++ b/src/xrGame/material_manager.cpp @@ -34,7 +34,7 @@ BOOL debug_character_material_load = FALSE; void CMaterialManager::Load(LPCSTR section) { R_ASSERT3( - pSettings->line_exist(section, "material"), "Material not found in the section ", *(m_object->cNameSect())); + pSettings->line_exist(section, "material"), "Material not found in the section ", m_object->cNameSect().c_str()); m_my_material_idx = GMLib.GetMaterialIdx(pSettings->r_string(section, "material")); #ifdef DEBUG @@ -85,7 +85,7 @@ void CMaterialManager::update(float time_delta, float volume, float step_time, b VERIFY(GAMEMTL_NONE_IDX != m_my_material_idx); VERIFY(GAMEMTL_NONE_IDX != m_last_material_idx); SGameMtlPair* mtl_pair = GMLib.GetMaterialPairByIndices(m_my_material_idx, m_last_material_idx); - VERIFY3(mtl_pair, "Undefined material pair: ", *GMLib.GetMaterialByIdx(m_last_material_idx)->m_Name); + VERIFY3(mtl_pair, "Undefined material pair: ", GMLib.GetMaterialByIdx(m_last_material_idx)->m_Name.c_str()); Fvector position = m_object->Position(); if (m_movement_control->CharacterExist()) { diff --git a/src/xrGame/movement_manager.cpp b/src/xrGame/movement_manager.cpp index b166372312f..956405808e8 100644 --- a/src/xrGame/movement_manager.cpp +++ b/src/xrGame/movement_manager.cpp @@ -131,7 +131,7 @@ GameGraph::_GRAPH_ID CMovementManager::game_dest_vertex_id() const void CMovementManager::set_level_dest_vertex(u32 const& level_vertex_id) { - VERIFY2(restrictions().accessible(level_vertex_id), *object().cName()); + VERIFY2(restrictions().accessible(level_vertex_id), object().cName().c_str()); level_path().set_dest_vertex(level_vertex_id); m_path_actuality = m_path_actuality && level_path().actual(); } diff --git a/src/xrGame/moving_objects.cpp b/src/xrGame/moving_objects.cpp index 8529493cbe4..7aaf60d82e5 100644 --- a/src/xrGame/moving_objects.cpp +++ b/src/xrGame/moving_objects.cpp @@ -24,7 +24,7 @@ void moving_objects::on_level_load() void moving_objects::register_object(moving_object* moving_object) { VERIFY2(m_objects.find(moving_object) == m_objects.end(), - make_string("moving object %s is registers twice", *moving_object->id())); + make_string("moving object %s is registers twice", moving_object->id().c_str())); #ifdef DEBUG m_objects.insert(moving_object); @@ -37,7 +37,7 @@ void moving_objects::register_object(moving_object* moving_object) void moving_objects::unregister_object(moving_object* moving_object) { VERIFY2(m_objects.find(moving_object) != m_objects.end(), - make_string("moving object %s is not yet registered or unregisters twice", *moving_object->id())); + make_string("moving object %s is not yet registered or unregisters twice", moving_object->id().c_str())); #ifdef DEBUG m_objects.erase(m_objects.find(moving_object)); @@ -50,7 +50,7 @@ void moving_objects::unregister_object(moving_object* moving_object) void moving_objects::on_object_move(moving_object* moving_object) { VERIFY2(m_objects.find(moving_object) != m_objects.end(), - make_string("moving object %s is not yet registered", *moving_object->id())); + make_string("moving object %s is not yet registered", moving_object->id().c_str())); #pragma todo("this place can be optimized in case of slowdowns") VERIFY(m_tree); diff --git a/src/xrGame/moving_objects_dynamic_collision.cpp b/src/xrGame/moving_objects_dynamic_collision.cpp index 5ec59480ef0..c037dc5ce65 100644 --- a/src/xrGame/moving_objects_dynamic_collision.cpp +++ b/src/xrGame/moving_objects_dynamic_collision.cpp @@ -206,12 +206,12 @@ void moving_objects::resolve_collision(boxes& current, moving_object* object0, c } #endif // 0 VERIFY2( - object0->action_frame() != Device.dwFrame, make_string("%d %s", Device.dwFrame, *object0->object().cName())); - VERIFY2(object0->action_frame() < Device.dwFrame, make_string("%d %s", Device.dwFrame, *object0->object().cName())); + object0->action_frame() != Device.dwFrame, make_string("%d %s", Device.dwFrame, object0->object().cName().c_str())); + VERIFY2(object0->action_frame() < Device.dwFrame, make_string("%d %s", Device.dwFrame, object0->object().cName().c_str())); VERIFY2( - object1->action_frame() != Device.dwFrame, make_string("%d %s", Device.dwFrame, *object0->object().cName())); - VERIFY2(object1->action_frame() < Device.dwFrame, make_string("%d %s", Device.dwFrame, *object0->object().cName())); + object1->action_frame() != Device.dwFrame, make_string("%d %s", Device.dwFrame, object0->object().cName().c_str())); + VERIFY2(object1->action_frame() < Device.dwFrame, make_string("%d %s", Device.dwFrame, object0->object().cName().c_str())); bool first_time = (std::find_if(m_previous_collisions.begin(), m_previous_collisions.end(), collision_predicate(std::make_pair(object0, object1))) == m_previous_collisions.end()); @@ -235,7 +235,7 @@ void moving_objects::resolve_collision(boxes& current, moving_object* object0, c return; } - VERIFY2(false, make_string("NODEFAULT: [%s][%s]", *object0->object().cName(), *object1->object().cName())); + VERIFY2(false, make_string("NODEFAULT: [%s][%s]", object0->object().cName().c_str(), object1->object().cName().c_str())); } bool moving_objects::collided_dynamic(moving_object* object0, const Fvector& position0, moving_object* object1, diff --git a/src/xrGame/object_handler.cpp b/src/xrGame/object_handler.cpp index 7ecc3d4b2e5..0662efb94f1 100644 --- a/src/xrGame/object_handler.cpp +++ b/src/xrGame/object_handler.cpp @@ -42,9 +42,9 @@ void CObjectHandler::reinit(CAI_Stalker* object) m_hammer_is_clutched = false; planner().setup(object); IKinematics* kinematics = smart_cast(planner().m_object->Visual()); - m_r_hand = kinematics->LL_BoneID(pSettings->r_string(*planner().m_object->cNameSect(), "weapon_bone0")); - m_l_finger1 = kinematics->LL_BoneID(pSettings->r_string(*planner().m_object->cNameSect(), "weapon_bone1")); - m_r_finger2 = kinematics->LL_BoneID(pSettings->r_string(*planner().m_object->cNameSect(), "weapon_bone2")); + m_r_hand = kinematics->LL_BoneID(pSettings->r_string(planner().m_object->cNameSect().c_str(), "weapon_bone0")); + m_l_finger1 = kinematics->LL_BoneID(pSettings->r_string(planner().m_object->cNameSect().c_str(), "weapon_bone1")); + m_r_finger2 = kinematics->LL_BoneID(pSettings->r_string(planner().m_object->cNameSect().c_str(), "weapon_bone2")); m_strap_object_id = ALife::_OBJECT_ID(-1); m_strap_bone0 = -1; m_strap_bone1 = -1; @@ -103,7 +103,7 @@ void CObjectHandler::OnItemDrop(CInventoryItem* inventory_item, bool just_before CWeaponAmmo* weapon_ammo = smart_cast(inventory_item); if (weapon_ammo) { - Level().spawn_item(*weapon_ammo->cNameSect(), planner().object().Position(), + Level().spawn_item(weapon_ammo->cNameSect().c_str(), planner().object().Position(), planner().object().ai_location().level_vertex_id(), planner().object().ID()); m_item_to_spawn = weapon_ammo->cNameSect(); m_ammo_in_box_to_spawn = weapon_ammo->m_boxSize; @@ -159,7 +159,7 @@ void CObjectHandler::weapon_bones(int& b0, int& b1, int& b2) const return; } - THROW3(weapon->can_be_strapped(), "Cannot strap weapon", *weapon->cName()); + THROW3(weapon->can_be_strapped(), "Cannot strap weapon", weapon->cName().c_str()); if (weapon->ID() != m_strap_object_id) { @@ -196,7 +196,7 @@ void CObjectHandler::actualize_strap_mode(CWeapon* weapon) const return; } - THROW3(weapon->can_be_strapped(), "Cannot strap weapon", *weapon->cName()); + THROW3(weapon->can_be_strapped(), "Cannot strap weapon", weapon->cName().c_str()); // Msg ( "[%6d][%s] actualizing: weapon_strapped = true", Device.dwTimeGlobal, // planner().object().cName().c_str() ); weapon->strapped_mode(true); diff --git a/src/xrGame/object_handler_planner.cpp b/src/xrGame/object_handler_planner.cpp index 2e854b5be80..3c41c718b24 100644 --- a/src/xrGame/object_handler_planner.cpp +++ b/src/xrGame/object_handler_planner.cpp @@ -70,7 +70,7 @@ void CObjectHandlerPlanner::set_goal(MonsterSpace::EObjectAction object_action, if (m_use_log) { Msg("%6d : Active item %s", Device.dwTimeGlobal, object().inventory().ActiveItem() ? - *object().inventory().ActiveItem()->object().cName() : + object().inventory().ActiveItem()->object().cName().c_str() : "no active items"); Msg("%6d : Goal %s", Device.dwTimeGlobal, property2string(condition_id)); } @@ -121,7 +121,7 @@ LPCSTR CObjectHandlerPlanner::action2string(const _action_id_type& id) pstr S = m_temp_string; if (action_object_id(id) != 0xffff) if (Level().Objects.net_Find(action_object_id(id))) - xr_strcpy(m_temp_string, *Level().Objects.net_Find(action_object_id(id))->cName()); + xr_strcpy(m_temp_string, Level().Objects.net_Find(action_object_id(id))->cName().c_str()); else xr_strcpy(m_temp_string, "no_items"); else @@ -313,7 +313,7 @@ LPCSTR CObjectHandlerPlanner::property2string(const _condition_type& id) { if (action_object_id(id) != 0xffff) if (Level().Objects.net_Find(action_object_id(id))) - xr_strcpy(m_temp_string, *Level().Objects.net_Find(action_object_id(id))->cName()); + xr_strcpy(m_temp_string, Level().Objects.net_Find(action_object_id(id))->cName().c_str()); else xr_strcpy(m_temp_string, "no_items"); else diff --git a/src/xrGame/patrol_path_manager.cpp b/src/xrGame/patrol_path_manager.cpp index c1283d80be2..103ee8c2c10 100644 --- a/src/xrGame/patrol_path_manager.cpp +++ b/src/xrGame/patrol_path_manager.cpp @@ -32,20 +32,20 @@ static void show_restrictions(LPCSTR restrictions) bool show_restrictions(CRestrictedObject* object) { Msg("DEFAULT OUT RESTRICTIONS :"); - show_restrictions(*Level().space_restriction_manager().default_out_restrictions() ? - *Level().space_restriction_manager().default_out_restrictions() : + show_restrictions(Level().space_restriction_manager().default_out_restrictions().c_str() ? + Level().space_restriction_manager().default_out_restrictions().c_str() : ""); Msg("DEFAULT IN RESTRICTIONS :"); - show_restrictions(*Level().space_restriction_manager().default_in_restrictions() ? - *Level().space_restriction_manager().default_in_restrictions() : + show_restrictions(Level().space_restriction_manager().default_in_restrictions().c_str() ? + Level().space_restriction_manager().default_in_restrictions().c_str() : ""); Msg("OUT RESTRICTIONS :"); - show_restrictions(*object->out_restrictions() ? *object->out_restrictions() : ""); + show_restrictions(object->out_restrictions().c_str() ? object->out_restrictions().c_str() : ""); Msg("IN RESTRICTIONS :"); - show_restrictions(*object->in_restrictions() ? *object->in_restrictions() : ""); + show_restrictions(object->in_restrictions().c_str() ? object->in_restrictions().c_str() : ""); return (false); } @@ -106,26 +106,26 @@ void CPatrolPathManager::select_point(const Fvector& position, u32& dest_vertex_ case ePatrolStartTypeFirst: { vertex = m_path->vertex(0); - VERIFY3(accessible(vertex) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(accessible(vertex) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); break; } case ePatrolStartTypeLast: { vertex = m_path->vertex(m_path->vertices().size() - 1); - VERIFY3(accessible(vertex) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(accessible(vertex) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); break; } case ePatrolStartTypeNearest: { vertex = m_path->point(position, CAccessabilityEvaluator(this)); - VERIFY3(accessible(vertex) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(accessible(vertex) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); break; } case ePatrolStartTypePoint: { - VERIFY3(m_path->vertex(m_start_point_index), *m_path_name, *m_game_object->cName()); + VERIFY3(m_path->vertex(m_start_point_index), m_path_name.c_str(), m_game_object->cName().c_str()); vertex = m_path->vertex(m_start_point_index); - VERIFY3(accessible(vertex) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(accessible(vertex) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); break; } case ePatrolStartTypeNext: @@ -149,7 +149,7 @@ void CPatrolPathManager::select_point(const Fvector& position, u32& dest_vertex_ if (!vertex) vertex = m_path->point(position, CAccessabilityEvaluator(this)); - VERIFY3(accessible(vertex) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(accessible(vertex) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); break; } default: NODEFAULT; @@ -158,15 +158,15 @@ void CPatrolPathManager::select_point(const Fvector& position, u32& dest_vertex_ { // ugly HACK, just because Plecha asked... VERIFY2(vertex || show_restrictions(m_object), - make_string("any vertex in patrol path [%s] is inaccessible for object [%s]", *m_path_name, - *m_game_object->cName())); + make_string("any vertex in patrol path [%s] is inaccessible for object [%s]", m_path_name.c_str(), + m_game_object->cName().c_str())); dest_vertex_id = m_game_object->ai_location().level_vertex_id(); return; } R_ASSERT2(ai().level_graph().valid_vertex_id(vertex->data().level_vertex_id()), - make_string("patrol path[%s], point on path [%s],object [%s]", *m_path_name, *vertex->data().name(), - *m_game_object->cName())); + make_string("patrol path[%s], point on path [%s],object [%s]", m_path_name.c_str(), vertex->data().name().c_str(), + m_game_object->cName().c_str())); if (!m_path->vertex(m_prev_point_index)) m_prev_point_index = vertex->vertex_id(); @@ -196,7 +196,7 @@ void CPatrolPathManager::select_point(const Fvector& position, u32& dest_vertex_ } #endif } - VERIFY3(m_path->vertex(m_curr_point_index) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(m_path->vertex(m_curr_point_index) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); m_game_object->callback(GameObject::ePatrolPathInPoint)( m_game_object->lua_game_object(), u32(ScriptEntity::eActionTypeMovement), m_curr_point_index); @@ -279,13 +279,13 @@ void CPatrolPathManager::select_point(const Fvector& position, u32& dest_vertex_ } } - VERIFY3(m_path->vertex(target) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(m_path->vertex(target) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); m_prev_point_index = m_curr_point_index; m_curr_point_index = target; dest_vertex_id = m_path->vertex(m_curr_point_index)->data().level_vertex_id(); m_dest_position = m_path->vertex(m_curr_point_index)->data().position(); - VERIFY3(accessible(m_dest_position) || show_restrictions(m_object), *m_path_name, *m_game_object->cName()); + VERIFY3(accessible(m_dest_position) || show_restrictions(m_object), m_path_name.c_str(), m_game_object->cName().c_str()); m_actuality = true; m_completed = false; } @@ -343,7 +343,7 @@ shared_str CPatrolPathManager::path_name() const if (!m_path) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "Path not specified (object %s)!", *m_game_object->cName()); + LuaMessageType::Error, "Path not specified (object %s)!", m_game_object->cName().c_str()); return (""); } VERIFY(m_path); @@ -355,14 +355,14 @@ void CPatrolPathManager::set_previous_point(int point_index) if (!m_path) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "Path not specified (object %s)!", *m_game_object->cName()); + LuaMessageType::Error, "Path not specified (object %s)!", m_game_object->cName().c_str()); return; } if (!m_path->vertex(point_index)) { GEnv.ScriptEngine->script_log(LuaMessageType::Error, "Start point violates path bounds %s (object %s)!", - *m_path_name, *m_game_object->cName()); + m_path_name.c_str(), m_game_object->cName().c_str()); return; } VERIFY(m_path); @@ -375,13 +375,13 @@ void CPatrolPathManager::set_start_point(int point_index) if (!m_path) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "Path not specified (object %s)!", *m_game_object->cName()); + LuaMessageType::Error, "Path not specified (object %s)!", m_game_object->cName().c_str()); return; } if (!m_path->vertex(point_index)) { GEnv.ScriptEngine->script_log(LuaMessageType::Error, "Start point violates path bounds %s (object %s)!", - *m_path_name, *m_game_object->cName()); + m_path_name.c_str(), m_game_object->cName().c_str()); return; } VERIFY(m_path); diff --git a/src/xrGame/physics_game.cpp b/src/xrGame/physics_game.cpp index 51c324f059b..ffc727b01d7 100644 --- a/src/xrGame/physics_game.cpp +++ b/src/xrGame/physics_game.cpp @@ -254,7 +254,7 @@ void TContactShotMark(CDB::TRI* T, dContactGeom* c) { SGameMtl* static_mtl = GMLib.GetMaterialByIdx(T->material); VERIFY(static_mtl); - LPCSTR ps_name = *mtl_pair->CollideParticles[::Random.randI(0, mtl_pair->CollideParticles.size())]; + LPCSTR ps_name = mtl_pair->CollideParticles[::Random.randI(0, mtl_pair->CollideParticles.size())].c_str(); play_particles(vel_cret, data, c, b_invert_normal, static_mtl, ps_name); } } diff --git a/src/xrGame/purchase_list.cpp b/src/xrGame/purchase_list.cpp index cce4ab77cd0..1242c363e77 100644 --- a/src/xrGame/purchase_list.cpp +++ b/src/xrGame/purchase_list.cpp @@ -48,8 +48,8 @@ void CPurchaseList::process(CInifile& ini_file, LPCSTR section, CInventoryOwner& void CPurchaseList::process( const CGameObject& owner, const shared_str& name, const u32& count, const float& probability) { - VERIFY3(count, "Invalid count for section in the purchase list", *name); - VERIFY3(!fis_zero(probability, EPS_S), "Invalid probability for section in the purchase list", *name); + VERIFY3(count, "Invalid count for section in the purchase list", name.c_str()); + VERIFY3(!fis_zero(probability, EPS_S), "Invalid probability for section in the purchase list", name.c_str()); const Fvector& position = owner.Position(); const u32& level_vertex_id = owner.ai_location().level_vertex_id(); @@ -62,7 +62,7 @@ void CPurchaseList::process( continue; ++j; - Level().spawn_item(*name, position, level_vertex_id, id, false); + Level().spawn_item(name.c_str(), position, level_vertex_id, id, false); } VERIFY3(m_deficits.find(name) == m_deficits.end(), "Duplicate section in the purchase list", name.c_str()); diff --git a/src/xrGame/restricted_object.cpp b/src/xrGame/restricted_object.cpp index 66796fe71ef..599ebbaab53 100644 --- a/src/xrGame/restricted_object.cpp +++ b/src/xrGame/restricted_object.cpp @@ -71,8 +71,8 @@ BOOL CRestrictedObject::net_Spawn(CSE_Abstract* data) string4096 temp0; string4096 temp1; - xr_strcpy(temp0, *monster->m_out_space_restrictors); - xr_strcpy(temp1, *monster->m_in_space_restrictors); + xr_strcpy(temp0, monster->m_out_space_restrictors.c_str()); + xr_strcpy(temp1, monster->m_in_space_restrictors.c_str()); if (ai().get_alife()) { @@ -266,7 +266,7 @@ IC void CRestrictedObject::construct_restriction_string(pstr temp_restrictions, for (; I != E; ++I) { IGameObject* object = Level().Objects.net_Find(*I); - if (!object || !!strstr(*current_restrictions, *object->cName()) == value) + if (!object || !!strstr(current_restrictions.c_str(), object->cName().c_str()) == value) continue; p(this, object->ID()); @@ -274,7 +274,7 @@ IC void CRestrictedObject::construct_restriction_string(pstr temp_restrictions, if (count) xr_strcat(temp_restrictions, temp_restrictions_size, ","); - xr_strcat(temp_restrictions, temp_restrictions_size, *object->cName()); + xr_strcat(temp_restrictions, temp_restrictions_size, object->cName().c_str()); count++; } @@ -355,7 +355,7 @@ void CRestrictedObject::add_restrictions(const shared_str& out_restrictions, con START_PROFILE("Restricted Object/Add Restrictions"); - Level().space_restriction_manager().add_restrictions(object().ID(), *out_restrictions, *in_restrictions); + Level().space_restriction_manager().add_restrictions(object().ID(), out_restrictions.c_str(), in_restrictions.c_str()); actual(false); @@ -369,7 +369,7 @@ void CRestrictedObject::remove_restrictions(const shared_str& out_restrictions, START_PROFILE("Restricted Object/Remove Restrictions"); - Level().space_restriction_manager().remove_restrictions(object().ID(), *out_restrictions, *in_restrictions); + Level().space_restriction_manager().remove_restrictions(object().ID(), out_restrictions.c_str(), in_restrictions.c_str()); actual(false); diff --git a/src/xrGame/script_entity.cpp b/src/xrGame/script_entity.cpp index f2e0aca6caa..f4819df3938 100644 --- a/src/xrGame/script_entity.cpp +++ b/src/xrGame/script_entity.cpp @@ -92,7 +92,7 @@ void CScriptEntity::reinit() void CScriptEntity::SetScriptControl(const bool bScriptControl, shared_str caSciptName) { if (!(((m_bScriptControl && !bScriptControl) || (!m_bScriptControl && bScriptControl)) && - (bScriptControl || (xr_strlen(*m_caScriptName) && !xr_strcmp(caSciptName, m_caScriptName))))) + (bScriptControl || (xr_strlen(m_caScriptName.c_str()) && !xr_strcmp(caSciptName, m_caScriptName))))) { GEnv.ScriptEngine->script_log( LuaMessageType::Error, "Invalid sequence of taking an entity under script control"); @@ -124,7 +124,7 @@ void CScriptEntity::SetScriptControl(const bool bScriptControl, shared_str caSci } bool CScriptEntity::GetScriptControl() const { return (m_bScriptControl); } -LPCSTR CScriptEntity::GetScriptControlName() const { return (*m_caScriptName); } +LPCSTR CScriptEntity::GetScriptControlName() const { return (m_caScriptName.c_str()); } bool CScriptEntity::CheckObjectVisibility(const CGameObject* tpObject) { if (!m_monster) @@ -145,7 +145,7 @@ bool CScriptEntity::CheckTypeVisibility(const char* section_name) for (; I != E; ++I) { VERIFY((*I).m_object); - if (!xr_strcmp(section_name, *(*I).m_object->cNameSect())) + if (!xr_strcmp(section_name, (*I).m_object->cNameSect().c_str())) return (true); } return (false); @@ -272,7 +272,7 @@ void CScriptEntity::ProcessScripts() #ifdef DEBUG if (empty_queue) GEnv.ScriptEngine->script_log( - LuaMessageType::Info, "Object %s has an empty script queue!", *object().cName()); + LuaMessageType::Info, "Object %s has an empty script queue!", object().cName().c_str()); #endif return; } @@ -345,7 +345,7 @@ bool CScriptEntity::bfAssignAnimation(CScriptEntityAction* tpEntityAction) return (true); IKinematicsAnimated& tVisualObject = *(smart_cast(object().Visual())); - m_tpNextAnimation = tVisualObject.ID_Cycle_Safe(*GetCurrentAction()->m_tAnimationAction.m_caAnimationToPlay); + m_tpNextAnimation = tVisualObject.ID_Cycle_Safe(GetCurrentAction()->m_tAnimationAction.m_caAnimationToPlay.c_str()); m_use_animation_movement_controller = GetCurrentAction()->m_tAnimationAction.m_use_animation_movement_controller; return (true); } @@ -401,7 +401,7 @@ bool CScriptEntity::bfAssignSound(CScriptEntityAction* tpEntityAction) if (xr_strlen(l_tSoundAction.m_caSoundToPlay)) { m_current_sound = xr_new(); - m_current_sound->create(*l_tSoundAction.m_caSoundToPlay, st_Effect, l_tSoundAction.m_sound_type); + m_current_sound->create(l_tSoundAction.m_caSoundToPlay.c_str(), st_Effect, l_tSoundAction.m_sound_type); } else l_tSoundAction.m_bCompleted = true; @@ -420,7 +420,7 @@ bool CScriptEntity::bfAssignParticles(CScriptEntityAction* tpEntityAction) { if (!l_tParticleAction.m_bStartedToPlay) { - const Fmatrix& l_tMatrix = GetUpdatedMatrix(*l_tParticleAction.m_caBoneName, + const Fmatrix& l_tMatrix = GetUpdatedMatrix(l_tParticleAction.m_caBoneName.c_str(), l_tParticleAction.m_tParticlePosition, l_tParticleAction.m_tParticleAngles); Fvector zero_vel = {0.f, 0.f, 0.f}; l_tParticleAction.m_tpParticleSystem->UpdateParent(l_tMatrix, zero_vel); @@ -513,7 +513,7 @@ bool CScriptEntity::bfAssignMovement(CScriptEntityAction* tpEntityAction) string256 S; xr_sprintf(S, "Cannot find corresponding level vertex for the specified position [%f][%f][%f] for monster %s", - VPUSH(l_tMovementAction.m_tDestinationPosition), *m_monster->cName()); + VPUSH(l_tMovementAction.m_tDestinationPosition), m_monster->cName().c_str()); THROW2(ai().level_graph().valid_vertex_id(vertex_id), S); } #endif @@ -568,13 +568,13 @@ LPCSTR CScriptEntity::GetPatrolPathName() if (!GetScriptControl()) { GEnv.ScriptEngine->script_log(LuaMessageType::Error, - "Object %s is not under script control while you are trying to get patrol path name!", *m_object->cName()); + "Object %s is not under script control while you are trying to get patrol path name!", m_object->cName().c_str()); return ""; } #endif if (m_tpActionQueue.empty()) return (""); - return (*m_tpActionQueue.back()->m_tMovementAction.m_path_name); + return (m_tpActionQueue.back()->m_tMovementAction.m_path_name.c_str()); } bool CScriptEntity::net_Spawn(CSE_Abstract* DC) @@ -627,7 +627,7 @@ bool CScriptEntity::bfScriptAnimation() #endif m_tpScriptAnimation = m_tpNextAnimation; IKinematicsAnimated* skeleton_animated = smart_cast(object().Visual()); - LPCSTR animation_id = *GetCurrentAction()->m_tAnimationAction.m_caAnimationToPlay; + LPCSTR animation_id = GetCurrentAction()->m_tAnimationAction.m_caAnimationToPlay.c_str(); MotionID animation = skeleton_animated->ID_Cycle(animation_id); CBlend* result = 0; for (u16 i = 0; i < MAX_PARTS; ++i) diff --git a/src/xrGame/script_game_object.cpp b/src/xrGame/script_game_object.cpp index 56124459bd3..d2de0e012e7 100644 --- a/src/xrGame/script_game_object.cpp +++ b/src/xrGame/script_game_object.cpp @@ -214,7 +214,7 @@ LPCSTR CScriptGameObject::WhoHitName() CEntityAlive* entity_alive = smart_cast(&object()); if (entity_alive) return entity_alive->conditions().GetWhoHitLastTime() ? - (*entity_alive->conditions().GetWhoHitLastTime()->cName()) : + (entity_alive->conditions().GetWhoHitLastTime()->cName().c_str()) : NULL; else { @@ -229,7 +229,7 @@ LPCSTR CScriptGameObject::WhoHitSectionName() CEntityAlive* entity_alive = smart_cast(&object()); if (entity_alive) return entity_alive->conditions().GetWhoHitLastTime() ? - (*entity_alive->conditions().GetWhoHitLastTime()->cNameSect()) : + (entity_alive->conditions().GetWhoHitLastTime()->cNameSect().c_str()) : NULL; else { diff --git a/src/xrGame/script_game_object3.cpp b/src/xrGame/script_game_object3.cpp index 27482d5032a..2f9d100555f 100644 --- a/src/xrGame/script_game_object3.cpp +++ b/src/xrGame/script_game_object3.cpp @@ -351,7 +351,7 @@ LPCSTR CScriptGameObject::GetPatrolPathName() return (script_monster->GetPatrolPathName()); } else - return (*stalker->movement().patrol().path_name()); + return (stalker->movement().patrol().path_name().c_str()); } void CScriptGameObject::add_animation(LPCSTR animation, bool hand_usage, bool use_movement_controller) @@ -572,7 +572,7 @@ void CScriptGameObject::set_desired_position(const Fvector* desired_position) GEnv.ScriptEngine->script_log(LuaMessageType::Error, "CAI_Stalker : cannot access class member movement!"); else { - THROW2(desired_position || stalker->movement().restrictions().accessible(*desired_position), *stalker->cName()); + THROW2(desired_position || stalker->movement().restrictions().accessible(*desired_position), stalker->cName().c_str()); stalker->movement().set_desired_position(desired_position); } } @@ -1239,7 +1239,7 @@ LPCSTR CScriptGameObject::sound_prefix() const return (0); } - return (*custom_monster->sound().sound_prefix()); + return (custom_monster->sound().sound_prefix().c_str()); } void CScriptGameObject::sound_prefix(LPCSTR sound_prefix) diff --git a/src/xrGame/script_game_object_inventory_owner.cpp b/src/xrGame/script_game_object_inventory_owner.cpp index 392b1d64ea2..a67ddbb4842 100644 --- a/src/xrGame/script_game_object_inventory_owner.cpp +++ b/src/xrGame/script_game_object_inventory_owner.cpp @@ -662,7 +662,7 @@ LPCSTR CScriptGameObject::ProfileName() if (!profile_id || !profile_id.size()) return NULL; else - return *profile_id; + return profile_id.c_str(); } LPCSTR CScriptGameObject::CharacterName() @@ -776,7 +776,7 @@ LPCSTR CScriptGameObject::CharacterCommunity() GEnv.ScriptEngine->script_log(LuaMessageType::Error, "CharacterCommunity available only for InventoryOwner"); return NULL; } - return *pInventoryOwner->CharacterInfo().Community().id(); + return pInventoryOwner->CharacterInfo().Community().id().c_str(); } void CScriptGameObject::SetCharacterCommunity(LPCSTR comm, int squad, int group) @@ -911,10 +911,10 @@ void construct_restriction_vector(shared_str restrictions, xr_vectorID()); @@ -972,7 +972,7 @@ LPCSTR CScriptGameObject::in_restrictions() LuaMessageType::Error, "CRestrictedObject : cannot access class member in_restrictions!"); return (""); } - return (*monster->movement().restrictions().in_restrictions()); + return (monster->movement().restrictions().in_restrictions().c_str()); } LPCSTR CScriptGameObject::out_restrictions() @@ -984,7 +984,7 @@ LPCSTR CScriptGameObject::out_restrictions() LuaMessageType::Error, "CRestrictedObject : cannot access class member out_restrictions!"); return (""); } - return (*monster->movement().restrictions().out_restrictions()); + return (monster->movement().restrictions().out_restrictions().c_str()); } LPCSTR CScriptGameObject::base_in_restrictions() @@ -996,7 +996,7 @@ LPCSTR CScriptGameObject::base_in_restrictions() LuaMessageType::Error, "CRestrictedObject : cannot access class member base_in_restrictions!"); return (""); } - return (*monster->movement().restrictions().base_in_restrictions()); + return (monster->movement().restrictions().base_in_restrictions().c_str()); } LPCSTR CScriptGameObject::base_out_restrictions() @@ -1008,7 +1008,7 @@ LPCSTR CScriptGameObject::base_out_restrictions() LuaMessageType::Error, "CRestrictedObject : cannot access class member base_out_restrictions!"); return (""); } - return (*monster->movement().restrictions().base_out_restrictions()); + return (monster->movement().restrictions().base_out_restrictions().c_str()); } bool CScriptGameObject::accessible_position(const Fvector& position) diff --git a/src/xrGame/script_game_object_use.cpp b/src/xrGame/script_game_object_use.cpp index 60347a173d7..279e7a66f57 100644 --- a/src/xrGame/script_game_object_use.cpp +++ b/src/xrGame/script_game_object_use.cpp @@ -56,9 +56,9 @@ CScriptGameObject* CScriptGameObject::Parent() const } int CScriptGameObject::clsid() const { return (object().clsid()); } -LPCSTR CScriptGameObject::Name() const { return (*object().cName()); } +LPCSTR CScriptGameObject::Name() const { return (object().cName().c_str()); } shared_str CScriptGameObject::cName() const { return (object().cName()); } -LPCSTR CScriptGameObject::Section() const { return (*object().cNameSect()); } +LPCSTR CScriptGameObject::Section() const { return (object().cNameSect().c_str()); } void CScriptGameObject::Kill(CScriptGameObject* who, bool bypass_actor_check) { CEntity* l_tpEntity = smart_cast(&object()); @@ -68,13 +68,13 @@ void CScriptGameObject::Kill(CScriptGameObject* who, bool bypass_actor_check) if (!l_tpEntity) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "%s cannot access class member Kill!", *object().cName()); + LuaMessageType::Error, "%s cannot access class member Kill!", object().cName().c_str()); return; } if (!l_tpEntity->AlreadyDie()) l_tpEntity->KillEntity(who ? who->object().ID() : object().ID(), bypass_actor_check); else - GEnv.ScriptEngine->script_log(LuaMessageType::Error, "attempt to kill dead object %s", *object().cName()); + GEnv.ScriptEngine->script_log(LuaMessageType::Error, "attempt to kill dead object %s", object().cName().c_str()); } bool CScriptGameObject::Alive() const @@ -94,7 +94,7 @@ ALife::ERelationType CScriptGameObject::GetRelationType(CScriptGameObject* who) if (!l_tpEntityAlive1) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "%s cannot access class member GetRelationType!", *object().cName()); + LuaMessageType::Error, "%s cannot access class member GetRelationType!", object().cName().c_str()); return ALife::eRelationTypeDummy; } @@ -102,7 +102,7 @@ ALife::ERelationType CScriptGameObject::GetRelationType(CScriptGameObject* who) if (!l_tpEntityAlive2) { GEnv.ScriptEngine->script_log(LuaMessageType::Error, - "%s cannot apply GetRelationType method for non-alive object!", *who->object().cName()); + "%s cannot apply GetRelationType method for non-alive object!", who->object().cName().c_str()); return ALife::eRelationTypeDummy; } @@ -197,7 +197,7 @@ void CScriptGameObject::set_const_force(const Fvector& dir, float value, u32 tim if (!shell) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "set_const_force : object %s has no physics shell!", *object().cName()); + LuaMessageType::Error, "set_const_force : object %s has no physics shell!", object().cName().c_str()); return; } //#ifdef DEBUG diff --git a/src/xrGame/script_particle_action.cpp b/src/xrGame/script_particle_action.cpp index 8ba7563346e..79b10e2df56 100644 --- a/src/xrGame/script_particle_action.cpp +++ b/src/xrGame/script_particle_action.cpp @@ -19,7 +19,7 @@ void CScriptParticleAction::SetParticle(LPCSTR caParticleToRun, bool bAutoRemove { m_caParticleToRun = caParticleToRun; m_tGoalType = eGoalTypeParticleAttached; - m_tpParticleSystem = CParticlesObject::Create(*m_caParticleToRun, BOOL(m_bAutoRemove = bAutoRemove)); + m_tpParticleSystem = CParticlesObject::Create(m_caParticleToRun.c_str(), BOOL(m_bAutoRemove = bAutoRemove)); m_bStartedToPlay = false; m_bCompleted = false; } diff --git a/src/xrGame/script_sound.cpp b/src/xrGame/script_sound.cpp index 37875e8e3f3..954a2962514 100644 --- a/src/xrGame/script_sound.cpp +++ b/src/xrGame/script_sound.cpp @@ -48,7 +48,7 @@ Fvector CScriptSound::GetPosition() const void CScriptSound::Play(CScriptGameObject* object, float delay, int flags) { - THROW3(m_sound._handle() || m_bIsNoSound, "There is no sound", *m_caSoundToPlay); + THROW3(m_sound._handle() || m_bIsNoSound, "There is no sound", m_caSoundToPlay.c_str()); // Msg ("%6d : CScriptSound::Play (%s), delay %f, flags //%d",Device.dwTimeGlobal,m_sound._handle()->file_name(),delay,flags); m_sound.play((object) ? &object->object() : NULL, flags, delay); @@ -56,7 +56,7 @@ void CScriptSound::Play(CScriptGameObject* object, float delay, int flags) void CScriptSound::PlayAtPos(CScriptGameObject* object, const Fvector& position, float delay, int flags) { - THROW3(m_sound._handle() || m_bIsNoSound, "There is no sound", *m_caSoundToPlay); + THROW3(m_sound._handle() || m_bIsNoSound, "There is no sound", m_caSoundToPlay.c_str()); // Msg ("%6d : CScriptSound::Play (%s), delay %f, flags //%d",m_sound._handle()->file_name(),delay,flags); m_sound.play_at_pos((object) ? &object->object() : NULL, position, flags, delay); @@ -65,6 +65,6 @@ void CScriptSound::PlayAtPos(CScriptGameObject* object, const Fvector& position, void CScriptSound::PlayNoFeedback( CScriptGameObject* object, u32 flags /*!< Looping */, float delay /*!< Delay */, Fvector pos, float vol) { - THROW3(m_sound._handle() || m_bIsNoSound, "There is no sound", *m_caSoundToPlay); + THROW3(m_sound._handle() || m_bIsNoSound, "There is no sound", m_caSoundToPlay.c_str()); m_sound.play_no_feedback((object) ? &object->object() : NULL, flags, delay, &pos, &vol); } diff --git a/src/xrGame/script_sound_action.cpp b/src/xrGame/script_sound_action.cpp index 859225d40b5..4c894aeccdd 100644 --- a/src/xrGame/script_sound_action.cpp +++ b/src/xrGame/script_sound_action.cpp @@ -18,7 +18,7 @@ void CScriptSoundAction::SetSound(LPCSTR caSoundToPlay) m_tGoalType = eGoalTypeSoundAttached; m_bStartedToPlay = false; string_path l_caFileName; - if (FS.exist(l_caFileName, "$game_sounds$", *m_caSoundToPlay, ".ogg")) + if (FS.exist(l_caFileName, "$game_sounds$", m_caSoundToPlay.c_str(), ".ogg")) { m_bStartedToPlay = false; m_bCompleted = false; diff --git a/src/xrGame/seniority_hierarchy_holder.cpp b/src/xrGame/seniority_hierarchy_holder.cpp index dc0b9236440..7a26dc4e342 100644 --- a/src/xrGame/seniority_hierarchy_holder.cpp +++ b/src/xrGame/seniority_hierarchy_holder.cpp @@ -14,7 +14,7 @@ CSeniorityHierarchyHolder::~CSeniorityHierarchyHolder() { delete_data(m_teams); } CTeamHierarchyHolder& CSeniorityHierarchyHolder::team(u32 team_id) { - VERIFY3(team_id < max_team_count, "Team id is invalid : ", *SeniorityHierarchy::to_string(team_id)); + VERIFY3(team_id < max_team_count, "Team id is invalid : ", SeniorityHierarchy::to_string(team_id).c_str()); if (!m_teams[team_id]) m_teams[team_id] = xr_new(this); return (*m_teams[team_id]); diff --git a/src/xrGame/smart_cover_description.cpp b/src/xrGame/smart_cover_description.cpp index acf17502f46..d80b9e99cf6 100644 --- a/src/xrGame/smart_cover_description.cpp +++ b/src/xrGame/smart_cover_description.cpp @@ -90,7 +90,7 @@ void description::load_loopholes(shared_str const& table_id) { string256 temp; xr_strcpy(temp, "smart_covers.descriptions."); - xr_strcat(temp, *table_id); + xr_strcat(temp, table_id.c_str()); xr_strcat(temp, ".loopholes"); m_table_id = table_id; @@ -146,7 +146,7 @@ void description::load_transitions(shared_str const& table_id) { string256 temp; xr_strcpy(temp, "smart_covers.descriptions."); - xr_strcat(temp, *table_id); + xr_strcat(temp, table_id.c_str()); xr_strcat(temp, ".transitions"); luabind::object transitions; diff --git a/src/xrGame/sound_player.cpp b/src/xrGame/sound_player.cpp index ea5428e079a..8d5ed470271 100644 --- a/src/xrGame/sound_player.cpp +++ b/src/xrGame/sound_player.cpp @@ -169,7 +169,7 @@ void CSoundPlayer::play( { #ifdef DEBUG Msg("- There are no sounds in sound collection \"%s\" with internal type %d (sound_script = %d)", - *sound.m_sound_prefix, internal_type, StalkerSpace::eStalkerSoundScript); + sound.m_sound_prefix.c_str(), internal_type, StalkerSpace::eStalkerSoundScript); #endif return; } @@ -239,12 +239,12 @@ CSoundPlayer::CSoundCollection::CSoundCollection(const CSoundCollectionParams& p { seed(u32(CPU::QPC() & 0xffffffff)); m_sounds.clear(); - for (int j = 0, N = _GetItemCount(*params.m_sound_prefix); j < N; ++j) + for (int j = 0, N = _GetItemCount(params.m_sound_prefix.c_str()); j < N; ++j) { string_path fn, s, temp; pstr S = (pstr)&s; - _GetItem(*params.m_sound_prefix, j, temp); - strconcat(sizeof(s), S, *params.m_sound_player_prefix, temp); + _GetItem(params.m_sound_prefix.c_str(), j, temp); + strconcat(sizeof(s), S, params.m_sound_player_prefix.c_str(), temp); if (FS.exist(fn, "$game_sounds$", S, ".ogg")) { ref_sound* temp = add(params.m_type, S); @@ -265,7 +265,7 @@ CSoundPlayer::CSoundCollection::CSoundCollection(const CSoundCollectionParams& p } #ifdef DEBUG if (m_sounds.empty()) - Msg("- There are no sounds with prefix %s", *params.m_sound_prefix); + Msg("- There are no sounds with prefix %s", params.m_sound_prefix.c_str()); #endif } diff --git a/src/xrGame/space_restriction.cpp b/src/xrGame/space_restriction.cpp index 57f61b521b3..9766a8cda88 100644 --- a/src/xrGame/space_restriction.cpp +++ b/src/xrGame/space_restriction.cpp @@ -140,12 +140,12 @@ void CSpaceRestriction::merge_in_out_restrictions() CSpaceRestriction::CBaseRestrictionPtr CSpaceRestriction::merge( CBaseRestrictionPtr bridge, const RESTRICTIONS& temp_restrictions) const { - u32 acc_length = xr_strlen(*bridge->name()) + 1; + u32 acc_length = xr_strlen(bridge->name().c_str()) + 1; { RESTRICTIONS::const_iterator I = temp_restrictions.begin(); RESTRICTIONS::const_iterator E = temp_restrictions.end(); for (; I != E; ++I) - acc_length += xr_strlen(*(*I)->name()) + 1; + acc_length += xr_strlen((*I)->name().c_str()) + 1; } pstr S = xr_alloc(acc_length); @@ -154,7 +154,7 @@ CSpaceRestriction::CBaseRestrictionPtr CSpaceRestriction::merge( RESTRICTIONS::const_iterator I = temp_restrictions.begin(); RESTRICTIONS::const_iterator E = temp_restrictions.end(); for (; I != E; ++I) - temp = strconcat(acc_length, S, *temp, ",", *(*I)->name()); + temp = strconcat(acc_length, S, temp.c_str(), ",", (*I)->name().c_str()); xr_free(S); @@ -223,7 +223,7 @@ void CSpaceRestriction::initialize() if (!m_out_space_restriction->object().correct()) { Msg("~ BAD out restrictions combination :"); - Msg("~ %s", *m_out_space_restriction->name()); + Msg("~ %s", m_out_space_restriction->name().c_str()); } } #endif diff --git a/src/xrGame/space_restriction_bridge_inline.h b/src/xrGame/space_restriction_bridge_inline.h index c8d87187923..e96639b6c9d 100644 --- a/src/xrGame/space_restriction_bridge_inline.h +++ b/src/xrGame/space_restriction_bridge_inline.h @@ -47,7 +47,7 @@ IC u32 CSpaceRestrictionBridge::accessible_nearest( } bool selected_vertex_id_is_valid = level_graph.valid_vertex_id(selected); VERIFY2(selected_vertex_id_is_valid, - make_string("vertex_id[%d], object[%s], position[%f][%f][%f]", selected, *name(), VPUSH(position))); + make_string("vertex_id[%d], object[%s], position[%f][%f][%f]", selected, name().c_str(), VPUSH(position))); if (!selected_vertex_id_is_valid) return -1; diff --git a/src/xrGame/space_restriction_composition.cpp b/src/xrGame/space_restriction_composition.cpp index 88c3ce6d004..a8bf3037e23 100644 --- a/src/xrGame/space_restriction_composition.cpp +++ b/src/xrGame/space_restriction_composition.cpp @@ -60,7 +60,7 @@ bool CSpaceRestrictionComposition::inside(const Fsphere& sphere) void CSpaceRestrictionComposition::initialize() { - u32 n = _GetItemCount(*m_space_restrictors); + u32 n = _GetItemCount(m_space_restrictors.c_str()); VERIFY(n); if (n == 1) { @@ -74,14 +74,14 @@ void CSpaceRestrictionComposition::initialize() string256 element; for (u32 i = 0; i < n; ++i) - if (!m_space_restriction_holder->restriction(_GetItem(*m_space_restrictors, i, element))->initialized()) + if (!m_space_restriction_holder->restriction(_GetItem(m_space_restrictors.c_str(), i, element))->initialized()) return; Fsphere* spheres = (Fsphere*)xr_alloca(n * sizeof(Fsphere)); for (u32 i = 0; i < n; ++i) { SpaceRestrictionHolder::CBaseRestrictionPtr restriction = - m_space_restriction_holder->restriction(_GetItem(*m_space_restrictors, i, element)); + m_space_restriction_holder->restriction(_GetItem(m_space_restrictors.c_str(), i, element)); merge(restriction); @@ -190,7 +190,7 @@ Fsphere CSpaceRestrictionComposition::sphere() const #ifdef DEBUG void CSpaceRestrictionComposition::check_restrictor_type() { - if (_GetItemCount(*m_space_restrictors) == 1) + if (_GetItemCount(m_space_restrictors.c_str()) == 1) return; if (!ai().get_alife()) @@ -201,7 +201,7 @@ void CSpaceRestrictionComposition::check_restrictor_type() return; CSpaceRestrictor* restrictor = smart_cast(object); - VERIFY3(restrictor, "you are trying to use object as a restrictor", *m_space_restrictors); + VERIFY3(restrictor, "you are trying to use object as a restrictor", m_space_restrictors.c_str()); VERIFY2(restrictor->restrictor_type() == RestrictionSpace::eRestrictorTypeNone, "you are trying to restrict yourself with restrictor with type eRestrictorTypeNone"); VERIFY2(restrictor->restrictor_type() != RestrictionSpace::eRestrictorTypeNone, diff --git a/src/xrGame/space_restriction_holder.cpp b/src/xrGame/space_restriction_holder.cpp index 7329851c6e6..4c942737858 100644 --- a/src/xrGame/space_restriction_holder.cpp +++ b/src/xrGame/space_restriction_holder.cpp @@ -36,7 +36,7 @@ shared_str CSpaceRestrictionHolder::normalize_string(shared_str space_restrictor pstr* string_current = strings; pstr temp_string = (pstr)xr_alloca((n + 1) * sizeof(char)); - LPCSTR I = *space_restrictors; + LPCSTR I = space_restrictors.c_str(); pstr i = temp_string, j = i; for (; *I; ++I, ++i) { @@ -120,9 +120,9 @@ void CSpaceRestrictionHolder::register_restrictor( temp1 = *temp; if (xr_strlen(*temp) && xr_strlen(space_restrictors)) - strconcat(sizeof(m_temp_string), m_temp_string, **temp, ",", *space_restrictors); + strconcat(sizeof(m_temp_string), m_temp_string, (*temp).c_str(), ",", space_restrictors.c_str()); else - strconcat(sizeof(m_temp_string), m_temp_string, **temp, *space_restrictors); + strconcat(sizeof(m_temp_string), m_temp_string, (*temp).c_str(), space_restrictors.c_str()); *temp = normalize_string(m_temp_string); @@ -149,9 +149,9 @@ bool try_remove_string(shared_str& search_string, const shared_str& string_to_se string256 temp; string4096 temp1; *temp1 = 0; - for (int i = 0, j = 0, n = _GetItemCount(*search_string); i < n; ++i, ++j) + for (int i = 0, j = 0, n = _GetItemCount(search_string.c_str()); i < n; ++i, ++j) { - if (xr_strcmp(string_to_search, _GetItem(*search_string, i, temp))) + if (xr_strcmp(string_to_search, _GetItem(search_string.c_str(), i, temp))) { if (j) xr_strcat(temp1, ","); diff --git a/src/xrGame/space_restriction_manager.cpp b/src/xrGame/space_restriction_manager.cpp index c803082038d..e122bbf62c6 100644 --- a/src/xrGame/space_restriction_manager.cpp +++ b/src/xrGame/space_restriction_manager.cpp @@ -30,8 +30,8 @@ CSpaceRestrictionManager::~CSpaceRestrictionManager() void show_restriction(const shared_str& restrictions) { string256 temp; - for (int i = 0, n = _GetItemCount(*restrictions); i < n; ++i) - Msg(" %s", _GetItem(*restrictions, i, temp)); + for (int i = 0, n = _GetItemCount(restrictions.c_str()); i < n; ++i) + Msg(" %s", _GetItem(restrictions.c_str(), i, temp)); } typedef intrusive_ptr CRestrictionPtr; @@ -171,7 +171,7 @@ CSpaceRestrictionManager::CRestrictionPtr CSpaceRestrictionManager::restriction( out_restrictors = normalize_string(out_restrictors); in_restrictors = normalize_string(in_restrictors); - strconcat(sizeof(m_temp), m_temp, *out_restrictors, "\x01", *in_restrictors); + strconcat(sizeof(m_temp), m_temp, out_restrictors.c_str(), "\x01", in_restrictors.c_str()); shared_str space_restrictions = m_temp; SPACE_RESTRICTIONS::const_iterator I = m_space_restrictions.find(space_restrictions); @@ -193,8 +193,8 @@ u32 CSpaceRestrictionManager::accessible_nearest(ALife::_OBJECT_ID id, const Fve bool CSpaceRestrictionManager::restriction_presented(shared_str restrictions, shared_str restriction) const { string4096 m_temp; - for (u32 i = 0, n = _GetItemCount(*restrictions); i < n; ++i) - if (!xr_strcmp(restriction, _GetItem(*restrictions, i, m_temp))) + for (u32 i = 0, n = _GetItemCount(restrictions.c_str()); i < n; ++i) + if (!xr_strcmp(restriction, _GetItem(restrictions.c_str(), i, m_temp))) return (true); return (false); } @@ -203,9 +203,9 @@ void CSpaceRestrictionManager::join_restrictions(shared_str& restrictions, share { string4096 m_temp1; string4096 m_temp2; - xr_strcpy(m_temp2, *restrictions); - for (u32 i = 0, n = _GetItemCount(*update), count = xr_strlen(m_temp2); i < n; ++i) - if (!restriction_presented(m_temp2, _GetItem(*update, i, m_temp1))) + xr_strcpy(m_temp2, restrictions.c_str()); + for (u32 i = 0, n = _GetItemCount(update.c_str()), count = xr_strlen(m_temp2); i < n; ++i) + if (!restriction_presented(m_temp2, _GetItem(update.c_str(), i, m_temp1))) { if (count) xr_strcat(m_temp2, ","); @@ -220,8 +220,8 @@ void CSpaceRestrictionManager::difference_restrictions(shared_str& restrictions, string4096 m_temp1; string4096 m_temp2; xr_strcpy(m_temp2, ""); - for (u32 i = 0, n = _GetItemCount(*restrictions), count = 0; i < n; ++i) - if (!restriction_presented(update, _GetItem(*restrictions, i, m_temp1))) + for (u32 i = 0, n = _GetItemCount(restrictions.c_str()), count = 0; i < n; ++i) + if (!restriction_presented(update, _GetItem(restrictions.c_str(), i, m_temp1))) { if (count) xr_strcat(m_temp2, ","); diff --git a/src/xrGame/space_restriction_shape.cpp b/src/xrGame/space_restriction_shape.cpp index 59b3dcb3cb0..e2b4a3b4829 100644 --- a/src/xrGame/space_restriction_shape.cpp +++ b/src/xrGame/space_restriction_shape.cpp @@ -102,7 +102,7 @@ void CSpaceRestrictionShape::build_border() process_borders(); - VERIFY3(!border().empty(), "space restrictor has no border", *m_restrictor->cName()); + VERIFY3(!border().empty(), "space restrictor has no border", m_restrictor->cName().c_str()); #ifdef DEBUG test_correctness(); diff --git a/src/xrGame/squad_hierarchy_holder.cpp b/src/xrGame/squad_hierarchy_holder.cpp index 81df4132686..3f38989435f 100644 --- a/src/xrGame/squad_hierarchy_holder.cpp +++ b/src/xrGame/squad_hierarchy_holder.cpp @@ -16,7 +16,7 @@ CSquadHierarchyHolder::~CSquadHierarchyHolder() { delete_data(m_groups); } CGroupHierarchyHolder& CSquadHierarchyHolder::group(u32 group_id) const { - VERIFY3(group_id < max_group_count, "Group id is invalid : ", *SeniorityHierarchy::to_string(group_id)); + VERIFY3(group_id < max_group_count, "Group id is invalid : ", SeniorityHierarchy::to_string(group_id).c_str()); if (!m_groups[group_id]) m_groups[group_id] = xr_new(const_cast(this)); return (*m_groups[group_id]); diff --git a/src/xrGame/stalker_animation_callbacks.cpp b/src/xrGame/stalker_animation_callbacks.cpp index c5aa132e29e..f60d4cff8cc 100644 --- a/src/xrGame/stalker_animation_callbacks.cpp +++ b/src/xrGame/stalker_animation_callbacks.cpp @@ -119,7 +119,7 @@ void CStalkerAnimationManager::assign_bone_callbacks() m_spine_params.invalidate(); #endif // #ifdef DEBUG - LPCSTR section = *object().cNameSect(); + LPCSTR section = object().cNameSect().c_str(); m_head_params.m_rotation = &object().sight().current_head_rotation(); m_head_params.m_object = &object(); @@ -159,7 +159,7 @@ void CStalkerAnimationManager::assign_bone_blend_callbacks(bool const& forward_d m_spine_params.invalidate(); #endif // #ifdef DEBUG - LPCSTR section = *object().cNameSect(); + LPCSTR section = object().cNameSect().c_str(); m_head_params.m_rotation = &object().sight().current_head_rotation(); m_head_params.m_object = &object(); @@ -200,7 +200,7 @@ void CStalkerAnimationManager::remove_bone_callbacks() m_spine_params.invalidate(); #endif // #ifdef DEBUG - LPCSTR section = *object().cNameSect(); + LPCSTR section = object().cNameSect().c_str(); int head_bone = kinematics->LL_BoneID(pSettings->r_string(section, "bone_head")); kinematics->LL_GetBoneInstance(u16(head_bone)).set_callback(bctCustom, 0, 0); diff --git a/src/xrGame/stalker_animation_global.cpp b/src/xrGame/stalker_animation_global.cpp index 37651b62d9c..c6a95ea3457 100644 --- a/src/xrGame/stalker_animation_global.cpp +++ b/src/xrGame/stalker_animation_global.cpp @@ -51,7 +51,7 @@ MotionID CStalkerAnimationManager::global_critical_hit() CWeapon* weapon = smart_cast(object().inventory().ActiveItem()); VERIFY2(weapon, make_string("current active item: %s", object().inventory().ActiveItem() ? - *object().inventory().ActiveItem()->object().cName() : + object().inventory().ActiveItem()->object().cName().c_str() : "no active item")); u32 animation_slot = weapon->animation_slot(); diff --git a/src/xrGame/stalker_animation_manager.cpp b/src/xrGame/stalker_animation_manager.cpp index 9cccf28d579..a05ced01611 100644 --- a/src/xrGame/stalker_animation_manager.cpp +++ b/src/xrGame/stalker_animation_manager.cpp @@ -90,11 +90,11 @@ void CStalkerAnimationManager::reload() assign_bone_callbacks(); #ifdef DEBUG - global().set_dbg_info(*object().cName(), "Global"); - head().set_dbg_info(*object().cName(), "Head "); - torso().set_dbg_info(*object().cName(), "Torso "); - legs().set_dbg_info(*object().cName(), "Legs "); - script().set_dbg_info(*object().cName(), "Script"); + global().set_dbg_info(object().cName().c_str(), "Global"); + head().set_dbg_info(object().cName().c_str(), "Head "); + torso().set_dbg_info(object().cName().c_str(), "Torso "); + legs().set_dbg_info(object().cName().c_str(), "Legs "); + script().set_dbg_info(object().cName().c_str(), "Script"); #endif }; @@ -111,7 +111,7 @@ void CStalkerAnimationManager::play_fx(float power_factor, int fx_index) ->LL_MotionDefName_dbg( m_data_storage->m_part_animations.A[object().movement().body_state()].m_global.A[0].A[fx_index]) .first; - Msg("%6d [%s][%s][%s][%f]", Device.dwTimeGlobal, *object().cName(), "FX", name, power_factor); + Msg("%6d [%s][%s][%s][%f]", Device.dwTimeGlobal, object().cName().c_str(), "FX", name, power_factor); } #endif m_skeleton_animated->PlayFX( diff --git a/src/xrGame/stalker_animation_manager_debug.cpp b/src/xrGame/stalker_animation_manager_debug.cpp index f41b5b5f1a8..8ae3032219c 100644 --- a/src/xrGame/stalker_animation_manager_debug.cpp +++ b/src/xrGame/stalker_animation_manager_debug.cpp @@ -101,8 +101,8 @@ void show_animations() Msg("frames starts animation animation_set"); for (i = animations; i != e; ++i) - Msg("%6d %6d %-32s %s", (*i)->second.m_frame_count, (*i)->second.m_start_count, *(*i)->first.first, - *(*i)->first.second); + Msg("%6d %6d %-32s %s", (*i)->second.m_frame_count, (*i)->second.m_start_count, (*i)->first.first.c_str(), + (*i)->first.second.c_str()); } void show_blends() @@ -130,8 +130,8 @@ void show_blends() " " " animation_set2"); for (i = blends; i != e; ++i) - Msg("%-32s %32s ->[%6d]-> %-32s %32s", *(*i)->first.second.second, *(*i)->first.second.first, (*i)->second, - *(*i)->first.first.first, *(*i)->first.first.second); + Msg("%-32s %32s ->[%6d]-> %-32s %32s", (*i)->first.second.second.c_str(), (*i)->first.second.first.c_str(), (*i)->second, + (*i)->first.first.first.c_str(), (*i)->first.first.second.c_str()); } void show_animation_stats() @@ -193,7 +193,7 @@ void CStalkerAnimationManager::add_animation_stats( const ANIMATION_ID& animation_id, const BLEND_ID* blend_id, bool just_started) { ::add_animation_stats( - animation_id.first, animation_id.second, *object().Visual()->getDebugName(), blend_id, just_started); + animation_id.first, animation_id.second, object().Visual()->getDebugName().c_str(), blend_id, just_started); } void CStalkerAnimationManager::add_animation_stats() diff --git a/src/xrGame/stalker_animation_manager_update.cpp b/src/xrGame/stalker_animation_manager_update.cpp index 6a7b4881850..e519e07663d 100644 --- a/src/xrGame/stalker_animation_manager_update.cpp +++ b/src/xrGame/stalker_animation_manager_update.cpp @@ -228,7 +228,7 @@ void CStalkerAnimationManager::update() } catch (...) { - Msg("! error in stalker with visual %s and ID %u", *object().cNameVisual(), object().ID()); + Msg("! error in stalker with visual %s and ID %u", object().cNameVisual().c_str(), object().ID()); /* avo: prevent game from crashing */ head().reset(); torso().reset(); diff --git a/src/xrGame/stalker_animation_script.cpp b/src/xrGame/stalker_animation_script.cpp index 0664bf84451..5340a65d29f 100644 --- a/src/xrGame/stalker_animation_script.cpp +++ b/src/xrGame/stalker_animation_script.cpp @@ -51,7 +51,7 @@ void CStalkerAnimationManager::add_script_animation( if (!motion) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "There is no animation %s (object %s)!", animation, *object().cName()); + LuaMessageType::Error, "There is no animation %s (object %s)!", animation, object().cName().c_str()); return; } @@ -75,7 +75,7 @@ void CStalkerAnimationManager::add_script_animation(LPCSTR animation, bool hand_ if (!motion) { GEnv.ScriptEngine->script_log( - LuaMessageType::Error, "There is no animation %s (object %s)!", animation, *object().cName()); + LuaMessageType::Error, "There is no animation %s (object %s)!", animation, object().cName().c_str()); return; } diff --git a/src/xrGame/stalker_animation_torso.cpp b/src/xrGame/stalker_animation_torso.cpp index 21ebc1b65e1..8d1928b7402 100644 --- a/src/xrGame/stalker_animation_torso.cpp +++ b/src/xrGame/stalker_animation_torso.cpp @@ -78,7 +78,7 @@ MotionID CStalkerAnimationManager::no_object_animation(const EBodyState& body_st if (eMentalStateFree == movement.mental_state()) { VERIFY3(eBodyStateStand == movement.body_state(), "Cannot run FREE animations, when body state is not stand!", - *stalker.cName()); + stalker.cName().c_str()); if (standing()) return (animation[9].A[1]); @@ -147,7 +147,7 @@ MotionID CStalkerAnimationManager::unknown_object_animation(u32 slot, const EBod if (eMentalStateFree == movement.mental_state()) { VERIFY3(eBodyStateStand == movement.body_state(), "Cannot run FREE animation when body state is not stand!", - *object().cName()); + object().cName().c_str()); if (standing()) return (animation[9].A[1]); diff --git a/src/xrGame/stalker_combat_action_base.cpp b/src/xrGame/stalker_combat_action_base.cpp index 10f9d53c783..8888a090501 100644 --- a/src/xrGame/stalker_combat_action_base.cpp +++ b/src/xrGame/stalker_combat_action_base.cpp @@ -252,7 +252,7 @@ void CStalkerActionCombatBase::play_attack_sound( if (object().agent_manager().member().combat_members().empty()) Msg("! I am in combat, but there is no combat members at all (including me), " "npc[%s],team[%d],squad[%d],group[%d]", - *object().cName(), object().g_Team(), object().g_Squad(), object().g_Group()); + object().cName().c_str(), object().g_Team(), object().g_Squad(), object().g_Group()); #endif // DEBUG if (object().agent_manager().member().combat_members().size() > 1) @@ -278,7 +278,7 @@ void CStalkerActionCombatBase::play_start_search_sound( if (object().agent_manager().member().combat_members().empty()) Msg("! I am in combat, but there is no combat members at all (including me), " "npc[%s],team[%d],squad[%d],group[%d]", - *object().cName(), object().g_Team(), object().g_Squad(), object().g_Group()); + object().cName().c_str(), object().g_Team(), object().g_Squad(), object().g_Group()); #endif // DEBUG bool search_with_allies = object().agent_manager().member().combat_members().size() > 1; @@ -297,7 +297,7 @@ void CStalkerActionCombatBase::play_enemy_lost_sound( if (object().agent_manager().member().combat_members().empty()) Msg("! I am in combat, but there is no combat members at all (including me), " "npc[%s],team[%d],squad[%d],group[%d]", - *object().cName(), object().g_Team(), object().g_Squad(), object().g_Group()); + object().cName().c_str(), object().g_Team(), object().g_Squad(), object().g_Group()); #endif // DEBUG bool search_with_allies = object().agent_manager().member().combat_members().size() > 1; diff --git a/src/xrGame/stalker_movement_manager_base.cpp b/src/xrGame/stalker_movement_manager_base.cpp index 9ccabb9db5a..e43dca5bf35 100644 --- a/src/xrGame/stalker_movement_manager_base.cpp +++ b/src/xrGame/stalker_movement_manager_base.cpp @@ -192,7 +192,7 @@ void stalker_movement_manager_base::initialize() void stalker_movement_manager_base::set_desired_position(const Fvector* position) { - VERIFY2(!position || accessible(*position) || show_restrictions(&restrictions()), *object().cName()); + VERIFY2(!position || accessible(*position) || show_restrictions(&restrictions()), object().cName().c_str()); m_target.desired_position(position); } @@ -276,7 +276,7 @@ void stalker_movement_manager_base::setup_movement_params(stalker_movement_param Fvector vertex_position = ai().level_graph().vertex_position(level_path().dest_vertex_id()); VERIFY2(restrictions().accessible(vertex_position) || show_restrictions(&restrictions()), make_string("vertex_id[%d],position[%f][%f][%f],object[%s]", vertex_id, VPUSH(vertex_position), - *object().cName())); + object().cName().c_str())); detail().set_dest_position(vertex_position); } } @@ -476,7 +476,7 @@ void stalker_movement_manager_base::parse_velocity_mask(stalker_movement_params& { CStalkerCombatPlanner& planner = smart_cast(m_object->brain().current_action()); if (planner.current_action_id() != StalkerDecisionSpace::eWorldOperatorKillWoundedEnemy) - Msg("~ stalker %s is doing bad thing (action %s)", *m_object->cName(), + Msg("~ stalker %s is doing bad thing (action %s)", m_object->cName().c_str(), planner.current_action().m_action_name); } #endif // DEBUG @@ -497,7 +497,7 @@ void stalker_movement_manager_base::parse_velocity_mask(stalker_movement_params& } VERIFY2((movement_params.m_mental_state != eMentalStateFree) || movement_params.m_body_state != eBodyStateCrouch, - *object().cName()); + object().cName().c_str()); switch (point.velocity & eVelocityMovementType) { @@ -553,10 +553,10 @@ void stalker_movement_manager_base::set_nearest_accessible_position(Fvector desi VERIFY(ai().level_graph().inside(level_vertex_id, desired_position)); - VERIFY2(restrictions().accessible(level_vertex_id) || show_restrictions(&restrictions()), *object().cName()); + VERIFY2(restrictions().accessible(level_vertex_id) || show_restrictions(&restrictions()), object().cName().c_str()); set_level_dest_vertex(level_vertex_id); - VERIFY2(restrictions().accessible(desired_position) || show_restrictions(&restrictions()), *object().cName()); + VERIFY2(restrictions().accessible(desired_position) || show_restrictions(&restrictions()), object().cName().c_str()); set_desired_position(&desired_position); } diff --git a/src/xrGame/stalker_movement_manager_smart_cover.cpp b/src/xrGame/stalker_movement_manager_smart_cover.cpp index 0592edc9720..4ecc91df368 100644 --- a/src/xrGame/stalker_movement_manager_smart_cover.cpp +++ b/src/xrGame/stalker_movement_manager_smart_cover.cpp @@ -222,10 +222,10 @@ void stalker_movement_manager_smart_cover::reach_enter_location(u32 const& time_ VERIFY(ai().level_graph().inside(level_vertex_id, position)); - VERIFY2(restrictions().accessible(level_vertex_id) || show_restrictions(&restrictions()), *object().cName()); + VERIFY2(restrictions().accessible(level_vertex_id) || show_restrictions(&restrictions()), object().cName().c_str()); CMovementManager::set_level_dest_vertex(level_vertex_id); - VERIFY2(restrictions().accessible(position) || show_restrictions(&restrictions()), *object().cName()); + VERIFY2(restrictions().accessible(position) || show_restrictions(&restrictions()), object().cName().c_str()); m_current.desired_position(&position); } else diff --git a/src/xrGame/stalker_planner.cpp b/src/xrGame/stalker_planner.cpp index a9733a5411d..7cb716abbcd 100644 --- a/src/xrGame/stalker_planner.cpp +++ b/src/xrGame/stalker_planner.cpp @@ -41,7 +41,7 @@ LPCSTR CStalkerPlanner::property2string(const _condition_type& property_id) LPCSTR CStalkerPlanner::object_name() const { VERIFY(m_object); - return (*m_object->cName()); + return (m_object->cName().c_str()); } #endif diff --git a/src/xrGame/stalker_sound_data_visitor.cpp b/src/xrGame/stalker_sound_data_visitor.cpp index b8344b23966..0187c9a31c8 100644 --- a/src/xrGame/stalker_sound_data_visitor.cpp +++ b/src/xrGame/stalker_sound_data_visitor.cpp @@ -46,7 +46,7 @@ void CStalkerSoundDataVisitor::visit(CStalkerSoundData* data) if (!object().g_Alive()) return; #ifdef DEBUG - Msg("%s : Adding fiction hit by sound info from stalker %s", *object().cName(), *data->object().cName()); + Msg("%s : Adding fiction hit by sound info from stalker %s", object().cName().c_str(), data->object().cName().c_str()); #endif //#ifdef DEBUG object().memory().make_object_visible_somewhen(data->object().memory().enemy().selected()); diff --git a/src/xrGame/step_manager.cpp b/src/xrGame/step_manager.cpp index 3dcc3b7aeaf..00b766402a2 100644 --- a/src/xrGame/step_manager.cpp +++ b/src/xrGame/step_manager.cpp @@ -203,7 +203,7 @@ void CStepManager::update(bool b_hud_view) // Играть партиклы if (b_play && !mtl_pair->CollideParticles.empty()) { - LPCSTR ps_name = *mtl_pair->CollideParticles[::Random.randI(0, mtl_pair->CollideParticles.size())]; + LPCSTR ps_name = mtl_pair->CollideParticles[::Random.randI(0, mtl_pair->CollideParticles.size())].c_str(); //отыграть партиклы столкновения материалов CParticlesObject* ps = CParticlesObject::Create(ps_name, TRUE); @@ -273,16 +273,16 @@ void CStepManager::load_foot_bones(CInifile::Sect& data) { const CInifile::Item& item = *I; - u16 index = smart_cast(m_object->Visual())->LL_BoneID(*item.second); - VERIFY3(index != BI_NONE, "foot bone not found", *item.second); + u16 index = smart_cast(m_object->Visual())->LL_BoneID(item.second.c_str()); + VERIFY3(index != BI_NONE, "foot bone not found", item.second.c_str()); - if (xr_strcmp(*item.first, "front_left") == 0) + if (xr_strcmp(item.first.c_str(), "front_left") == 0) m_foot_bones[eFrontLeft] = index; - else if (xr_strcmp(*item.first, "front_right") == 0) + else if (xr_strcmp(item.first.c_str(), "front_right") == 0) m_foot_bones[eFrontRight] = index; - else if (xr_strcmp(*item.first, "back_right") == 0) + else if (xr_strcmp(item.first.c_str(), "back_right") == 0) m_foot_bones[eBackRight] = index; - else if (xr_strcmp(*item.first, "back_left") == 0) + else if (xr_strcmp(item.first.c_str(), "back_left") == 0) m_foot_bones[eBackLeft] = index; } } @@ -296,9 +296,9 @@ void CStepManager::reload_foot_bones() } else { - if (!pSettings->line_exist(*m_object->cNameSect(), "foot_bones")) + if (!pSettings->line_exist(m_object->cNameSect().c_str(), "foot_bones")) R_ASSERT2(false, "section [foot_bones] not found in monster user_data"); - load_foot_bones(pSettings->r_section(pSettings->r_string(*m_object->cNameSect(), "foot_bones"))); + load_foot_bones(pSettings->r_section(pSettings->r_string(m_object->cNameSect().c_str(), "foot_bones"))); } // проверка на соответсвие diff --git a/src/xrGame/team_hierarchy_holder.cpp b/src/xrGame/team_hierarchy_holder.cpp index e25f4fcf536..1faa6f119f8 100644 --- a/src/xrGame/team_hierarchy_holder.cpp +++ b/src/xrGame/team_hierarchy_holder.cpp @@ -15,7 +15,7 @@ CTeamHierarchyHolder::~CTeamHierarchyHolder() { delete_data(m_squads); } CSquadHierarchyHolder& CTeamHierarchyHolder::squad(u32 squad_id) const { - VERIFY3(squad_id < max_squad_count, "Squad id is invalid : ", *SeniorityHierarchy::to_string(squad_id)); + VERIFY3(squad_id < max_squad_count, "Squad id is invalid : ", SeniorityHierarchy::to_string(squad_id).c_str()); if (!m_squads[squad_id]) m_squads[squad_id] = xr_new(const_cast(this)); return (*m_squads[squad_id]); diff --git a/src/xrGame/trade_parameters_inline.h b/src/xrGame/trade_parameters_inline.h index d94d6f98b17..40cc2fc8846 100644 --- a/src/xrGame/trade_parameters_inline.h +++ b/src/xrGame/trade_parameters_inline.h @@ -67,7 +67,7 @@ IC const CTradeFactors& CTradeParameters::factors(_action_type type, const share template IC void CTradeParameters::process(_action_type type, CInifile& ini_file, const shared_str& section) { - R_ASSERT2(ini_file.section_exist(section), make_string("cannot find section %s", *section)); + R_ASSERT2(ini_file.section_exist(section), make_string("cannot find section %s", section.c_str())); CTradeActionParameters& _action = action(type); _action.clear(); diff --git a/src/xrGame/ui/ChangeWeatherDialog.cpp b/src/xrGame/ui/ChangeWeatherDialog.cpp index a3da7be7e3f..34d73578cc7 100644 --- a/src/xrGame/ui/ChangeWeatherDialog.cpp +++ b/src/xrGame/ui/ChangeWeatherDialog.cpp @@ -112,7 +112,7 @@ void ChangeWeatherDialog::InitChangeWeather(CUIXml& xmlDoc) void ChangeWeatherDialog::OnButtonClick(int i) { string1024 command; - xr_sprintf(command, "cl_votestart changeweather %s %s", *weathers[i].Name, *weathers[i].Time); + xr_sprintf(command, "cl_votestart changeweather %s %s", weathers[i].Name.c_str(), weathers[i].Time.c_str()); Console->Execute(command); HideDialog(); } diff --git a/src/xrGame/ui/ServerList.cpp b/src/xrGame/ui/ServerList.cpp index 11b5820a045..14dcb3ba485 100644 --- a/src/xrGame/ui/ServerList.cpp +++ b/src/xrGame/ui/ServerList.cpp @@ -218,14 +218,14 @@ void CServerList::AddServerDetail(const GameInfo& info) void CServerList::AddBoolED(const char* keyName, bool value) { - AddServerDetail(GameInfo(*StringTable().translate(keyName), - value ? *StringTable().translate("mp_si_enabled") : *StringTable().translate("mp_si_disabled"))); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), + value ? StringTable().translate("mp_si_enabled").c_str() : StringTable().translate("mp_si_disabled").c_str())); } void CServerList::AddBoolYN(const char* keyName, bool value) { - AddServerDetail(GameInfo(*StringTable().translate(keyName), - value ? *StringTable().translate("mp_si_yes") : *StringTable().translate("mp_si_no"))); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), + value ? StringTable().translate("mp_si_yes").c_str() : StringTable().translate("mp_si_no").c_str())); } void CServerList::AddBoolKeyED(void* s, const char* keyName, int k) { AddBoolED(keyName, browser().GetBool(s, k)); } @@ -234,7 +234,7 @@ void CServerList::AddIntKey(void* s, const char* keyName, int k) { string256 tmp; xr_sprintf(tmp, "%d", browser().GetInt(s, k)); - AddServerDetail(GameInfo(*StringTable().translate(keyName), tmp)); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), tmp)); } void CServerList::AddIntKeyN(void* s, float m, const char* keyName, const char* suffix, int k) @@ -243,10 +243,10 @@ void CServerList::AddIntKeyN(void* s, float m, const char* keyName, const char* { string256 tmp; xr_sprintf(tmp, "%d%s", int(browser().GetInt(s, k) * m), suffix); - AddServerDetail(GameInfo(*StringTable().translate(keyName), tmp)); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), tmp)); } else - AddServerDetail(GameInfo(*StringTable().translate(keyName), *StringTable().translate("mp_si_no"))); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), StringTable().translate("mp_si_no").c_str())); } void CServerList::AddTimeKey(void* s, const char* keyName, const char* format, const char* suffix, int k) @@ -254,21 +254,21 @@ void CServerList::AddTimeKey(void* s, const char* keyName, const char* format, c if (browser().GetInt(s, k)) { string256 tmp; - xr_sprintf(tmp, format, browser().GetFloat(s, k), *StringTable().translate(suffix)); - AddServerDetail(GameInfo(*StringTable().translate(keyName), tmp)); + xr_sprintf(tmp, format, browser().GetFloat(s, k), StringTable().translate(suffix).c_str()); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), tmp)); } else - AddServerDetail(GameInfo(*StringTable().translate(keyName), *StringTable().translate("mp_si_no"))); + AddServerDetail(GameInfo(StringTable().translate(keyName).c_str(), StringTable().translate("mp_si_no").c_str())); } void CServerList::AddString(const char* key, const char* value) { - AddServerDetail(GameInfo(*StringTable().translate(key), value)); + AddServerDetail(GameInfo(StringTable().translate(key).c_str(), value)); } void CServerList::AddStringSt(const char* key, const char* value) { - AddServerDetail(GameInfo(*StringTable().translate(key), *StringTable().translate(value))); + AddServerDetail(GameInfo(StringTable().translate(key).c_str(), StringTable().translate(value).c_str())); } void CServerList::FillUpDetailedServerInfo() diff --git a/src/xrGame/ui/TeamInfo.cpp b/src/xrGame/ui/TeamInfo.cpp index 422bd1fbab9..2718c84392e 100644 --- a/src/xrGame/ui/TeamInfo.cpp +++ b/src/xrGame/ui/TeamInfo.cpp @@ -67,9 +67,9 @@ LPCSTR CTeamInfo::GetTeam_name(int team) string32 tmp; R_ASSERT2(team == 1 || team == 2 || team == 3, xr_itoa(team, tmp, 10)); if (team == 1) - return *GetTeam1_name(); + return GetTeam1_name().c_str(); else - return *GetTeam2_name(); + return GetTeam2_name().c_str(); } LPCSTR CTeamInfo::GetTeam_color_tag(int team) @@ -108,12 +108,12 @@ LPCSTR CTeamInfo::GetTeam_color_tag(int team) { flags.set(flTeam1_col_t, true); team1_color_tag = str.c_str(); - return *team1_color_tag; + return team1_color_tag.c_str(); } else { flags.set(flTeam2_col_t, true); team2_color_tag = str.c_str(); - return *team2_color_tag; + return team2_color_tag.c_str(); } } diff --git a/src/xrGame/ui/UIActorInfo.cpp b/src/xrGame/ui/UIActorInfo.cpp index ce2673345a3..72b1ae158ba 100644 --- a/src/xrGame/ui/UIActorInfo.cpp +++ b/src/xrGame/ui/UIActorInfo.cpp @@ -219,7 +219,7 @@ void CUIActorInfoWnd::FillPointsDetail(const shared_str& id) xr_sprintf(buff, "%d.", _cntr); itm->m_text0->SetText(buff); - itm->m_text1->SetTextST(*CStringTable().translate((*it).key)); + itm->m_text1->SetTextST(CStringTable().translate((*it).key).c_str()); itm->m_text1->AdjustHeightToText(); if (0 == (*it).str_value.size()) @@ -268,7 +268,7 @@ void CUIActorInfoWnd::FillReputationDetails(CUIXml* xml, LPCSTR path) CUIActorStaticticDetail* itm = xr_new(); itm->Init(xml, path, 0); comm.set(xml->Read(_list_node, "r", i, "unknown_community")); - itm->m_text1->SetTextST(*(comm.id())); + itm->m_text1->SetTextST(comm.id().c_str()); CHARACTER_GOODWILL gw = RELATION_REGISTRY().GetCommunityGoodwill(comm.index(), Actor()->ID()); gw += CHARACTER_COMMUNITY::relation(Actor()->Community(), comm.index()); diff --git a/src/xrGame/ui/UICellCustomItems.cpp b/src/xrGame/ui/UICellCustomItems.cpp index da179462e93..0ffd9d296c2 100644 --- a/src/xrGame/ui/UICellCustomItems.cpp +++ b/src/xrGame/ui/UICellCustomItems.cpp @@ -415,7 +415,7 @@ void CUIWeaponCellItem::Update() { CreateIcon(eSilencer); RefreshOffset(); - InitAddon(GetIcon(eSilencer), *object()->GetSilencerName(), m_addon_offset[eSilencer], Heading()); + InitAddon(GetIcon(eSilencer), object()->GetSilencerName().c_str(), m_addon_offset[eSilencer], Heading()); } } else @@ -433,7 +433,7 @@ void CUIWeaponCellItem::Update() { CreateIcon(eScope); RefreshOffset(); - InitAddon(GetIcon(eScope), *object()->GetScopeName(), m_addon_offset[eScope], Heading()); + InitAddon(GetIcon(eScope), object()->GetScopeName().c_str(), m_addon_offset[eScope], Heading()); } } else @@ -452,7 +452,7 @@ void CUIWeaponCellItem::Update() CreateIcon(eLauncher); RefreshOffset(); InitAddon( - GetIcon(eLauncher), *object()->GetGrenadeLauncherName(), m_addon_offset[eLauncher], Heading()); + GetIcon(eLauncher), object()->GetGrenadeLauncherName().c_str(), m_addon_offset[eLauncher], Heading()); } } else @@ -483,15 +483,15 @@ void CUIWeaponCellItem::SetTextureColor(u32 color) void CUIWeaponCellItem::OnAfterChild(CUIDragDropListEx* parent_list) { if (is_silencer() && GetIcon(eSilencer)) - InitAddon(GetIcon(eSilencer), *object()->GetSilencerName(), m_addon_offset[eSilencer], + InitAddon(GetIcon(eSilencer), object()->GetSilencerName().c_str(), m_addon_offset[eSilencer], parent_list->GetVerticalPlacement()); if (is_scope() && GetIcon(eScope)) InitAddon( - GetIcon(eScope), *object()->GetScopeName(), m_addon_offset[eScope], parent_list->GetVerticalPlacement()); + GetIcon(eScope), object()->GetScopeName().c_str(), m_addon_offset[eScope], parent_list->GetVerticalPlacement()); if (is_launcher() && GetIcon(eLauncher)) - InitAddon(GetIcon(eLauncher), *object()->GetGrenadeLauncherName(), m_addon_offset[eLauncher], + InitAddon(GetIcon(eLauncher), object()->GetGrenadeLauncherName().c_str(), m_addon_offset[eLauncher], parent_list->GetVerticalPlacement()); } @@ -561,7 +561,7 @@ CUIDragItem* CUIWeaponCellItem::CreateDragItem() s = xr_new("Silencer"); s->SetAutoDelete(true); s->SetShader(InventoryUtilities::GetEquipmentIconsShader()); - InitAddon(s, *object()->GetSilencerName(), m_addon_offset[eSilencer], false); + InitAddon(s, object()->GetSilencerName().c_str(), m_addon_offset[eSilencer], false); s->SetTextureColor(i->wnd()->GetTextureColor()); i->wnd()->AttachChild(s); } @@ -571,7 +571,7 @@ CUIDragItem* CUIWeaponCellItem::CreateDragItem() s = xr_new("Scope"); s->SetAutoDelete(true); s->SetShader(InventoryUtilities::GetEquipmentIconsShader()); - InitAddon(s, *object()->GetScopeName(), m_addon_offset[eScope], false); + InitAddon(s, object()->GetScopeName().c_str(), m_addon_offset[eScope], false); s->SetTextureColor(i->wnd()->GetTextureColor()); i->wnd()->AttachChild(s); } @@ -581,7 +581,7 @@ CUIDragItem* CUIWeaponCellItem::CreateDragItem() s = xr_new("Grenade launcher"); s->SetAutoDelete(true); s->SetShader(InventoryUtilities::GetEquipmentIconsShader()); - InitAddon(s, *object()->GetGrenadeLauncherName(), m_addon_offset[eLauncher], false); + InitAddon(s, object()->GetGrenadeLauncherName().c_str(), m_addon_offset[eLauncher], false); s->SetTextureColor(i->wnd()->GetTextureColor()); i->wnd()->AttachChild(s); } diff --git a/src/xrGame/ui/UIColorAnimatorWrapper.cpp b/src/xrGame/ui/UIColorAnimatorWrapper.cpp index a187a2885b8..70bf6d1cd31 100644 --- a/src/xrGame/ui/UIColorAnimatorWrapper.cpp +++ b/src/xrGame/ui/UIColorAnimatorWrapper.cpp @@ -33,7 +33,7 @@ CUIColorAnimatorWrapper::CUIColorAnimatorWrapper(u32* colorToModify) ////////////////////////////////////////////////////////////////////////// CUIColorAnimatorWrapper::CUIColorAnimatorWrapper(const shared_str& animationName) - : colorAnimation(LALib.FindItem(*animationName)), animationTime(0), color(NULL), isDone(false), reverse(false), + : colorAnimation(LALib.FindItem(animationName.c_str())), animationTime(0), color(NULL), isDone(false), reverse(false), kRev(0.0f) { VERIFY(colorAnimation); @@ -45,8 +45,8 @@ void CUIColorAnimatorWrapper::SetColorAnimation(const shared_str& animationName) { if (animationName.size() != 0) { - colorAnimation = LALib.FindItem(*animationName); - R_ASSERT2(colorAnimation, *animationName); + colorAnimation = LALib.FindItem(animationName.c_str()); + R_ASSERT2(colorAnimation, animationName.c_str()); } else { diff --git a/src/xrGame/ui/UIInventoryUtilities.cpp b/src/xrGame/ui/UIInventoryUtilities.cpp index 672124bf6c3..b89d340cb3f 100644 --- a/src/xrGame/ui/UIInventoryUtilities.cpp +++ b/src/xrGame/ui/UIInventoryUtilities.cpp @@ -334,20 +334,20 @@ LPCSTR InventoryUtilities::GetTimePeriodAsString(pstr _buff, u32 buff_sz, ALife: u8 yrdiff = ((year2 - year1) * 12); if (month1 != month2 || yrdiff > 0) - cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", month2 + (yrdiff - month1), *CStringTable().translate("ui_st_months")); + cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", month2 + (yrdiff - month1), CStringTable().translate("ui_st_months").c_str()); if (!cnt && day1 != day2) - cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", day2 - day1, *StringTable().translate("ui_st_days")); + cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", day2 - day1, StringTable().translate("ui_st_days").c_str()); if (!cnt && hours1 != hours2) cnt = - xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", hours2 - hours1, *StringTable().translate("ui_st_hours")); + xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", hours2 - hours1, StringTable().translate("ui_st_hours").c_str()); if (!cnt && mins1 != mins2) - cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", mins2 - mins1, *StringTable().translate("ui_st_mins")); + cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", mins2 - mins1, StringTable().translate("ui_st_mins").c_str()); if (!cnt && secs1 != secs2) - cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", secs2 - secs1, *StringTable().translate("ui_st_secs")); + cnt = xr_sprintf(_buff + cnt, buff_sz - cnt, "%d %s", secs2 - secs1, StringTable().translate("ui_st_secs").c_str()); return _buff; } diff --git a/src/xrGame/ui/UIItemInfo.cpp b/src/xrGame/ui/UIItemInfo.cpp index 9f7bd546be1..d5adfbdc300 100644 --- a/src/xrGame/ui/UIItemInfo.cpp +++ b/src/xrGame/ui/UIItemInfo.cpp @@ -276,7 +276,7 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, pItem->SetFont(m_desc_info.pDescFont); pItem->SetWidth(UIDesc->GetDesiredChildWidth()); pItem->SetTextComplexMode(true); - pItem->SetText(*pInvItem->ItemDescription()); + pItem->SetText(pInvItem->ItemDescription().c_str()); pItem->AdjustHeightToText(); UIDesc->AddWindow(pItem, true); } diff --git a/src/xrGame/ui/UIKeyBinding.cpp b/src/xrGame/ui/UIKeyBinding.cpp index f79c9e16266..11e11eb585c 100644 --- a/src/xrGame/ui/UIKeyBinding.cpp +++ b/src/xrGame/ui/UIKeyBinding.cpp @@ -80,7 +80,7 @@ void CUIKeyBinding::FillUpList(CUIXml& xml_doc_ui, LPCSTR path_ui) shared_str exe = xml_doc.ReadAttrib("command", j, "exe"); #ifdef DEBUG - if (kNOTBINDED == ActionNameToId(*exe)) + if (kNOTBINDED == ActionNameToId(exe.c_str())) { Msg("action [%s] not exist. update data", exe.c_str()); continue; @@ -187,7 +187,7 @@ bool CUIKeyBinding::IsActionExist(LPCSTR action, CUIXml& xml_doc) { // first field of list item shared_str command_id = xml_doc.ReadAttrib("command", j, "exe"); - if (0 == xr_strcmp(action, *command_id)) + if (0 == xr_strcmp(action, command_id.c_str())) { ret = true; break; diff --git a/src/xrGame/ui/UIListItemServer.cpp b/src/xrGame/ui/UIListItemServer.cpp index 66474c67410..cc1523e274d 100644 --- a/src/xrGame/ui/UIListItemServer.cpp +++ b/src/xrGame/ui/UIListItemServer.cpp @@ -132,7 +132,7 @@ void CUIListItemServer::CreateConsoleCommand( xr_string& command, LPCSTR player_name, LPCSTR player_pass, LPCSTR server_psw) { command = "start client("; - command += *m_srv_info.info.address; + command += m_srv_info.info.address.c_str(); command += "/name="; command += player_name; command += "/pass="; diff --git a/src/xrGame/ui/UIMainIngameWnd.cpp b/src/xrGame/ui/UIMainIngameWnd.cpp index eadd88bdae2..633acc52355 100644 --- a/src/xrGame/ui/UIMainIngameWnd.cpp +++ b/src/xrGame/ui/UIMainIngameWnd.cpp @@ -181,14 +181,14 @@ void CUIMainIngameWnd::Init() { // Читаем данные порогов для каждого индикатора shared_str cfgRecord = - pSettings->r_string("main_ingame_indicators_thresholds", *warningStrings[static_cast(j) - 1]); - u32 count = _GetItemCount(*cfgRecord); + pSettings->r_string("main_ingame_indicators_thresholds", warningStrings[static_cast(j) - 1].c_str()); + u32 count = _GetItemCount(cfgRecord.c_str()); char singleThreshold[8]; float f = 0; for (u32 k = 0; k < count; ++k) { - _GetItem(*cfgRecord, k, singleThreshold); + _GetItem(cfgRecord.c_str(), k, singleThreshold); sscanf(singleThreshold, "%f", &f); m_Thresholds[j].push_back(f); diff --git a/src/xrGame/ui/UIMap.cpp b/src/xrGame/ui/UIMap.cpp index 9e88f0ac3b3..a52bfde1f3f 100644 --- a/src/xrGame/ui/UIMap.cpp +++ b/src/xrGame/ui/UIMap.cpp @@ -451,7 +451,7 @@ void CUILevelMap::Init_internal(const shared_str& name, const CInifile& pLtx, co if (FALSE == fsimilar(kw, kh, EPS_L)) { - Msg(" --incorrect global rect definition for map [%s] kw=%f kh=%f", *MapName(), kw, kh); + Msg(" --incorrect global rect definition for map [%s] kw=%f kh=%f", MapName().c_str(), kw, kh); Msg(" --try x2=%f or y2=%f", m_GlobalRect.x1 + kh * BoundRect().width(), m_GlobalRect.y1 + kw * BoundRect().height()); } diff --git a/src/xrGame/ui/UIMapInfo.cpp b/src/xrGame/ui/UIMapInfo.cpp index 981659a3612..650bb7c1044 100644 --- a/src/xrGame/ui/UIMapInfo.cpp +++ b/src/xrGame/ui/UIMapInfo.cpp @@ -23,13 +23,13 @@ void CUIMapInfo::InitMapInfo(Fvector2 pos, Fvector2 size) } #define ADD_TEXT(x, y, z) \ - text = *StringTable().translate(x); \ + text = StringTable().translate(x).c_str(); \ text += ": "; \ text += txt_color_tag; \ if (ltx.line_exist("map_info", y)) \ - text += *StringTable().translate(ltx.r_string_wb("map_info", y)); \ + text += StringTable().translate(ltx.r_string_wb("map_info", y)).c_str(); \ else \ - text += *StringTable().translate(z); \ + text += StringTable().translate(z).c_str(); \ text += "%c[default]\\n"; \ st = xr_new("Text"); \ st->SetTextComplexMode(true); \ @@ -90,27 +90,27 @@ void CUIMapInfo::InitMap(LPCSTR map_name, LPCSTR map_ver) shared_str _modes = ltx.r_string_wb("map_info", "modes"); - text = *StringTable().translate("modes"); + text = StringTable().translate("modes").c_str(); text += ": "; text += txt_color_tag; bool b_ = false; if (strstr(_modes.c_str(), "st_deathmatch")) { - text += *StringTable().translate("st_deathmatch"); + text += StringTable().translate("st_deathmatch").c_str(); b_ = true; } if (strstr(_modes.c_str(), "st_team_deathmatch")) { if (b_) text += ", "; - text += *StringTable().translate("st_team_deathmatch"); + text += StringTable().translate("st_team_deathmatch").c_str(); b_ = true; } if (strstr(_modes.c_str(), "st_artefacthunt")) { if (b_) text += ", "; - text += *StringTable().translate("st_artefacthunt"); + text += StringTable().translate("st_artefacthunt").c_str(); } text += "%c[default]\\n"; @@ -140,4 +140,4 @@ void CUIMapInfo::InitMap(LPCSTR map_name, LPCSTR map_ver) } } -const char* CUIMapInfo::GetLargeDesc() { return *m_large_desc; } +const char* CUIMapInfo::GetLargeDesc() { return m_large_desc.c_str(); } diff --git a/src/xrGame/ui/UIMapList.cpp b/src/xrGame/ui/UIMapList.cpp index 036cae46c8f..39b7997d94a 100644 --- a/src/xrGame/ui/UIMapList.cpp +++ b/src/xrGame/ui/UIMapList.cpp @@ -287,7 +287,7 @@ void CUIMapList::SetServerParams(LPCSTR params) { m_srv_params = params; } void CUIMapList::AddWeather(const shared_str& WeatherType, const shared_str& WeatherTime, u32 _id) { R_ASSERT2(m_pWeatherSelector, "m_pWeatherSelector == NULL"); - m_pWeatherSelector->AddItem_(*WeatherType, 0)->SetTAG(_id); + m_pWeatherSelector->AddItem_(WeatherType.c_str(), 0)->SetTAG(_id); m_mapWeather.resize(m_mapWeather.size() + 1); m_mapWeather.back().weather_name = WeatherType; diff --git a/src/xrGame/ui/UIMapWnd.cpp b/src/xrGame/ui/UIMapWnd.cpp index 75df8e9e9cf..abebb22778e 100644 --- a/src/xrGame/ui/UIMapWnd.cpp +++ b/src/xrGame/ui/UIMapWnd.cpp @@ -211,13 +211,13 @@ bool CUIMapWnd::Init(cpcstr xml_name, cpcstr start_from, bool critical /*= true* VERIFY(l2); if (l->GlobalRect().intersected(l2->GlobalRect())) { - Msg(" --error-incorrect map definition global rect of map [%s] intersects with [%s]", *l->MapName(), - *l2->MapName()); + Msg(" --error-incorrect map definition global rect of map [%s] intersects with [%s]", l->MapName().c_str(), + l2->MapName().c_str()); } } if (FALSE == l->GlobalRect().intersected(GlobalMap()->BoundRect())) { - Msg(" --error-incorrect map definition map [%s] places outside global map", *l->MapName()); + Msg(" --error-incorrect map definition map [%s] places outside global map", l->MapName().c_str()); } } #endif diff --git a/src/xrGame/ui/UIOutfitSlot.cpp b/src/xrGame/ui/UIOutfitSlot.cpp index afb48713d17..637b67c81b2 100644 --- a/src/xrGame/ui/UIOutfitSlot.cpp +++ b/src/xrGame/ui/UIOutfitSlot.cpp @@ -36,9 +36,9 @@ void CUIOutfitDragDropList::SetOutfit(CUICellItem* itm) xr_string iconName; if (pActor) - iconName = *pActor->cNameVisual(); + iconName = pActor->cNameVisual().c_str(); else - iconName = *m_default_outfit; + iconName = m_default_outfit.c_str(); xr_string::iterator it = std::find(iconName.rbegin(), iconName.rend(), '\\').base(); diff --git a/src/xrGame/ui/UISkinSelector.cpp b/src/xrGame/ui/UISkinSelector.cpp index dc1185138d3..3067a2a6773 100644 --- a/src/xrGame/ui/UISkinSelector.cpp +++ b/src/xrGame/ui/UISkinSelector.cpp @@ -29,8 +29,8 @@ CUISkinSelectorWnd::~CUISkinSelectorWnd() void CUISkinSelectorWnd::InitSkins() { - R_ASSERT2(pSettings->section_exist(m_strSection), *m_strSection); - R_ASSERT2(pSettings->line_exist(m_strSection, "skins"), *m_strSection); + R_ASSERT2(pSettings->section_exist(m_strSection), m_strSection.c_str()); + R_ASSERT2(pSettings->line_exist(m_strSection, "skins"), m_strSection.c_str()); LPCSTR lst = pSettings->r_string(m_strSection, "skins"); string256 singleItem; diff --git a/src/xrGame/ui/UIStatsPlayerInfo.cpp b/src/xrGame/ui/UIStatsPlayerInfo.cpp index ec0ea7ca87f..97a43e947da 100644 --- a/src/xrGame/ui/UIStatsPlayerInfo.cpp +++ b/src/xrGame/ui/UIStatsPlayerInfo.cpp @@ -70,7 +70,7 @@ void CUIStatsPlayerInfo::Update() xr_vector& field_info = *m_field_info; for (u32 i = 0; i < m_fields.size(); i++) - m_fields[i]->TextItemControl()->SetText(GetInfoByID(*field_info[i].name)); + m_fields[i]->TextItemControl()->SetText(GetInfoByID(field_info[i].name.c_str())); m_pPlayerInfo = NULL; } @@ -145,7 +145,7 @@ const char* CUIStatsPlayerInfo::GetInfoByID(const char* id) else if (0 == xr_strcmp(id, "status")) { if (m_pPlayerInfo->testFlag(GAME_PLAYER_FLAG_READY)) - xr_strcpy(ans, *StringTable().translate("st_mp_ready")); + xr_strcpy(ans, StringTable().translate("st_mp_ready").c_str()); else xr_strcpy(ans, ""); } diff --git a/src/xrGame/ui/UIStatsPlayerList.cpp b/src/xrGame/ui/UIStatsPlayerList.cpp index 06d72e00391..384b7f40259 100644 --- a/src/xrGame/ui/UIStatsPlayerList.cpp +++ b/src/xrGame/ui/UIStatsPlayerList.cpp @@ -134,7 +134,7 @@ void CUIStatsPlayerList::InitHeader(CUIXml& xml_doc, LPCSTR path) st->SetText(""); else { - st->SetTextST(GetST_entry(*m_field_info[i].name)); + st->SetTextST(GetST_entry(m_field_info[i].name.c_str())); } if (m_h.f) @@ -234,16 +234,16 @@ void CUIStatsPlayerList::Update() { game_cl_ArtefactHunt* game = static_cast(&Game()); pl_artefacts = game->teams[m_CurTeam - 1].score; - xr_sprintf(teaminfo, "%s: %u, %s: %u, %s: %d", *StringTable().translate("mp_artefacts_upcase"), pl_artefacts, - *StringTable().translate("mp_players"), pl_count, *StringTable().translate("mp_frags_upcase"), pl_frags); + xr_sprintf(teaminfo, "%s: %u, %s: %u, %s: %d", StringTable().translate("mp_artefacts_upcase").c_str(), pl_artefacts, + StringTable().translate("mp_players").c_str(), pl_count, StringTable().translate("mp_frags_upcase").c_str(), pl_frags); m_header_text->SetText(teaminfo); } else if (GameID() == eGameIDTeamDeathmatch && !m_bSpectator) { game_cl_TeamDeathmatch* game = static_cast(&Game()); pl_frags = game->teams[m_CurTeam - 1].score; - xr_sprintf(teaminfo, "%s: %d, %s: %u", *StringTable().translate("mp_frags_upcase"), pl_frags, - *StringTable().translate("mp_players"), pl_count); + xr_sprintf(teaminfo, "%s: %d, %s: %u", StringTable().translate("mp_frags_upcase").c_str(), pl_frags, + StringTable().translate("mp_players").c_str(), pl_count); m_header_text->SetText(teaminfo); } diff --git a/src/xrGame/ui/UITalkWnd.cpp b/src/xrGame/ui/UITalkWnd.cpp index ce57eb257d1..69b277e88c0 100644 --- a/src/xrGame/ui/UITalkWnd.cpp +++ b/src/xrGame/ui/UITalkWnd.cpp @@ -316,7 +316,7 @@ void CUITalkWnd::AddAnswer(const shared_str& text, LPCSTR SpeakerName) PlaySnd(text.c_str()); bool i_am = (0 == xr_strcmp(SpeakerName, m_pOurInvOwner->Name())); // XXX: not reliable when both persons have same names - UITalkDialogWnd->AddAnswer(SpeakerName, *StringTable().translate(text), i_am); + UITalkDialogWnd->AddAnswer(SpeakerName, StringTable().translate(text).c_str(), i_am); } void CUITalkWnd::SwitchToTrade() diff --git a/src/xrGame/vision_client.cpp b/src/xrGame/vision_client.cpp index 88c4aef6685..eaa59705027 100644 --- a/src/xrGame/vision_client.cpp +++ b/src/xrGame/vision_client.cpp @@ -98,7 +98,7 @@ void vision_client::shedule_Update(u32 dt) shared_str vision_client::shedule_Name() const { string256 temp; - xr_sprintf(temp, "vision_client[%s]", *object().cName()); + xr_sprintf(temp, "vision_client[%s]", object().cName().c_str()); return (temp); } diff --git a/src/xrGame/xrGameSpyServer.cpp b/src/xrGame/xrGameSpyServer.cpp index 8a213df56b4..6d80d85799c 100644 --- a/src/xrGame/xrGameSpyServer.cpp +++ b/src/xrGame/xrGameSpyServer.cpp @@ -63,7 +63,7 @@ xrGameSpyServer::EConnect xrGameSpyServer::Connect(shared_str& session_name, Gam if (res != ErrNoError) return res; - if (0 == *(game->get_option_s(*session_name, "hname", NULL))) + if (0 == *(game->get_option_s(session_name.c_str(), "hname", NULL))) { #ifdef XR_PLATFORM_WINDOWS // XXX: add Linux implementation string1024 CompName; @@ -73,20 +73,20 @@ xrGameSpyServer::EConnect xrGameSpyServer::Connect(shared_str& session_name, Gam #endif } else - HostName = game->get_option_s(*session_name, "hname", NULL); + HostName = game->get_option_s(session_name.c_str(), "hname", NULL); - if (0 != *(game->get_option_s(*session_name, "psw", NULL))) - Password._set(game->get_option_s(*session_name, "psw", NULL)); + if (0 != *(game->get_option_s(session_name.c_str(), "psw", NULL))) + Password._set(game->get_option_s(session_name.c_str(), "psw", NULL)); string4096 tMapName = ""; - const char* SName = *session_name; - strncpy_s(tMapName, *session_name, strchr(SName, '/') - SName); + const char* SName = session_name.c_str(); + strncpy_s(tMapName, session_name.c_str(), strchr(SName, '/') - SName); MapName = tMapName; // = (session_name); - m_iReportToMasterServer = game->get_option_i(*session_name, "public", 0); - m_iMaxPlayers = game->get_option_i(*session_name, "maxplayers", 32); + m_iReportToMasterServer = game->get_option_i(session_name.c_str(), "public", 0); + m_iMaxPlayers = game->get_option_i(session_name.c_str(), "maxplayers", 32); // m_bCheckCDKey = game->get_option_i (*session_name,"cdkey",0) != 0; - m_bCheckCDKey = game->get_option_i(*session_name, "public", 0) != 0; + m_bCheckCDKey = game->get_option_i(session_name.c_str(), "public", 0) != 0; //--------------------------------------------// if (game->Type() != eGameIDSingle) { @@ -95,11 +95,11 @@ xrGameSpyServer::EConnect xrGameSpyServer::Connect(shared_str& session_name, Gam shared_str result_string; if (!GSA.CheckAvailableServices(result_string)) { - Msg(*result_string); + Msg(session_name.c_str()); }; //------ Init of QR2 SDK ------------- - iGameSpyBasePort = game->get_option_i(*session_name, "portgs", -1); + iGameSpyBasePort = game->get_option_i(session_name.c_str(), "portgs", -1); QR2_Init(iGameSpyBasePort); //------ Init of CDKey SDK ----------- diff --git a/src/xrGame/xrServer_Connect.cpp b/src/xrGame/xrServer_Connect.cpp index 50f7bbd5a43..9ceb9721f15 100644 --- a/src/xrGame/xrServer_Connect.cpp +++ b/src/xrGame/xrServer_Connect.cpp @@ -36,16 +36,16 @@ LPCSTR xrServer::get_map_download_url(LPCSTR level_name, LPCSTR level_version) xrServer::EConnect xrServer::Connect(shared_str& session_name, GameDescriptionData& game_descr) { #ifdef DEBUG - Msg("* sv_Connect: %s", *session_name); + Msg("* sv_Connect: %s", session_name.c_str()); #endif // Parse options and create game - if (0 == strchr(*session_name, '/')) + if (0 == strchr(session_name.c_str(), '/')) return ErrConnect; string1024 options; R_ASSERT2(xr_strlen(session_name) <= sizeof(options), "session_name too BIIIGGG!!!"); - xr_strcpy(options, strchr(*session_name, '/') + 1); + xr_strcpy(options, strchr(session_name.c_str(), '/') + 1); // Parse game type string1024 type; @@ -83,7 +83,7 @@ xrServer::EConnect xrServer::Connect(shared_str& session_name, GameDescriptionDa game->Create(session_name); - return IPureServer::Connect(*session_name, game_descr); + return IPureServer::Connect(session_name.c_str(), game_descr); } IClient* xrServer::new_client(SClientConnectData* cl_data) diff --git a/src/xrGame/xrServer_process_event.cpp b/src/xrGame/xrServer_process_event.cpp index 64e0037dca5..e7e43a099fd 100644 --- a/src/xrGame/xrServer_process_event.cpp +++ b/src/xrGame/xrServer_process_event.cpp @@ -202,7 +202,7 @@ void xrServer::Process_event(NET_Packet& P, ClientID sender) #ifndef MASTER_GOLD if (game->Type() != eGameIDSingle) - Msg("* [%2d] is [%s:%s]", id_dest, *e_dest->s_name, e_dest->name_replace()); + Msg("* [%2d] is [%s:%s]", id_dest, e_dest->s_name.c_str(), e_dest->name_replace()); #endif // #ifndef MASTER_GOLD CSE_Abstract* e_src = game->get_entity_from_eid(id_src); // кто убил @@ -221,7 +221,7 @@ void xrServer::Process_event(NET_Packet& P, ClientID sender) // R_ASSERT2 (e_dest && e_src, "Killer or/and being killed are offline or not exist at all :("); #ifndef MASTER_GOLD if (game->Type() != eGameIDSingle) - Msg("* [%2d] is [%s:%s]", id_src, *e_src->s_name, e_src->name_replace()); + Msg("* [%2d] is [%s:%s]", id_src, e_src->s_name.c_str(), e_src->name_replace()); #endif // #ifndef MASTER_GOLD game->on_death(e_dest, e_src); diff --git a/src/xrGame/xrServer_process_event_activate.cpp b/src/xrGame/xrServer_process_event_activate.cpp index 185167ffdad..b5060ce02be 100644 --- a/src/xrGame/xrServer_process_event_activate.cpp +++ b/src/xrGame/xrServer_process_event_activate.cpp @@ -28,7 +28,7 @@ void xrServer::Process_event_activate( { #ifndef MASTER_GOLD Msg("~ ERROR: can't activate independant object. entity[%s:%d], parent[%s:%d], section[%s]", - e_entity->name_replace(), id_entity, e_parent->name_replace(), id_parent, *e_entity->s_name); + e_entity->name_replace(), id_entity, e_parent->name_replace(), id_parent, e_entity->s_name.c_str()); #endif // #ifndef MASTER_GOLD return; } diff --git a/src/xrGame/xrServer_process_spawn.cpp b/src/xrGame/xrServer_process_spawn.cpp index a4cf8657944..3f1b973a615 100644 --- a/src/xrGame/xrServer_process_spawn.cpp +++ b/src/xrGame/xrServer_process_spawn.cpp @@ -32,7 +32,7 @@ CSE_Abstract* xrServer::Process_spawn( !E->m_gameType.MatchType((u16)game->Type()) || !E->match_configuration() || !game->OnPreCreate(E)) { #ifndef MASTER_GOLD - Msg("- SERVER: Entity [%s] incompatible with current game type.", *E->s_name); + Msg("- SERVER: Entity [%s] incompatible with current game type.", E->s_name.c_str()); #endif // #ifndef MASTER_GOLD F_entity_Destroy(E); return NULL; @@ -76,7 +76,7 @@ CSE_Abstract* xrServer::Process_spawn( if (E->RespawnTime && (0xffff == E->ID_Phantom)) { // Create phantom - CSE_Abstract* Phantom = entity_Create(*E->s_name); + CSE_Abstract* Phantom = entity_Create(E->s_name.c_str()); R_ASSERT(Phantom); Phantom->Spawn_Read(P); Phantom->ID = PerformIDgen(0xffff); diff --git a/src/xrGame/xr_time.cpp b/src/xrGame/xr_time.cpp index 56fca449c6a..39ebb80bd5c 100644 --- a/src/xrGame/xr_time.cpp +++ b/src/xrGame/xr_time.cpp @@ -17,11 +17,11 @@ u32 get_time() { return u32(__game_time() & u32(-1)); } xrTime get_time_struct() { return xrTime(__game_time()); } LPCSTR xrTime::dateToString(int mode) { - return *InventoryUtilities::GetDateAsString(m_time, (InventoryUtilities::EDatePrecision)mode); + return InventoryUtilities::GetDateAsString(m_time, (InventoryUtilities::EDatePrecision)mode).c_str(); } LPCSTR xrTime::timeToString(int mode) { - return *InventoryUtilities::GetTimeAsString(m_time, (InventoryUtilities::ETimePrecision)mode); + return InventoryUtilities::GetTimeAsString(m_time, (InventoryUtilities::ETimePrecision)mode).c_str(); } void xrTime::add(const xrTime& other) { m_time += other.m_time; } diff --git a/src/xrGame/zone_effector.cpp b/src/xrGame/zone_effector.cpp index 21fde1167a9..90523af7ebe 100644 --- a/src/xrGame/zone_effector.cpp +++ b/src/xrGame/zone_effector.cpp @@ -41,7 +41,7 @@ void CZoneEffector::Activate() m_pp_effector->SetType(EEffectorPPType(u32(u64(this) & u32(-1)))); m_pp_effector->SetCyclic(true); m_pp_effector->SetFactorFunc(GET_KOEFF_FUNC(this, &CZoneEffector::GetFactor)); - m_pp_effector->Load(*m_pp_fname); + m_pp_effector->Load(m_pp_fname.c_str()); m_pActor->Cameras().AddPPEffector(m_pp_effector); } diff --git a/src/xrNetServer/empty/NET_Client.cpp b/src/xrNetServer/empty/NET_Client.cpp index c898d533ea1..d514fd58e1f 100644 --- a/src/xrNetServer/empty/NET_Client.cpp +++ b/src/xrNetServer/empty/NET_Client.cpp @@ -406,7 +406,7 @@ bool IPureClient::Connect(pcstr options) net_csEnumeration->Enter(); // real connect for (u32 I = 0; I < net_Hosts.size(); I++) - Msg("* HOST #%d: %s\n", I + 1, *net_Hosts[I].dpSessionName); + Msg("* HOST #%d: %s\n", I + 1, net_Hosts[I].dpSessionName.c_str()); if (res != S_OK) return false; diff --git a/src/xrNetServer/empty/NET_Client.h b/src/xrNetServer/empty/NET_Client.h index 2f3b20c09cb..407441afac8 100644 --- a/src/xrNetServer/empty/NET_Client.h +++ b/src/xrNetServer/empty/NET_Client.h @@ -105,7 +105,7 @@ class XRNETSERVER_API IPureClient : MultipacketReciever, { return m_game_description; } - pcstr net_SessionName() { return *net_Hosts.front().dpSessionName; } + pcstr net_SessionName() { return net_Hosts.front().dpSessionName.c_str(); } // receive void StartProcessQueue() { diff --git a/src/xrPhysics/PHScriptCall.cpp b/src/xrPhysics/PHScriptCall.cpp index 9f97add8ccb..b10496852c6 100644 --- a/src/xrPhysics/PHScriptCall.cpp +++ b/src/xrPhysics/PHScriptCall.cpp @@ -66,7 +66,7 @@ bool CPHScriptObjectAction::compare(const CPHScriptObjectAction* v) const } void CPHScriptObjectAction::run() { - luabind::call_member(*m_lua_object, *m_method_name); + luabind::call_member(*m_lua_object, m_method_name.c_str()); b_obsolete = true; } @@ -90,7 +90,7 @@ bool CPHScriptObjectCondition::compare(const CPHScriptObjectCondition* v) const return m_method_name == v->m_method_name && compare_safe(*m_lua_object, *(v->m_lua_object)); } -bool CPHScriptObjectCondition::is_true() { return luabind::call_member(*m_lua_object, *m_method_name); } +bool CPHScriptObjectCondition::is_true() { return luabind::call_member(*m_lua_object, m_method_name.c_str()); } bool CPHScriptObjectCondition::obsolete() const { return false; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// CPHScriptObjectActionN::CPHScriptObjectActionN(const luabind::object& object, const luabind::functor& functor) diff --git a/src/xrServerEntities/PropertiesListTypes.h b/src/xrServerEntities/PropertiesListTypes.h index e113ddce11f..cb1ad436061 100644 --- a/src/xrServerEntities/PropertiesListTypes.h +++ b/src/xrServerEntities/PropertiesListTypes.h @@ -765,7 +765,7 @@ class RTokenValue : public CustomValue, public RTokenValueCustom else for (u32 k = 0; k < token_count; k++) if ((T)token[k].id == this->GetValue()) - return *token[k].name; + return token[k].name.c_str(); return draw_val; } }; diff --git a/src/xrServerEntities/object_factory_script.cpp b/src/xrServerEntities/object_factory_script.cpp index 8ba708028c9..011e9490806 100644 --- a/src/xrServerEntities/object_factory_script.cpp +++ b/src/xrServerEntities/object_factory_script.cpp @@ -75,7 +75,7 @@ void CObjectFactory::register_script() const const_iterator I = clsids().begin(), B = I; const_iterator E = clsids().end(); for (; I != E; ++I) - instance.enum_("_clsid")[luabind::value(*(*I)->script_clsid(), int(I - B))]; + instance.enum_("_clsid")[luabind::value((*I)->script_clsid().c_str(), int(I - B))]; luabind::module(GEnv.ScriptEngine->lua())[instance]; } diff --git a/src/xrServerEntities/script_net_packet_script.cpp b/src/xrServerEntities/script_net_packet_script.cpp index 2cbf48a1b6d..5524bd877ac 100644 --- a/src/xrServerEntities/script_net_packet_script.cpp +++ b/src/xrServerEntities/script_net_packet_script.cpp @@ -84,7 +84,7 @@ void CScriptNetPacket::script_register(lua_State* luaState) { shared_str temp; self->r_stringZ(temp); - return (*temp); + return (temp.c_str()); }) .def("r_matrix", &NET_Packet::r_matrix) .def("r_clientID", +[](NET_Packet* self) diff --git a/src/xrServerEntities/xrServer_Objects_ALife_Monsters_script.cpp b/src/xrServerEntities/xrServer_Objects_ALife_Monsters_script.cpp index cc119da67f4..4e2137645b2 100644 --- a/src/xrServerEntities/xrServer_Objects_ALife_Monsters_script.cpp +++ b/src/xrServerEntities/xrServer_Objects_ALife_Monsters_script.cpp @@ -38,7 +38,7 @@ void CSE_ALifeTraderAbstract::script_register(lua_State* luaState) selected_char.Load(ta->m_SpecificCharacter); ta->m_icon_name = selected_char.IconName(); } - return *ta->m_icon_name; + return ta->m_icon_name.c_str(); }) ]; } diff --git a/src/xrServerEntities/xrServer_Objects_Alife_Smartcovers.cpp b/src/xrServerEntities/xrServer_Objects_Alife_Smartcovers.cpp index 757eb479386..6413f4397c8 100644 --- a/src/xrServerEntities/xrServer_Objects_Alife_Smartcovers.cpp +++ b/src/xrServerEntities/xrServer_Objects_Alife_Smartcovers.cpp @@ -29,7 +29,7 @@ BOOL is_combat_cover(shared_str const& table_id) string256 temp; xr_strcpy(temp, "smart_covers.descriptions."); - xr_strcat(temp, *table_id); + xr_strcat(temp, table_id.c_str()); luabind::object table, value; bool result = GEnv.ScriptEngine->function_object(temp, table, LUA_TTABLE); @@ -129,20 +129,20 @@ void CSE_SmartCover::UPDATE_Write(NET_Packet& tNetPacket) { inherited1::UPDATE_W #ifndef MASTER_GOLD void CSE_SmartCover::FillProps(LPCSTR pref, PropItemVec& items) { - PHelper().CreateFloat(items, PrepareKey(pref, *s_name, "hold position time"), &m_hold_position_time, 0.f, 60.f); - RListValue* value = PHelper().CreateRList(items, PrepareKey(pref, *s_name, "description"), &m_description, + PHelper().CreateFloat(items, PrepareKey(pref, s_name.c_str(), "hold position time"), &m_hold_position_time, 0.f, 60.f); + RListValue* value = PHelper().CreateRList(items, PrepareKey(pref, s_name.c_str(), "description"), &m_description, &*fp_data.smart_covers.begin(), fp_data.smart_covers.size()); value->OnChangeEvent.bind(this, &CSE_SmartCover::OnChangeDescription); PHelper().CreateFloat( - items, PrepareKey(pref, *s_name, "enter min enemy distance"), &m_enter_min_enemy_distance, 0.f, 100.f); + items, PrepareKey(pref, s_name.c_str(), "enter min enemy distance"), &m_enter_min_enemy_distance, 0.f, 100.f); PHelper().CreateFloat( - items, PrepareKey(pref, *s_name, "exit min enemy distance"), &m_exit_min_enemy_distance, 0.f, 100.f); + items, PrepareKey(pref, s_name.c_str(), "exit min enemy distance"), &m_exit_min_enemy_distance, 0.f, 100.f); if (is_combat_cover(m_description)) { - PHelper().CreateBOOL(items, PrepareKey(pref, *s_name, "is combat cover"), &m_is_combat_cover); - PHelper().CreateBOOL(items, PrepareKey(pref, *s_name, "can fire"), &m_can_fire); + PHelper().CreateBOOL(items, PrepareKey(pref, s_name.c_str(), "is combat cover"), &m_is_combat_cover); + PHelper().CreateBOOL(items, PrepareKey(pref, s_name.c_str(), "can fire"), &m_can_fire); } } #endif // #ifndef MASTER_GOLD From 11246027dbf9c82d176280a4a85b5ca2a6795e37 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 24 Dec 2025 16:36:54 +0300 Subject: [PATCH 101/261] Support for SOC-style CUIItemInfo and CUIConditionParams (#392) --- src/xrGame/ui/UIItemInfo.cpp | 85 ++++++++++++++++++++++------------- src/xrGame/ui/UIWpnParams.cpp | 21 ++++++--- 2 files changed, 70 insertions(+), 36 deletions(-) diff --git a/src/xrGame/ui/UIItemInfo.cpp b/src/xrGame/ui/UIItemInfo.cpp index d5adfbdc300..773ea1b568c 100644 --- a/src/xrGame/ui/UIItemInfo.cpp +++ b/src/xrGame/ui/UIItemInfo.cpp @@ -92,12 +92,23 @@ bool CUIItemInfo::InitItemInfo(cpcstr xml_name) UICost = UIHelper::CreateStatic(uiXml, "static_cost", this, false); UITradeTip = UIHelper::CreateStatic(uiXml, "static_no_trade", this, false); - if (uiXml.NavigateToNode("descr_list", 0)) + const bool descr_list = !!uiXml.NavigateToNode("descr_list", 0); + const bool condition_progress = !!uiXml.NavigateToNode("condition_progress", 0); + + if (descr_list || condition_progress) { UIConditionWnd = xr_new(); if (!UIConditionWnd->InitFromXml(uiXml)) xr_delete(UIConditionWnd); + else if (condition_progress) // SOC + { + UIConditionWnd->SetWndRect(GetWndRect()); // don't like but meh + AttachChild(UIConditionWnd); + } + } + if (descr_list) + { UIWpnParams = xr_new(); if (!UIWpnParams->InitFromXml(uiXml)) xr_delete(UIWpnParams); @@ -175,13 +186,15 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, if (!m_pInvItem) return; - Fvector2 pos; - pos.set(0.0f, 0.0f); + const bool soc_style = IsChild(UIConditionWnd); + + Fvector2 pos{}; string256 str; if (UIName) { UIName->SetText(pInvItem->NameItem()); - UIName->AdjustHeightToText(); + if (!soc_style) + UIName->AdjustHeightToText(); pos.y = UIName->GetWndPos().y + UIName->GetHeight() + 4.0f; } if (UIWeight) @@ -191,7 +204,7 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, shared_str kg_str = "kg"; StringTable().translate("st_kg", kg_str); - if (!weight) + if (!fis_zero(weight)) { if (/*CWeaponAmmo* ammo =*/ smart_cast(pInvItem)) { @@ -216,9 +229,15 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, } if (UICost && IsGameTypeSingle()) { - if (item_price != u32(-1)) + if (soc_style && item_price == u32(-1)) + item_price = pInvItem->Cost(); + + if (item_price == u32(-1)) + UICost->Show(false); + else { - xr_sprintf(str, "%d %s", item_price, StringTable().GetCurrency().c_str()); // will be owerwritten in multiplayer + xr_sprintf(str, "%d %s", item_price, StringTable().GetCurrency().c_str()); + // will be overwritten in multiplayer UICost->SetText(str); pos.x = UICost->GetWndPos().x; if (m_complex_desc) @@ -227,17 +246,7 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, } UICost->Show(true); } - else - UICost->Show(false); } - - // CActor* actor = smart_cast( Level().CurrentViewEntity() ); - // if ( g_pGameLevel && Level().game && actor ) - // { - // game_cl_Deathmatch* gs_mp = smart_cast( Game() ); - // IBuyWnd* buy_menu = gs_mp->pCurBuyMenu->GetItemPrice(); - // GetItemPrice(); - // } if (UITradeTip && IsGameTypeSingle()) { pos.y = UITradeTip->GetWndPos().y; @@ -252,11 +261,11 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, { UITradeTip->SetText(StringTable().translate(trade_tip).c_str()); UITradeTip->AdjustHeightToText(); - UITradeTip->SetWndPos(pos); + if (!soc_style || m_complex_desc) + UITradeTip->SetWndPos(pos); UITradeTip->Show(true); } } - if (UIDesc) { pos = UIDesc->GetWndPos(); @@ -266,19 +275,22 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, if (UITradeTip && trade_tip != NULL) pos.y = UITradeTip->GetWndPos().y + UITradeTip->GetHeight() + 4.0f; - UIDesc->SetWndPos(pos); + if (!soc_style || m_complex_desc) + UIDesc->SetWndPos(pos); UIDesc->Clear(); VERIFY(0 == UIDesc->GetSize()); + + CUIStatic* descr{}; if (m_desc_info.bShowDescrText) { - auto* pItem = xr_new("Description"); - pItem->SetTextColor(m_desc_info.uDescClr); - pItem->SetFont(m_desc_info.pDescFont); - pItem->SetWidth(UIDesc->GetDesiredChildWidth()); - pItem->SetTextComplexMode(true); - pItem->SetText(pInvItem->ItemDescription().c_str()); - pItem->AdjustHeightToText(); - UIDesc->AddWindow(pItem, true); + descr = xr_new("Description"); + descr->SetTextColor(m_desc_info.uDescClr); + descr->SetFont(m_desc_info.pDescFont); + descr->SetWidth(UIDesc->GetDesiredChildWidth()); + descr->SetTextComplexMode(true); + descr->SetText(pInvItem->ItemDescription().c_str()); + descr->AdjustHeightToText(); + UIDesc->AddWindow(descr, !soc_style); } TryAddConditionInfo(*pInvItem, pCompareItem); TryAddWpnInfo(*pInvItem, pCompareItem); @@ -287,6 +299,12 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, TryAddUpgradeInfo(*pInvItem); TryAddBoosterInfo(*pInvItem); + if (descr && soc_style) + { + UIDesc->RemoveWindow(descr); + UIDesc->AddWindow(descr, true); + } + if (m_b_FitToHeight) { UIDesc->SetWndSize(Fvector2().set(UIDesc->GetWndSize().x, UIDesc->GetPadSize().y)); @@ -316,11 +334,17 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, UIItemImage->GetUIStaticItem().SetTextureRect(texture_rect); UIItemImage->TextureOn(); UIItemImage->SetStretchTexture(true); - Fvector2 v_r = {item_grid_rect.x2 * INV_GRID_WIDTH2, item_grid_rect.y2 * INV_GRID_HEIGHT2}; + Fvector2 v_r = + { + item_grid_rect.x2 * (soc_style ? INV_GRID_WIDTH : INV_GRID_WIDTH2), + item_grid_rect.y2 * (soc_style ? INV_GRID_HEIGHT : INV_GRID_HEIGHT2) + }; v_r.x *= UI().get_current_kx(); UIItemImage->GetUIStaticItem().SetSize(v_r); + if (soc_style) + v_r.min(UIItemImageSize); UIItemImage->SetWidth(v_r.x); UIItemImage->SetHeight(v_r.y); } @@ -336,7 +360,8 @@ void CUIItemInfo::TryAddConditionInfo(CInventoryItem& pInvItem, CInventoryItem* if (weapon || outfit) { UIConditionWnd->SetInfo(pCompareItem, pInvItem); - UIDesc->AddWindow(UIConditionWnd, false); + if (!IsChild(UIConditionWnd)) + UIDesc->AddWindow(UIConditionWnd, false); } } diff --git a/src/xrGame/ui/UIWpnParams.cpp b/src/xrGame/ui/UIWpnParams.cpp index bc001ebc5a1..7726172130a 100644 --- a/src/xrGame/ui/UIWpnParams.cpp +++ b/src/xrGame/ui/UIWpnParams.cpp @@ -262,12 +262,21 @@ CUIConditionParams::CUIConditionParams() bool CUIConditionParams::InitFromXml(CUIXml& xml_doc) { - if (!xml_doc.NavigateToNode("condition_params", 0)) - return false; - CUIXmlInit::InitWindow(xml_doc, "condition_params", 0, this); - CUIXmlInit::InitStatic(xml_doc, "condition_params:caption", 0, &m_text); - m_progress.InitFromXml(xml_doc, "condition_params:progress_state"); - return true; + if (xml_doc.NavigateToNode("condition_params", 0)) + { + CUIXmlInit::InitWindow(xml_doc, "condition_params", 0, this); + CUIXmlInit::InitStatic(xml_doc, "condition_params:caption", 0, &m_text); + m_progress.InitFromXml(xml_doc, "condition_params:progress_state"); + return true; + } + // SOC + if (xml_doc.NavigateToNode("condition_progress", 0)) + { + CUIXmlInit::InitStatic(xml_doc, "static_condition", 0, &m_text, false); + m_progress.InitFromXml(xml_doc, "condition_progress"); + return true; + } + return false; } void CUIConditionParams::SetInfo(CInventoryItem const* slot_item, CInventoryItem const& cur_item) From 583bee7e663639dc29966b139910ff3323102569 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 24 Dec 2025 16:36:54 +0300 Subject: [PATCH 102/261] Support for SOC-style CUIItemInfo and CUIConditionParams (#392) --- src/xrGame/ui/UIItemInfo.cpp | 85 ++++++++++++++++++++++------------- src/xrGame/ui/UIWpnParams.cpp | 21 ++++++--- 2 files changed, 70 insertions(+), 36 deletions(-) diff --git a/src/xrGame/ui/UIItemInfo.cpp b/src/xrGame/ui/UIItemInfo.cpp index d5adfbdc300..773ea1b568c 100644 --- a/src/xrGame/ui/UIItemInfo.cpp +++ b/src/xrGame/ui/UIItemInfo.cpp @@ -92,12 +92,23 @@ bool CUIItemInfo::InitItemInfo(cpcstr xml_name) UICost = UIHelper::CreateStatic(uiXml, "static_cost", this, false); UITradeTip = UIHelper::CreateStatic(uiXml, "static_no_trade", this, false); - if (uiXml.NavigateToNode("descr_list", 0)) + const bool descr_list = !!uiXml.NavigateToNode("descr_list", 0); + const bool condition_progress = !!uiXml.NavigateToNode("condition_progress", 0); + + if (descr_list || condition_progress) { UIConditionWnd = xr_new(); if (!UIConditionWnd->InitFromXml(uiXml)) xr_delete(UIConditionWnd); + else if (condition_progress) // SOC + { + UIConditionWnd->SetWndRect(GetWndRect()); // don't like but meh + AttachChild(UIConditionWnd); + } + } + if (descr_list) + { UIWpnParams = xr_new(); if (!UIWpnParams->InitFromXml(uiXml)) xr_delete(UIWpnParams); @@ -175,13 +186,15 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, if (!m_pInvItem) return; - Fvector2 pos; - pos.set(0.0f, 0.0f); + const bool soc_style = IsChild(UIConditionWnd); + + Fvector2 pos{}; string256 str; if (UIName) { UIName->SetText(pInvItem->NameItem()); - UIName->AdjustHeightToText(); + if (!soc_style) + UIName->AdjustHeightToText(); pos.y = UIName->GetWndPos().y + UIName->GetHeight() + 4.0f; } if (UIWeight) @@ -191,7 +204,7 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, shared_str kg_str = "kg"; StringTable().translate("st_kg", kg_str); - if (!weight) + if (!fis_zero(weight)) { if (/*CWeaponAmmo* ammo =*/ smart_cast(pInvItem)) { @@ -216,9 +229,15 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, } if (UICost && IsGameTypeSingle()) { - if (item_price != u32(-1)) + if (soc_style && item_price == u32(-1)) + item_price = pInvItem->Cost(); + + if (item_price == u32(-1)) + UICost->Show(false); + else { - xr_sprintf(str, "%d %s", item_price, StringTable().GetCurrency().c_str()); // will be owerwritten in multiplayer + xr_sprintf(str, "%d %s", item_price, StringTable().GetCurrency().c_str()); + // will be overwritten in multiplayer UICost->SetText(str); pos.x = UICost->GetWndPos().x; if (m_complex_desc) @@ -227,17 +246,7 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, } UICost->Show(true); } - else - UICost->Show(false); } - - // CActor* actor = smart_cast( Level().CurrentViewEntity() ); - // if ( g_pGameLevel && Level().game && actor ) - // { - // game_cl_Deathmatch* gs_mp = smart_cast( Game() ); - // IBuyWnd* buy_menu = gs_mp->pCurBuyMenu->GetItemPrice(); - // GetItemPrice(); - // } if (UITradeTip && IsGameTypeSingle()) { pos.y = UITradeTip->GetWndPos().y; @@ -252,11 +261,11 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, { UITradeTip->SetText(StringTable().translate(trade_tip).c_str()); UITradeTip->AdjustHeightToText(); - UITradeTip->SetWndPos(pos); + if (!soc_style || m_complex_desc) + UITradeTip->SetWndPos(pos); UITradeTip->Show(true); } } - if (UIDesc) { pos = UIDesc->GetWndPos(); @@ -266,19 +275,22 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, if (UITradeTip && trade_tip != NULL) pos.y = UITradeTip->GetWndPos().y + UITradeTip->GetHeight() + 4.0f; - UIDesc->SetWndPos(pos); + if (!soc_style || m_complex_desc) + UIDesc->SetWndPos(pos); UIDesc->Clear(); VERIFY(0 == UIDesc->GetSize()); + + CUIStatic* descr{}; if (m_desc_info.bShowDescrText) { - auto* pItem = xr_new("Description"); - pItem->SetTextColor(m_desc_info.uDescClr); - pItem->SetFont(m_desc_info.pDescFont); - pItem->SetWidth(UIDesc->GetDesiredChildWidth()); - pItem->SetTextComplexMode(true); - pItem->SetText(pInvItem->ItemDescription().c_str()); - pItem->AdjustHeightToText(); - UIDesc->AddWindow(pItem, true); + descr = xr_new("Description"); + descr->SetTextColor(m_desc_info.uDescClr); + descr->SetFont(m_desc_info.pDescFont); + descr->SetWidth(UIDesc->GetDesiredChildWidth()); + descr->SetTextComplexMode(true); + descr->SetText(pInvItem->ItemDescription().c_str()); + descr->AdjustHeightToText(); + UIDesc->AddWindow(descr, !soc_style); } TryAddConditionInfo(*pInvItem, pCompareItem); TryAddWpnInfo(*pInvItem, pCompareItem); @@ -287,6 +299,12 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, TryAddUpgradeInfo(*pInvItem); TryAddBoosterInfo(*pInvItem); + if (descr && soc_style) + { + UIDesc->RemoveWindow(descr); + UIDesc->AddWindow(descr, true); + } + if (m_b_FitToHeight) { UIDesc->SetWndSize(Fvector2().set(UIDesc->GetWndSize().x, UIDesc->GetPadSize().y)); @@ -316,11 +334,17 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, UIItemImage->GetUIStaticItem().SetTextureRect(texture_rect); UIItemImage->TextureOn(); UIItemImage->SetStretchTexture(true); - Fvector2 v_r = {item_grid_rect.x2 * INV_GRID_WIDTH2, item_grid_rect.y2 * INV_GRID_HEIGHT2}; + Fvector2 v_r = + { + item_grid_rect.x2 * (soc_style ? INV_GRID_WIDTH : INV_GRID_WIDTH2), + item_grid_rect.y2 * (soc_style ? INV_GRID_HEIGHT : INV_GRID_HEIGHT2) + }; v_r.x *= UI().get_current_kx(); UIItemImage->GetUIStaticItem().SetSize(v_r); + if (soc_style) + v_r.min(UIItemImageSize); UIItemImage->SetWidth(v_r.x); UIItemImage->SetHeight(v_r.y); } @@ -336,7 +360,8 @@ void CUIItemInfo::TryAddConditionInfo(CInventoryItem& pInvItem, CInventoryItem* if (weapon || outfit) { UIConditionWnd->SetInfo(pCompareItem, pInvItem); - UIDesc->AddWindow(UIConditionWnd, false); + if (!IsChild(UIConditionWnd)) + UIDesc->AddWindow(UIConditionWnd, false); } } diff --git a/src/xrGame/ui/UIWpnParams.cpp b/src/xrGame/ui/UIWpnParams.cpp index bc001ebc5a1..7726172130a 100644 --- a/src/xrGame/ui/UIWpnParams.cpp +++ b/src/xrGame/ui/UIWpnParams.cpp @@ -262,12 +262,21 @@ CUIConditionParams::CUIConditionParams() bool CUIConditionParams::InitFromXml(CUIXml& xml_doc) { - if (!xml_doc.NavigateToNode("condition_params", 0)) - return false; - CUIXmlInit::InitWindow(xml_doc, "condition_params", 0, this); - CUIXmlInit::InitStatic(xml_doc, "condition_params:caption", 0, &m_text); - m_progress.InitFromXml(xml_doc, "condition_params:progress_state"); - return true; + if (xml_doc.NavigateToNode("condition_params", 0)) + { + CUIXmlInit::InitWindow(xml_doc, "condition_params", 0, this); + CUIXmlInit::InitStatic(xml_doc, "condition_params:caption", 0, &m_text); + m_progress.InitFromXml(xml_doc, "condition_params:progress_state"); + return true; + } + // SOC + if (xml_doc.NavigateToNode("condition_progress", 0)) + { + CUIXmlInit::InitStatic(xml_doc, "static_condition", 0, &m_text, false); + m_progress.InitFromXml(xml_doc, "condition_progress"); + return true; + } + return false; } void CUIConditionParams::SetInfo(CInventoryItem const* slot_item, CInventoryItem const& cur_item) From 2e650c910f37799dacc73158df4fc66c45421d51 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 16:55:52 +0300 Subject: [PATCH 103/261] Work on SOC inventory (#392) --- src/xrGame/Inventory.cpp | 2 + src/xrGame/ui/UIActorMenu.cpp | 64 ++++++++++++++++++++- src/xrGame/ui/UIActorMenuDeadBodySearch.cpp | 4 ++ src/xrGame/ui/UIActorMenuInitialize.cpp | 6 ++ src/xrGame/ui/UIActorMenuInventory.cpp | 7 ++- src/xrGame/ui/UIActorMenuTrade.cpp | 3 +- src/xrGame/ui/UIItemInfo.cpp | 3 - 7 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/xrGame/Inventory.cpp b/src/xrGame/Inventory.cpp index 033a4119455..78c481bfb04 100644 --- a/src/xrGame/Inventory.cpp +++ b/src/xrGame/Inventory.cpp @@ -1280,6 +1280,8 @@ bool CInventory::CanTakeItem(CInventoryItem* inventory_item) const u32 CInventory::BeltWidth() const { + if (ShadowOfChernobylMode) + return m_iMaxBelt; CActor* pActor = smart_cast(m_pOwner); if (pActor) { diff --git a/src/xrGame/ui/UIActorMenu.cpp b/src/xrGame/ui/UIActorMenu.cpp index d497272d0d2..484f27b0686 100644 --- a/src/xrGame/ui/UIActorMenu.cpp +++ b/src/xrGame/ui/UIActorMenu.cpp @@ -409,8 +409,68 @@ void CUIActorMenu::SetCurrentItem(CUICellItem* itm) } TryHidePropertiesBox(); - if (GetModeSpecificItemInfo()) - GetModeSpecificItemInfo()->InitItem(itm); + if (auto* info = GetModeSpecificItemInfo()) + { + u32 item_price = u32(-1); + PIItem compare_item = nullptr; + pcstr trade_tip = nullptr; + if (PIItem current_item = itm ? (PIItem)itm->m_pData : nullptr) + { + if (GetMenuMode() != mmTrade) + item_price = current_item->Cost(); + else + { + u16 compare_slot = current_item->BaseSlot(); + if (compare_slot != NO_ACTIVE_SLOT) + { + compare_item = m_pActorInvOwner->inventory().ItemFromSlot(compare_slot); + } + + CInventoryOwner* item_owner = smart_cast(current_item->m_pInventory->GetOwner()); + + if (item_owner && item_owner == m_pActorInvOwner) + item_price = m_partner_trade->GetItemPrice(current_item, true); + else + item_price = m_partner_trade->GetItemPrice(current_item, false); + + // if(item_price>500) + // item_price = iFloor(item_price/10+0.5f)*10; + + CWeaponAmmo* ammo = smart_cast(current_item); + if (ammo) + { + for (u32 j = 0; j < itm->ChildsCount(); ++j) + { + u32 tmp_price = 0; + PIItem jitem = (PIItem)itm->Child(j)->m_pData; + CInventoryOwner* ammo_owner = smart_cast(jitem->m_pInventory->GetOwner()); + if (ammo_owner && ammo_owner == m_pActorInvOwner) + tmp_price = m_partner_trade->GetItemPrice(jitem, true); + else + tmp_price = m_partner_trade->GetItemPrice(jitem, false); + + // if(tmp_price>500) + // tmp_price = iFloor(tmp_price/10+0.5f)*10; + + item_price += tmp_price; + } + } + if (!current_item->CanTrade() || (!m_pPartnerInvOwner->trade_parameters().enabled( + CTradeParameters::action_buy(0), current_item->object().cNameSect()) && + item_owner && item_owner == m_pActorInvOwner)) + { + item_price = u32(-1); + trade_tip = "st_no_trade_tip_1"; + } + else if (current_item->GetCondition() < m_pPartnerInvOwner->trade_parameters().buy_item_condition_factor) + { + item_price = u32(-1); + trade_tip = "st_no_trade_tip_2"; + } + } + } + info->InitItem(itm, compare_item, item_price, trade_tip); + } if (m_currMenuMode == mmUpgrade) { diff --git a/src/xrGame/ui/UIActorMenuDeadBodySearch.cpp b/src/xrGame/ui/UIActorMenuDeadBodySearch.cpp index 9c7c7fb2768..dc03ef5dcd5 100644 --- a/src/xrGame/ui/UIActorMenuDeadBodySearch.cpp +++ b/src/xrGame/ui/UIActorMenuDeadBodySearch.cpp @@ -58,6 +58,10 @@ void CUIActorMenu::InitDeadBodySearchMode() m_takeall_button->Show(true); GetModeSpecificPartnerInfo(mmDeadBodySearch)->Show(nullptr != m_pPartnerInvOwner); + auto partnerIcon = m_SearchLootPartnerCharacterInfo->GetParent(); + if (partnerIcon != this) + partnerIcon->Show(false); + InitInventoryContents(m_pLists[eSearchLootActorBagList], m_pLists[eSearchLootActorBagList] != m_pLists[eInventoryBagList]); diff --git a/src/xrGame/ui/UIActorMenuInitialize.cpp b/src/xrGame/ui/UIActorMenuInitialize.cpp index 27226cb232a..fc6739c5422 100644 --- a/src/xrGame/ui/UIActorMenuInitialize.cpp +++ b/src/xrGame/ui/UIActorMenuInitialize.cpp @@ -327,6 +327,8 @@ void CUIActorMenu::InitializeInventoryMode(CUIXml& uiXml) m_pInventoryWnd->AttachChild(m_OutfitInfo); m_OutfitInfo->InitFromXml(uiXml); + CUIXmlInit::InitAutoStaticGroup(uiXml, "", 0, m_pInventoryWnd); + std::tuple inventory_lists[] = { // { id, "xml_section_name", parent } @@ -414,6 +416,8 @@ void CUIActorMenu::InitializeTradeMode(CUIXml& uiXml) descWnd->AttachChild(m_ItemInfoTradeMode); m_ItemInfoTradeMode->InitItemInfo({ 0.f, 0.f }, descWnd->GetWndSize(), TRADE_ITEM_XML); + CUIXmlInit::InitAutoStaticGroup(uiXml, "", 0, m_pTradeWnd); + m_trade_button = UIHelper::Create3tButton(uiXml, "button", 0, m_pTradeWnd); CUI3tButton* toTalkBtn = UIHelper::Create3tButton(uiXml, "button", 1, m_pTradeWnd); RegisterCallback(toTalkBtn, BUTTON_CLICKED, @@ -457,6 +461,8 @@ void CUIActorMenu::InitializeSearchLootMode(CUIXml& uiXml) descWnd->AttachChild(m_ItemInfoSearchLootMode); m_ItemInfoSearchLootMode->InitItemInfo({ 0.f, 0.f }, descWnd->GetWndSize(), CARBODY_ITEM_XML); + CUIXmlInit::InitAutoStaticGroup(uiXml, "", 0, m_pSearchLootWnd); + m_takeall_button = UIHelper::Create3tButton(uiXml, "take_all_btn", m_pSearchLootWnd); } diff --git a/src/xrGame/ui/UIActorMenuInventory.cpp b/src/xrGame/ui/UIActorMenuInventory.cpp index e059e90a343..ec86b98c3e6 100644 --- a/src/xrGame/ui/UIActorMenuInventory.cpp +++ b/src/xrGame/ui/UIActorMenuInventory.cpp @@ -424,7 +424,12 @@ void CUIActorMenu::InitInventoryContents(CUIDragDropListEx* pBagList, bool onlyB CUIDragDropListEx* curr_list = pBagList; - TIItemContainer ruck_list = m_pActorInvOwner->inventory().m_ruck; + TIItemContainer ruck_list; + if (onlyBagList) + m_pActorInvOwner->inventory().AddAvailableItems(ruck_list, true); + else + ruck_list = m_pActorInvOwner->inventory().m_ruck; + std::sort(ruck_list.begin(), ruck_list.end(), InventoryUtilities::GreaterRoomInRuck); for (PIItem item : ruck_list) diff --git a/src/xrGame/ui/UIActorMenuTrade.cpp b/src/xrGame/ui/UIActorMenuTrade.cpp index 04d4ffcdfac..2d9193413b2 100644 --- a/src/xrGame/ui/UIActorMenuTrade.cpp +++ b/src/xrGame/ui/UIActorMenuTrade.cpp @@ -49,7 +49,8 @@ void CUIActorMenu::InitTradeMode() VERIFY(m_pPartnerInvOwner); m_pPartnerInvOwner->StartTrading(); - InitInventoryContents(m_pLists[eTradeActorBagList]); + InitInventoryContents(m_pLists[eTradeActorBagList], + m_pLists[eSearchLootActorBagList] != m_pLists[eInventoryBagList]); InitPartnerInventoryContents(); m_actor_trade = m_pActorInvOwner->GetTrade(); diff --git a/src/xrGame/ui/UIItemInfo.cpp b/src/xrGame/ui/UIItemInfo.cpp index 773ea1b568c..53ec36681bf 100644 --- a/src/xrGame/ui/UIItemInfo.cpp +++ b/src/xrGame/ui/UIItemInfo.cpp @@ -229,9 +229,6 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, } if (UICost && IsGameTypeSingle()) { - if (soc_style && item_price == u32(-1)) - item_price = pInvItem->Cost(); - if (item_price == u32(-1)) UICost->Show(false); else From 48d538fb5e92e3245f1f1dd61eec8f946c169289 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 16:55:52 +0300 Subject: [PATCH 104/261] Work on SOC inventory (#392) --- src/xrGame/Inventory.cpp | 2 + src/xrGame/ui/UIActorMenu.cpp | 64 ++++++++++++++++++++- src/xrGame/ui/UIActorMenuDeadBodySearch.cpp | 4 ++ src/xrGame/ui/UIActorMenuInitialize.cpp | 6 ++ src/xrGame/ui/UIActorMenuInventory.cpp | 7 ++- src/xrGame/ui/UIActorMenuTrade.cpp | 3 +- src/xrGame/ui/UIItemInfo.cpp | 3 - 7 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/xrGame/Inventory.cpp b/src/xrGame/Inventory.cpp index 033a4119455..78c481bfb04 100644 --- a/src/xrGame/Inventory.cpp +++ b/src/xrGame/Inventory.cpp @@ -1280,6 +1280,8 @@ bool CInventory::CanTakeItem(CInventoryItem* inventory_item) const u32 CInventory::BeltWidth() const { + if (ShadowOfChernobylMode) + return m_iMaxBelt; CActor* pActor = smart_cast(m_pOwner); if (pActor) { diff --git a/src/xrGame/ui/UIActorMenu.cpp b/src/xrGame/ui/UIActorMenu.cpp index d497272d0d2..484f27b0686 100644 --- a/src/xrGame/ui/UIActorMenu.cpp +++ b/src/xrGame/ui/UIActorMenu.cpp @@ -409,8 +409,68 @@ void CUIActorMenu::SetCurrentItem(CUICellItem* itm) } TryHidePropertiesBox(); - if (GetModeSpecificItemInfo()) - GetModeSpecificItemInfo()->InitItem(itm); + if (auto* info = GetModeSpecificItemInfo()) + { + u32 item_price = u32(-1); + PIItem compare_item = nullptr; + pcstr trade_tip = nullptr; + if (PIItem current_item = itm ? (PIItem)itm->m_pData : nullptr) + { + if (GetMenuMode() != mmTrade) + item_price = current_item->Cost(); + else + { + u16 compare_slot = current_item->BaseSlot(); + if (compare_slot != NO_ACTIVE_SLOT) + { + compare_item = m_pActorInvOwner->inventory().ItemFromSlot(compare_slot); + } + + CInventoryOwner* item_owner = smart_cast(current_item->m_pInventory->GetOwner()); + + if (item_owner && item_owner == m_pActorInvOwner) + item_price = m_partner_trade->GetItemPrice(current_item, true); + else + item_price = m_partner_trade->GetItemPrice(current_item, false); + + // if(item_price>500) + // item_price = iFloor(item_price/10+0.5f)*10; + + CWeaponAmmo* ammo = smart_cast(current_item); + if (ammo) + { + for (u32 j = 0; j < itm->ChildsCount(); ++j) + { + u32 tmp_price = 0; + PIItem jitem = (PIItem)itm->Child(j)->m_pData; + CInventoryOwner* ammo_owner = smart_cast(jitem->m_pInventory->GetOwner()); + if (ammo_owner && ammo_owner == m_pActorInvOwner) + tmp_price = m_partner_trade->GetItemPrice(jitem, true); + else + tmp_price = m_partner_trade->GetItemPrice(jitem, false); + + // if(tmp_price>500) + // tmp_price = iFloor(tmp_price/10+0.5f)*10; + + item_price += tmp_price; + } + } + if (!current_item->CanTrade() || (!m_pPartnerInvOwner->trade_parameters().enabled( + CTradeParameters::action_buy(0), current_item->object().cNameSect()) && + item_owner && item_owner == m_pActorInvOwner)) + { + item_price = u32(-1); + trade_tip = "st_no_trade_tip_1"; + } + else if (current_item->GetCondition() < m_pPartnerInvOwner->trade_parameters().buy_item_condition_factor) + { + item_price = u32(-1); + trade_tip = "st_no_trade_tip_2"; + } + } + } + info->InitItem(itm, compare_item, item_price, trade_tip); + } if (m_currMenuMode == mmUpgrade) { diff --git a/src/xrGame/ui/UIActorMenuDeadBodySearch.cpp b/src/xrGame/ui/UIActorMenuDeadBodySearch.cpp index 9c7c7fb2768..dc03ef5dcd5 100644 --- a/src/xrGame/ui/UIActorMenuDeadBodySearch.cpp +++ b/src/xrGame/ui/UIActorMenuDeadBodySearch.cpp @@ -58,6 +58,10 @@ void CUIActorMenu::InitDeadBodySearchMode() m_takeall_button->Show(true); GetModeSpecificPartnerInfo(mmDeadBodySearch)->Show(nullptr != m_pPartnerInvOwner); + auto partnerIcon = m_SearchLootPartnerCharacterInfo->GetParent(); + if (partnerIcon != this) + partnerIcon->Show(false); + InitInventoryContents(m_pLists[eSearchLootActorBagList], m_pLists[eSearchLootActorBagList] != m_pLists[eInventoryBagList]); diff --git a/src/xrGame/ui/UIActorMenuInitialize.cpp b/src/xrGame/ui/UIActorMenuInitialize.cpp index 27226cb232a..fc6739c5422 100644 --- a/src/xrGame/ui/UIActorMenuInitialize.cpp +++ b/src/xrGame/ui/UIActorMenuInitialize.cpp @@ -327,6 +327,8 @@ void CUIActorMenu::InitializeInventoryMode(CUIXml& uiXml) m_pInventoryWnd->AttachChild(m_OutfitInfo); m_OutfitInfo->InitFromXml(uiXml); + CUIXmlInit::InitAutoStaticGroup(uiXml, "", 0, m_pInventoryWnd); + std::tuple inventory_lists[] = { // { id, "xml_section_name", parent } @@ -414,6 +416,8 @@ void CUIActorMenu::InitializeTradeMode(CUIXml& uiXml) descWnd->AttachChild(m_ItemInfoTradeMode); m_ItemInfoTradeMode->InitItemInfo({ 0.f, 0.f }, descWnd->GetWndSize(), TRADE_ITEM_XML); + CUIXmlInit::InitAutoStaticGroup(uiXml, "", 0, m_pTradeWnd); + m_trade_button = UIHelper::Create3tButton(uiXml, "button", 0, m_pTradeWnd); CUI3tButton* toTalkBtn = UIHelper::Create3tButton(uiXml, "button", 1, m_pTradeWnd); RegisterCallback(toTalkBtn, BUTTON_CLICKED, @@ -457,6 +461,8 @@ void CUIActorMenu::InitializeSearchLootMode(CUIXml& uiXml) descWnd->AttachChild(m_ItemInfoSearchLootMode); m_ItemInfoSearchLootMode->InitItemInfo({ 0.f, 0.f }, descWnd->GetWndSize(), CARBODY_ITEM_XML); + CUIXmlInit::InitAutoStaticGroup(uiXml, "", 0, m_pSearchLootWnd); + m_takeall_button = UIHelper::Create3tButton(uiXml, "take_all_btn", m_pSearchLootWnd); } diff --git a/src/xrGame/ui/UIActorMenuInventory.cpp b/src/xrGame/ui/UIActorMenuInventory.cpp index e059e90a343..ec86b98c3e6 100644 --- a/src/xrGame/ui/UIActorMenuInventory.cpp +++ b/src/xrGame/ui/UIActorMenuInventory.cpp @@ -424,7 +424,12 @@ void CUIActorMenu::InitInventoryContents(CUIDragDropListEx* pBagList, bool onlyB CUIDragDropListEx* curr_list = pBagList; - TIItemContainer ruck_list = m_pActorInvOwner->inventory().m_ruck; + TIItemContainer ruck_list; + if (onlyBagList) + m_pActorInvOwner->inventory().AddAvailableItems(ruck_list, true); + else + ruck_list = m_pActorInvOwner->inventory().m_ruck; + std::sort(ruck_list.begin(), ruck_list.end(), InventoryUtilities::GreaterRoomInRuck); for (PIItem item : ruck_list) diff --git a/src/xrGame/ui/UIActorMenuTrade.cpp b/src/xrGame/ui/UIActorMenuTrade.cpp index 04d4ffcdfac..2d9193413b2 100644 --- a/src/xrGame/ui/UIActorMenuTrade.cpp +++ b/src/xrGame/ui/UIActorMenuTrade.cpp @@ -49,7 +49,8 @@ void CUIActorMenu::InitTradeMode() VERIFY(m_pPartnerInvOwner); m_pPartnerInvOwner->StartTrading(); - InitInventoryContents(m_pLists[eTradeActorBagList]); + InitInventoryContents(m_pLists[eTradeActorBagList], + m_pLists[eSearchLootActorBagList] != m_pLists[eInventoryBagList]); InitPartnerInventoryContents(); m_actor_trade = m_pActorInvOwner->GetTrade(); diff --git a/src/xrGame/ui/UIItemInfo.cpp b/src/xrGame/ui/UIItemInfo.cpp index 773ea1b568c..53ec36681bf 100644 --- a/src/xrGame/ui/UIItemInfo.cpp +++ b/src/xrGame/ui/UIItemInfo.cpp @@ -229,9 +229,6 @@ void CUIItemInfo::InitItem(CUICellItem* pCellItem, CInventoryItem* pCompareItem, } if (UICost && IsGameTypeSingle()) { - if (soc_style && item_price == u32(-1)) - item_price = pInvItem->Cost(); - if (item_price == u32(-1)) UICost->Show(false); else From 933748010190723e73a5526d2e959f2074c79d30 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 18:35:12 +0300 Subject: [PATCH 105/261] Work on SOC support: proper immunities and damage calculations (#392) CUIOutfitInfo and CUIArtefactParams work 100% identically now. --- src/xrGame/Actor.cpp | 4 +- src/xrGame/Actor.h | 2 +- src/xrGame/ActorHelmet.cpp | 2 +- src/xrGame/CustomOutfit.cpp | 16 ++---- src/xrGame/hit_immunity.cpp | 70 ++++++++++++++++---------- src/xrGame/script_game_object3.cpp | 6 +-- src/xrGame/ui/UIActorMenuInventory.cpp | 2 +- src/xrGame/ui/UIOutfitInfo.cpp | 24 +++++---- src/xrGame/ui/UIOutfitInfo.h | 5 +- src/xrGame/ui/ui_af_params.cpp | 46 ++++++++--------- src/xrGame/ui/ui_af_params.h | 2 +- 11 files changed, 93 insertions(+), 86 deletions(-) diff --git a/src/xrGame/Actor.cpp b/src/xrGame/Actor.cpp index 899af0eddfd..cade69e8128 100644 --- a/src/xrGame/Actor.cpp +++ b/src/xrGame/Actor.cpp @@ -1939,10 +1939,10 @@ float CActor::HitArtefactsOnBelt(float hit_power, ALife::EHitType hit_type) return hit_power; } -float CActor::GetProtection_ArtefactsOnBelt(ALife::EHitType hit_type) +float CActor::GetProtection_ArtefactsOnBelt(ALife::EHitType hit_type) const { float sum = 0.0f; - for (auto& it : inventory().m_belt) + for (const auto& it : inventory().m_belt) { const auto artefact = smart_cast(it); if (artefact) diff --git a/src/xrGame/Actor.h b/src/xrGame/Actor.h index ddd8bc6dfdd..20df7cdd470 100644 --- a/src/xrGame/Actor.h +++ b/src/xrGame/Actor.h @@ -203,7 +203,7 @@ class CActor : public CEntityAlive, //свойства артефактов virtual void UpdateArtefactsOnBeltAndOutfit(); float HitArtefactsOnBelt(float hit_power, ALife::EHitType hit_type); - float GetProtection_ArtefactsOnBelt(ALife::EHitType hit_type); + float GetProtection_ArtefactsOnBelt(ALife::EHitType hit_type) const; protected: //звук тяжелого дыхания diff --git a/src/xrGame/ActorHelmet.cpp b/src/xrGame/ActorHelmet.cpp index 79dfbfe5320..8b045846ec0 100644 --- a/src/xrGame/ActorHelmet.cpp +++ b/src/xrGame/ActorHelmet.cpp @@ -361,7 +361,7 @@ float CHelmet::HitThroughArmor(float hit_power, s16 element, float ap, bool& add } else { - NewHitPower *= GetHitTypeProtection(hit_type, element); + NewHitPower -= GetHitTypeProtection(hit_type, element); } //увеличить изношенность шлема diff --git a/src/xrGame/CustomOutfit.cpp b/src/xrGame/CustomOutfit.cpp index 1b4757a44a0..5b16e930234 100644 --- a/src/xrGame/CustomOutfit.cpp +++ b/src/xrGame/CustomOutfit.cpp @@ -136,23 +136,14 @@ void CCustomOutfit::Hit(float hit_power, ALife::EHitType hit_type) float CCustomOutfit::GetDefHitTypeProtection(ALife::EHitType hit_type) const { - const float base = m_HitTypeProtection[hit_type] * GetCondition(); - - if (m_boneProtection.m_hitFracType == SBoneProtections::HitFraction) - return 1.0f - base; // SOC - - return base; // CS/COP + return m_HitTypeProtection[hit_type] * GetCondition(); } float CCustomOutfit::GetHitTypeProtection(ALife::EHitType hit_type, s16 element) const { const float base = m_HitTypeProtection[hit_type] * GetCondition(); const float bone = m_boneProtection.getBoneProtection(element); - - if (m_boneProtection.m_hitFracType == SBoneProtections::HitFraction) - return 1.0f - base * bone; // SOC - - return base * bone; // CS/COP + return base * bone; } float CCustomOutfit::GetBoneArmor(s16 element) const @@ -280,7 +271,7 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo } else { - NewHitPower *= GetHitTypeProtection(hit_type, element); + NewHitPower -= GetHitTypeProtection(hit_type, element); } //увеличить изношенность костюма @@ -425,6 +416,7 @@ bool CCustomOutfit::install_upgrade_impl(LPCSTR section, bool test) section, "fire_wound_protection", &CInifile::r_float, m_HitTypeProtection[ALife::eHitTypeFireWound], test); result |= process_if_exists( section, "physic_strike_protection", &CInifile::r_float, m_HitTypeProtection[ALife::eHitTypePhysicStrike], test); + LPCSTR str{}; bool result2 = process_if_exists_set(section, "nightvision_sect", &CInifile::r_string, str, test); if (result2 && !test) diff --git a/src/xrGame/hit_immunity.cpp b/src/xrGame/hit_immunity.cpp index efaf0e5cb89..83e67a37001 100644 --- a/src/xrGame/hit_immunity.cpp +++ b/src/xrGame/hit_immunity.cpp @@ -9,42 +9,60 @@ CHitImmunity::CHitImmunity() { m_HitImmunityKoefs.resize(ALife::eHitTypeMax); for (u32 i = 0; i < ALife::eHitTypeMax; i++) - m_HitImmunityKoefs[i] = 1.0f; + m_HitImmunityKoefs[i] = 0.0f; } +constexpr std::tuple hit_immunities[] = +{ + // { hit type, "immunity", optional } + { ALife::eHitTypeBurn, "burn_immunity", false }, + { ALife::eHitTypeStrike, "strike_immunity", false }, + { ALife::eHitTypeShock, "shock_immunity", false }, + { ALife::eHitTypeWound, "wound_immunity", false }, + { ALife::eHitTypeRadiation, "radiation_immunity", false }, + { ALife::eHitTypeTelepatic, "telepatic_immunity", false }, + { ALife::eHitTypeChemicalBurn, "chemical_burn_immunity", false }, + { ALife::eHitTypeExplosion, "explosion_immunity", false }, + { ALife::eHitTypeFireWound, "fire_wound_immunity", false }, + { ALife::eHitTypeLightBurn, "burn_immunity", false }, + { ALife::eHitTypePhysicStrike, "physic_strike_wound_immunity", true }, +}; + void CHitImmunity::LoadImmunities(const char* imm_sect, const CInifile* ini) { R_ASSERT2(ini->section_exist(imm_sect), imm_sect); - m_HitImmunityKoefs[ALife::eHitTypeBurn] = ini->r_float(imm_sect, "burn_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeStrike] = ini->r_float(imm_sect, "strike_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeShock] = ini->r_float(imm_sect, "shock_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeWound] = ini->r_float(imm_sect, "wound_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeRadiation] = ini->r_float(imm_sect, "radiation_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeTelepatic] = ini->r_float(imm_sect, "telepatic_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeChemicalBurn] = ini->r_float(imm_sect, "chemical_burn_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeExplosion] = ini->r_float(imm_sect, "explosion_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeFireWound] = ini->r_float(imm_sect, "fire_wound_immunity"); - m_HitImmunityKoefs[ALife::eHitTypePhysicStrike] = ini->read_if_exists( - imm_sect, "physic_strike_wound_immunity", 1.0f); - m_HitImmunityKoefs[ALife::eHitTypeLightBurn] = m_HitImmunityKoefs[ALife::eHitTypeBurn]; + // SOC vs CS/COP are inverted, convert to CS/COP format. + const bool is_soc = GMLib.GetLibraryVersion() <= GAMEMTL_VERSION_SOC; + + for (const auto& [hit_type, immunity_name, optional] : hit_immunities) + { + if (optional && !ini->line_exist(imm_sect, immunity_name)) + continue; + + float immunity = ini->r_float(imm_sect, immunity_name); + if (is_soc) + immunity = 1.0f - immunity; + + m_HitImmunityKoefs[hit_type] = immunity; + } } void CHitImmunity::AddImmunities(const char* imm_sect, const CInifile* ini) { R_ASSERT2(ini->section_exist(imm_sect), imm_sect); - m_HitImmunityKoefs[ALife::eHitTypeBurn] += READ_IF_EXISTS(ini, r_float, imm_sect, "burn_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeStrike] += READ_IF_EXISTS(ini, r_float, imm_sect, "strike_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeShock] += READ_IF_EXISTS(ini, r_float, imm_sect, "shock_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeWound] += READ_IF_EXISTS(ini, r_float, imm_sect, "wound_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeRadiation] += READ_IF_EXISTS(ini, r_float, imm_sect, "radiation_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeTelepatic] += READ_IF_EXISTS(ini, r_float, imm_sect, "telepatic_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeChemicalBurn] += - READ_IF_EXISTS(ini, r_float, imm_sect, "chemical_burn_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeExplosion] += READ_IF_EXISTS(ini, r_float, imm_sect, "explosion_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeFireWound] += READ_IF_EXISTS(ini, r_float, imm_sect, "fire_wound_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypePhysicStrike] += ini->read_if_exists( - imm_sect, "physic_strike_wound_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeLightBurn] = m_HitImmunityKoefs[ALife::eHitTypeBurn]; + // Consistency is the key. + // We want this to have SOC format in SOC, the same as LoadImmunities does. + const bool is_soc = GMLib.GetLibraryVersion() <= GAMEMTL_VERSION_SOC; + + for (const auto& [hit_type, immunity_name, _] : hit_immunities) + { + if (!ini->line_exist(imm_sect, immunity_name)) + continue; + float immunity = ini->r_float(imm_sect, immunity_name); + if (is_soc) + immunity = 1.0f - immunity; + m_HitImmunityKoefs[hit_type] += immunity; + } } diff --git a/src/xrGame/script_game_object3.cpp b/src/xrGame/script_game_object3.cpp index 2f9d100555f..4a1f6c7cdfd 100644 --- a/src/xrGame/script_game_object3.cpp +++ b/src/xrGame/script_game_object3.cpp @@ -303,11 +303,7 @@ float CScriptGameObject::GetCurrentOutfitProtection(int hit_type) if (!o) return 0.0f; - const float protection = o->GetDefHitTypeProtection(ALife::EHitType(hit_type)); - - if (o->GetHitFracType() == SBoneProtections::HitFraction) - return 1.0f - protection; // SOC - return protection; + return o->GetDefHitTypeProtection(ALife::EHitType(hit_type)); } CScriptGameObject* CScriptGameObject::GetFood() const diff --git a/src/xrGame/ui/UIActorMenuInventory.cpp b/src/xrGame/ui/UIActorMenuInventory.cpp index ec86b98c3e6..31968d4b3ec 100644 --- a/src/xrGame/ui/UIActorMenuInventory.cpp +++ b/src/xrGame/ui/UIActorMenuInventory.cpp @@ -1555,7 +1555,7 @@ void CUIActorMenu::UpdateOutfit() if (m_OutfitInfo) { - m_OutfitInfo->UpdateInfo(outfit, nullptr, true); + m_OutfitInfo->UpdateInfo(outfit, nullptr, true, true); } if (ShadowOfChernobylMode) diff --git a/src/xrGame/ui/UIOutfitInfo.cpp b/src/xrGame/ui/UIOutfitInfo.cpp index 2736a327e50..42fe7863cb3 100644 --- a/src/xrGame/ui/UIOutfitInfo.cpp +++ b/src/xrGame/ui/UIOutfitInfo.cpp @@ -87,7 +87,7 @@ bool CUIOutfitImmunity::InitFromXml(CUIXml& xml_doc, pcstr base_str, pcstr immun return true; } -void CUIOutfitImmunity::SetProgressValue(float cur, float comp) +void CUIOutfitImmunity::SetProgressValue(float cur, float comp, float add /*= 0.0f*/) { string128 buf; @@ -96,12 +96,19 @@ void CUIOutfitImmunity::SetProgressValue(float cur, float comp) if (m_progress.IsShown()) { + if (fsimilar(cur, comp) && !fis_zero(add)) + comp = add; m_progress.SetTwoPos(cur, comp); - xr_sprintf(buf, "%.0f", cur); + + const auto sz = xr_sprintf(buf, "%.0f", cur); + if (!fis_zero(add)) + xr_sprintf(buf + sz, sizeof(buf) - sz, "+ %.0f%", add * m_magnitude); } else // SOC { - xr_sprintf(buf, "%s %+3.0f%%", cur > 0.0f ? "%c[green]" : "%c[red]", cur); + const auto sz = xr_sprintf(buf, "%s %+3.0f%%", cur > 0.0f ? "%c[green]" : "%c[red]", cur); + if (!fis_zero(add)) + xr_sprintf(buf + sz, sizeof(buf) - sz, "%s %+3.0f%%", add > 0.0f ? "%c[green]" : "%c[red]", add * m_magnitude); } m_value.SetText(buf); @@ -172,7 +179,8 @@ void CUIOutfitInfo::AdjustElements() SetWndSize(pos); } -void CUIOutfitInfo::UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit /*= nullptr*/, bool hide_if_zero_immunity /*= false*/) +void CUIOutfitInfo::UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit /*= nullptr*/, + bool add_artefact_info /*= false*/, bool hide_if_zero_immunity /*= false*/) { const CActor* actor = smart_cast(Level().CurrentViewEntity()); if (!actor || !cur_outfit) @@ -200,6 +208,7 @@ void CUIOutfitInfo::UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_ou float cur = cur_outfit->GetDefHitTypeProtection(hit_type); float slot = slot_outfit ? slot_outfit->GetDefHitTypeProtection(hit_type) : cur; + float add = add_artefact_info ? actor->GetProtection_ArtefactsOnBelt(hit_type) : 0.0f; if (is_cs_cop) { @@ -207,17 +216,12 @@ void CUIOutfitInfo::UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_ou cur /= max_power; // = 0..1 slot /= max_power; // = 0..1 } - else // SOC - { - cur = 1.0f - cur; - slot = 1.0f - slot; - } const bool hide = hide_if_zero_immunity && fis_zero(cur) && fis_zero(slot); item->Show(!hide); if (!hide) - item->SetProgressValue(cur, slot); + item->SetProgressValue(cur, slot, add); } if (const auto& fireWoundItem = m_items[ALife::eHitTypeFireWound]; diff --git a/src/xrGame/ui/UIOutfitInfo.h b/src/xrGame/ui/UIOutfitInfo.h index 29b79a07de6..37887dc2df0 100644 --- a/src/xrGame/ui/UIOutfitInfo.h +++ b/src/xrGame/ui/UIOutfitInfo.h @@ -15,7 +15,7 @@ class CUIOutfitImmunity final : public CUIStatic CUIOutfitImmunity(pcstr immunity_name); bool InitFromXml(CUIXml& xml_doc, pcstr base_str, pcstr immunity, pcstr immunity_text); - void SetProgressValue(float cur, float comp); + void SetProgressValue(float cur, float comp, float add = 0.0f); pcstr GetDebugType() override { return "CUIOutfitImmunity"; } @@ -33,7 +33,8 @@ class CUIOutfitInfo final : public CUIWindow CUIOutfitInfo() : CUIWindow("CUIOutfitInfo") {} void InitFromXml(CUIXml& xml_doc); - void UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit = nullptr, bool hide_if_zero_immunity = false); + void UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit = nullptr, + bool add_artefact_info = false, bool hide_if_zero_immunity = false); void UpdateInfo(CHelmet* cur_helmet, CHelmet* slot_helmet = nullptr); void AdjustElements(); diff --git a/src/xrGame/ui/ui_af_params.cpp b/src/xrGame/ui/ui_af_params.cpp index 27b9bb1ea8f..5946146376d 100644 --- a/src/xrGame/ui/ui_af_params.cpp +++ b/src/xrGame/ui/ui_af_params.cpp @@ -26,18 +26,18 @@ CUIArtefactParams::~CUIArtefactParams() xr_delete(m_Prop_line); } -constexpr std::tuple af_immunity[] = +constexpr std::tuple af_immunity[] = { - //{ ALife::infl_, "section", "caption", magnitude, sign_inverse, "unit" } - { ALife::infl_rad, "radiation_immunity", "ui_inv_outfit_radiation_protection", 100.0f, false, "%" }, - { ALife::infl_fire, "burn_immunity", "ui_inv_outfit_burn_protection", 100.0f, false, "%" }, - { ALife::infl_acid, "chemical_burn_immunity", "ui_inv_outfit_chemical_burn_protection", 100.0f, false, "%" }, - { ALife::infl_psi, "telepatic_immunity", "ui_inv_outfit_telepatic_protection", 100.0f, false, "%" }, - { ALife::infl_electra, "shock_immunity", "ui_inv_outfit_shock_protection", 100.0f, false, "%" }, - { 5, "strike_immunity", "ui_inv_outfit_strike_protection", 100.0f, false, "%" }, - { 6, "wound_immunity", "ui_inv_outfit_wound_protection", 100.0f, false, "%" }, - { 7, "explosion_immunity", "ui_inv_outfit_explosion_protection", 100.0f, false, "%" }, - { 8, "fire_wound_immunity", "ui_inv_outfit_fire_wound_protection", 100.0f, false, "%" }, + //{ ALife::EHitType, "section", "caption", magnitude, sign_inverse, "unit" } + { ALife::eHitTypeRadiation, "radiation_immunity", "ui_inv_outfit_radiation_protection", 100.0f, false, "%" }, + { ALife::eHitTypeBurn, "burn_immunity", "ui_inv_outfit_burn_protection", 100.0f, false, "%" }, + { ALife::eHitTypeChemicalBurn, "chemical_burn_immunity", "ui_inv_outfit_chemical_burn_protection", 100.0f, false, "%" }, + { ALife::eHitTypeTelepatic, "telepatic_immunity", "ui_inv_outfit_telepatic_protection", 100.0f, false, "%" }, + { ALife::eHitTypeShock, "shock_immunity", "ui_inv_outfit_shock_protection", 100.0f, false, "%" }, + { ALife::eHitTypeStrike, "strike_immunity", "ui_inv_outfit_strike_protection", 100.0f, false, "%" }, + { ALife::eHitTypeWound, "wound_immunity", "ui_inv_outfit_wound_protection", 100.0f, false, "%" }, + { ALife::eHitTypeExplosion, "explosion_immunity", "ui_inv_outfit_explosion_protection", 100.0f, false, "%" }, + { ALife::eHitTypeFireWound, "fire_wound_immunity", "ui_inv_outfit_fire_wound_protection", 100.0f, false, "%" }, }; constexpr std::tuple af_restore[] = @@ -126,6 +126,7 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) const auto& af_section = pInvItem.object().cNameSect().c_str(); const auto& actor_sect = actor->cNameSect().c_str(); const auto& condition_sect = pSettings->read_if_exists(actor_sect, "condition_sect", actor_sect); + const auto& hit_absorbation_sect = pSettings->r_string(af_section, "hit_absorbation_sect"); float h = 0.0f; if (m_Prop_line) @@ -168,29 +169,24 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) setValue(m_restore_item[id], val); } + CHitImmunity immunities; + immunities.LoadImmunities(hit_absorbation_sect, pSettings); + for (auto [id, immunity_section, immunity_caption, magnitude, sign_inverse, unit] : af_immunity) { if (!m_immunity_item[id]) continue; - shared_str const& hit_absorbation_sect = pSettings->r_string(af_section, "hit_absorbation_sect"); - float val = pSettings->r_float(hit_absorbation_sect, immunity_section); + float val = immunities.GetHitImmunity(id); + if (fis_zero(val)) + continue; + val *= pInvItem.GetCondition(); if (is_cs_cop) { - if (fis_zero(val)) - continue; - const float max_val = actor->conditions().GetZoneMaxPower(static_cast(id)); - val = val * pInvItem.GetCondition() / max_val; - } - else // SOC - { - if (fsimilar(val, 1.0f)) - continue; - val = 1.0f - val * pInvItem.GetCondition(); - + const float max_val = actor->conditions().GetZoneMaxPower(id); + val /= max_val; } - setValue(m_immunity_item[id], val); } diff --git a/src/xrGame/ui/ui_af_params.h b/src/xrGame/ui/ui_af_params.h index f205a8f544a..6ebf85a6f71 100644 --- a/src/xrGame/ui/ui_af_params.h +++ b/src/xrGame/ui/ui_af_params.h @@ -23,7 +23,7 @@ class CUIArtefactParams final : public CUIWindow protected: UIArtefactParamItem* m_disp_condition{}; //Alundaio: Show AF Condition - UIArtefactParamItem* m_immunity_item[9]{}; + UIArtefactParamItem* m_immunity_item[ALife::eHitTypeMax - 3]{}; UIArtefactParamItem* m_restore_item[ALife::eRestoreTypeMax]{}; UIArtefactParamItem* m_additional_weight{}; From 0b335b9f1f386aa84eb19194e536bf7b430ca4ab Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 18:35:12 +0300 Subject: [PATCH 106/261] Work on SOC support: proper immunities and damage calculations (#392) CUIOutfitInfo and CUIArtefactParams work 100% identically now. --- src/xrGame/Actor.cpp | 4 +- src/xrGame/Actor.h | 2 +- src/xrGame/ActorHelmet.cpp | 2 +- src/xrGame/CustomOutfit.cpp | 16 ++---- src/xrGame/hit_immunity.cpp | 70 ++++++++++++++++---------- src/xrGame/script_game_object3.cpp | 6 +-- src/xrGame/ui/UIActorMenuInventory.cpp | 2 +- src/xrGame/ui/UIOutfitInfo.cpp | 24 +++++---- src/xrGame/ui/UIOutfitInfo.h | 5 +- src/xrGame/ui/ui_af_params.cpp | 46 ++++++++--------- src/xrGame/ui/ui_af_params.h | 2 +- 11 files changed, 93 insertions(+), 86 deletions(-) diff --git a/src/xrGame/Actor.cpp b/src/xrGame/Actor.cpp index 899af0eddfd..cade69e8128 100644 --- a/src/xrGame/Actor.cpp +++ b/src/xrGame/Actor.cpp @@ -1939,10 +1939,10 @@ float CActor::HitArtefactsOnBelt(float hit_power, ALife::EHitType hit_type) return hit_power; } -float CActor::GetProtection_ArtefactsOnBelt(ALife::EHitType hit_type) +float CActor::GetProtection_ArtefactsOnBelt(ALife::EHitType hit_type) const { float sum = 0.0f; - for (auto& it : inventory().m_belt) + for (const auto& it : inventory().m_belt) { const auto artefact = smart_cast(it); if (artefact) diff --git a/src/xrGame/Actor.h b/src/xrGame/Actor.h index ddd8bc6dfdd..20df7cdd470 100644 --- a/src/xrGame/Actor.h +++ b/src/xrGame/Actor.h @@ -203,7 +203,7 @@ class CActor : public CEntityAlive, //свойства артефактов virtual void UpdateArtefactsOnBeltAndOutfit(); float HitArtefactsOnBelt(float hit_power, ALife::EHitType hit_type); - float GetProtection_ArtefactsOnBelt(ALife::EHitType hit_type); + float GetProtection_ArtefactsOnBelt(ALife::EHitType hit_type) const; protected: //звук тяжелого дыхания diff --git a/src/xrGame/ActorHelmet.cpp b/src/xrGame/ActorHelmet.cpp index 79dfbfe5320..8b045846ec0 100644 --- a/src/xrGame/ActorHelmet.cpp +++ b/src/xrGame/ActorHelmet.cpp @@ -361,7 +361,7 @@ float CHelmet::HitThroughArmor(float hit_power, s16 element, float ap, bool& add } else { - NewHitPower *= GetHitTypeProtection(hit_type, element); + NewHitPower -= GetHitTypeProtection(hit_type, element); } //увеличить изношенность шлема diff --git a/src/xrGame/CustomOutfit.cpp b/src/xrGame/CustomOutfit.cpp index 1b4757a44a0..5b16e930234 100644 --- a/src/xrGame/CustomOutfit.cpp +++ b/src/xrGame/CustomOutfit.cpp @@ -136,23 +136,14 @@ void CCustomOutfit::Hit(float hit_power, ALife::EHitType hit_type) float CCustomOutfit::GetDefHitTypeProtection(ALife::EHitType hit_type) const { - const float base = m_HitTypeProtection[hit_type] * GetCondition(); - - if (m_boneProtection.m_hitFracType == SBoneProtections::HitFraction) - return 1.0f - base; // SOC - - return base; // CS/COP + return m_HitTypeProtection[hit_type] * GetCondition(); } float CCustomOutfit::GetHitTypeProtection(ALife::EHitType hit_type, s16 element) const { const float base = m_HitTypeProtection[hit_type] * GetCondition(); const float bone = m_boneProtection.getBoneProtection(element); - - if (m_boneProtection.m_hitFracType == SBoneProtections::HitFraction) - return 1.0f - base * bone; // SOC - - return base * bone; // CS/COP + return base * bone; } float CCustomOutfit::GetBoneArmor(s16 element) const @@ -280,7 +271,7 @@ float CCustomOutfit::HitThroughArmor(float hit_power, s16 element, float ap, boo } else { - NewHitPower *= GetHitTypeProtection(hit_type, element); + NewHitPower -= GetHitTypeProtection(hit_type, element); } //увеличить изношенность костюма @@ -425,6 +416,7 @@ bool CCustomOutfit::install_upgrade_impl(LPCSTR section, bool test) section, "fire_wound_protection", &CInifile::r_float, m_HitTypeProtection[ALife::eHitTypeFireWound], test); result |= process_if_exists( section, "physic_strike_protection", &CInifile::r_float, m_HitTypeProtection[ALife::eHitTypePhysicStrike], test); + LPCSTR str{}; bool result2 = process_if_exists_set(section, "nightvision_sect", &CInifile::r_string, str, test); if (result2 && !test) diff --git a/src/xrGame/hit_immunity.cpp b/src/xrGame/hit_immunity.cpp index efaf0e5cb89..83e67a37001 100644 --- a/src/xrGame/hit_immunity.cpp +++ b/src/xrGame/hit_immunity.cpp @@ -9,42 +9,60 @@ CHitImmunity::CHitImmunity() { m_HitImmunityKoefs.resize(ALife::eHitTypeMax); for (u32 i = 0; i < ALife::eHitTypeMax; i++) - m_HitImmunityKoefs[i] = 1.0f; + m_HitImmunityKoefs[i] = 0.0f; } +constexpr std::tuple hit_immunities[] = +{ + // { hit type, "immunity", optional } + { ALife::eHitTypeBurn, "burn_immunity", false }, + { ALife::eHitTypeStrike, "strike_immunity", false }, + { ALife::eHitTypeShock, "shock_immunity", false }, + { ALife::eHitTypeWound, "wound_immunity", false }, + { ALife::eHitTypeRadiation, "radiation_immunity", false }, + { ALife::eHitTypeTelepatic, "telepatic_immunity", false }, + { ALife::eHitTypeChemicalBurn, "chemical_burn_immunity", false }, + { ALife::eHitTypeExplosion, "explosion_immunity", false }, + { ALife::eHitTypeFireWound, "fire_wound_immunity", false }, + { ALife::eHitTypeLightBurn, "burn_immunity", false }, + { ALife::eHitTypePhysicStrike, "physic_strike_wound_immunity", true }, +}; + void CHitImmunity::LoadImmunities(const char* imm_sect, const CInifile* ini) { R_ASSERT2(ini->section_exist(imm_sect), imm_sect); - m_HitImmunityKoefs[ALife::eHitTypeBurn] = ini->r_float(imm_sect, "burn_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeStrike] = ini->r_float(imm_sect, "strike_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeShock] = ini->r_float(imm_sect, "shock_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeWound] = ini->r_float(imm_sect, "wound_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeRadiation] = ini->r_float(imm_sect, "radiation_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeTelepatic] = ini->r_float(imm_sect, "telepatic_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeChemicalBurn] = ini->r_float(imm_sect, "chemical_burn_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeExplosion] = ini->r_float(imm_sect, "explosion_immunity"); - m_HitImmunityKoefs[ALife::eHitTypeFireWound] = ini->r_float(imm_sect, "fire_wound_immunity"); - m_HitImmunityKoefs[ALife::eHitTypePhysicStrike] = ini->read_if_exists( - imm_sect, "physic_strike_wound_immunity", 1.0f); - m_HitImmunityKoefs[ALife::eHitTypeLightBurn] = m_HitImmunityKoefs[ALife::eHitTypeBurn]; + // SOC vs CS/COP are inverted, convert to CS/COP format. + const bool is_soc = GMLib.GetLibraryVersion() <= GAMEMTL_VERSION_SOC; + + for (const auto& [hit_type, immunity_name, optional] : hit_immunities) + { + if (optional && !ini->line_exist(imm_sect, immunity_name)) + continue; + + float immunity = ini->r_float(imm_sect, immunity_name); + if (is_soc) + immunity = 1.0f - immunity; + + m_HitImmunityKoefs[hit_type] = immunity; + } } void CHitImmunity::AddImmunities(const char* imm_sect, const CInifile* ini) { R_ASSERT2(ini->section_exist(imm_sect), imm_sect); - m_HitImmunityKoefs[ALife::eHitTypeBurn] += READ_IF_EXISTS(ini, r_float, imm_sect, "burn_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeStrike] += READ_IF_EXISTS(ini, r_float, imm_sect, "strike_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeShock] += READ_IF_EXISTS(ini, r_float, imm_sect, "shock_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeWound] += READ_IF_EXISTS(ini, r_float, imm_sect, "wound_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeRadiation] += READ_IF_EXISTS(ini, r_float, imm_sect, "radiation_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeTelepatic] += READ_IF_EXISTS(ini, r_float, imm_sect, "telepatic_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeChemicalBurn] += - READ_IF_EXISTS(ini, r_float, imm_sect, "chemical_burn_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeExplosion] += READ_IF_EXISTS(ini, r_float, imm_sect, "explosion_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeFireWound] += READ_IF_EXISTS(ini, r_float, imm_sect, "fire_wound_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypePhysicStrike] += ini->read_if_exists( - imm_sect, "physic_strike_wound_immunity", 0.0f); - m_HitImmunityKoefs[ALife::eHitTypeLightBurn] = m_HitImmunityKoefs[ALife::eHitTypeBurn]; + // Consistency is the key. + // We want this to have SOC format in SOC, the same as LoadImmunities does. + const bool is_soc = GMLib.GetLibraryVersion() <= GAMEMTL_VERSION_SOC; + + for (const auto& [hit_type, immunity_name, _] : hit_immunities) + { + if (!ini->line_exist(imm_sect, immunity_name)) + continue; + float immunity = ini->r_float(imm_sect, immunity_name); + if (is_soc) + immunity = 1.0f - immunity; + m_HitImmunityKoefs[hit_type] += immunity; + } } diff --git a/src/xrGame/script_game_object3.cpp b/src/xrGame/script_game_object3.cpp index 2f9d100555f..4a1f6c7cdfd 100644 --- a/src/xrGame/script_game_object3.cpp +++ b/src/xrGame/script_game_object3.cpp @@ -303,11 +303,7 @@ float CScriptGameObject::GetCurrentOutfitProtection(int hit_type) if (!o) return 0.0f; - const float protection = o->GetDefHitTypeProtection(ALife::EHitType(hit_type)); - - if (o->GetHitFracType() == SBoneProtections::HitFraction) - return 1.0f - protection; // SOC - return protection; + return o->GetDefHitTypeProtection(ALife::EHitType(hit_type)); } CScriptGameObject* CScriptGameObject::GetFood() const diff --git a/src/xrGame/ui/UIActorMenuInventory.cpp b/src/xrGame/ui/UIActorMenuInventory.cpp index ec86b98c3e6..31968d4b3ec 100644 --- a/src/xrGame/ui/UIActorMenuInventory.cpp +++ b/src/xrGame/ui/UIActorMenuInventory.cpp @@ -1555,7 +1555,7 @@ void CUIActorMenu::UpdateOutfit() if (m_OutfitInfo) { - m_OutfitInfo->UpdateInfo(outfit, nullptr, true); + m_OutfitInfo->UpdateInfo(outfit, nullptr, true, true); } if (ShadowOfChernobylMode) diff --git a/src/xrGame/ui/UIOutfitInfo.cpp b/src/xrGame/ui/UIOutfitInfo.cpp index 2736a327e50..42fe7863cb3 100644 --- a/src/xrGame/ui/UIOutfitInfo.cpp +++ b/src/xrGame/ui/UIOutfitInfo.cpp @@ -87,7 +87,7 @@ bool CUIOutfitImmunity::InitFromXml(CUIXml& xml_doc, pcstr base_str, pcstr immun return true; } -void CUIOutfitImmunity::SetProgressValue(float cur, float comp) +void CUIOutfitImmunity::SetProgressValue(float cur, float comp, float add /*= 0.0f*/) { string128 buf; @@ -96,12 +96,19 @@ void CUIOutfitImmunity::SetProgressValue(float cur, float comp) if (m_progress.IsShown()) { + if (fsimilar(cur, comp) && !fis_zero(add)) + comp = add; m_progress.SetTwoPos(cur, comp); - xr_sprintf(buf, "%.0f", cur); + + const auto sz = xr_sprintf(buf, "%.0f", cur); + if (!fis_zero(add)) + xr_sprintf(buf + sz, sizeof(buf) - sz, "+ %.0f%", add * m_magnitude); } else // SOC { - xr_sprintf(buf, "%s %+3.0f%%", cur > 0.0f ? "%c[green]" : "%c[red]", cur); + const auto sz = xr_sprintf(buf, "%s %+3.0f%%", cur > 0.0f ? "%c[green]" : "%c[red]", cur); + if (!fis_zero(add)) + xr_sprintf(buf + sz, sizeof(buf) - sz, "%s %+3.0f%%", add > 0.0f ? "%c[green]" : "%c[red]", add * m_magnitude); } m_value.SetText(buf); @@ -172,7 +179,8 @@ void CUIOutfitInfo::AdjustElements() SetWndSize(pos); } -void CUIOutfitInfo::UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit /*= nullptr*/, bool hide_if_zero_immunity /*= false*/) +void CUIOutfitInfo::UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit /*= nullptr*/, + bool add_artefact_info /*= false*/, bool hide_if_zero_immunity /*= false*/) { const CActor* actor = smart_cast(Level().CurrentViewEntity()); if (!actor || !cur_outfit) @@ -200,6 +208,7 @@ void CUIOutfitInfo::UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_ou float cur = cur_outfit->GetDefHitTypeProtection(hit_type); float slot = slot_outfit ? slot_outfit->GetDefHitTypeProtection(hit_type) : cur; + float add = add_artefact_info ? actor->GetProtection_ArtefactsOnBelt(hit_type) : 0.0f; if (is_cs_cop) { @@ -207,17 +216,12 @@ void CUIOutfitInfo::UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_ou cur /= max_power; // = 0..1 slot /= max_power; // = 0..1 } - else // SOC - { - cur = 1.0f - cur; - slot = 1.0f - slot; - } const bool hide = hide_if_zero_immunity && fis_zero(cur) && fis_zero(slot); item->Show(!hide); if (!hide) - item->SetProgressValue(cur, slot); + item->SetProgressValue(cur, slot, add); } if (const auto& fireWoundItem = m_items[ALife::eHitTypeFireWound]; diff --git a/src/xrGame/ui/UIOutfitInfo.h b/src/xrGame/ui/UIOutfitInfo.h index 29b79a07de6..37887dc2df0 100644 --- a/src/xrGame/ui/UIOutfitInfo.h +++ b/src/xrGame/ui/UIOutfitInfo.h @@ -15,7 +15,7 @@ class CUIOutfitImmunity final : public CUIStatic CUIOutfitImmunity(pcstr immunity_name); bool InitFromXml(CUIXml& xml_doc, pcstr base_str, pcstr immunity, pcstr immunity_text); - void SetProgressValue(float cur, float comp); + void SetProgressValue(float cur, float comp, float add = 0.0f); pcstr GetDebugType() override { return "CUIOutfitImmunity"; } @@ -33,7 +33,8 @@ class CUIOutfitInfo final : public CUIWindow CUIOutfitInfo() : CUIWindow("CUIOutfitInfo") {} void InitFromXml(CUIXml& xml_doc); - void UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit = nullptr, bool hide_if_zero_immunity = false); + void UpdateInfo(CCustomOutfit* cur_outfit, CCustomOutfit* slot_outfit = nullptr, + bool add_artefact_info = false, bool hide_if_zero_immunity = false); void UpdateInfo(CHelmet* cur_helmet, CHelmet* slot_helmet = nullptr); void AdjustElements(); diff --git a/src/xrGame/ui/ui_af_params.cpp b/src/xrGame/ui/ui_af_params.cpp index 27b9bb1ea8f..5946146376d 100644 --- a/src/xrGame/ui/ui_af_params.cpp +++ b/src/xrGame/ui/ui_af_params.cpp @@ -26,18 +26,18 @@ CUIArtefactParams::~CUIArtefactParams() xr_delete(m_Prop_line); } -constexpr std::tuple af_immunity[] = +constexpr std::tuple af_immunity[] = { - //{ ALife::infl_, "section", "caption", magnitude, sign_inverse, "unit" } - { ALife::infl_rad, "radiation_immunity", "ui_inv_outfit_radiation_protection", 100.0f, false, "%" }, - { ALife::infl_fire, "burn_immunity", "ui_inv_outfit_burn_protection", 100.0f, false, "%" }, - { ALife::infl_acid, "chemical_burn_immunity", "ui_inv_outfit_chemical_burn_protection", 100.0f, false, "%" }, - { ALife::infl_psi, "telepatic_immunity", "ui_inv_outfit_telepatic_protection", 100.0f, false, "%" }, - { ALife::infl_electra, "shock_immunity", "ui_inv_outfit_shock_protection", 100.0f, false, "%" }, - { 5, "strike_immunity", "ui_inv_outfit_strike_protection", 100.0f, false, "%" }, - { 6, "wound_immunity", "ui_inv_outfit_wound_protection", 100.0f, false, "%" }, - { 7, "explosion_immunity", "ui_inv_outfit_explosion_protection", 100.0f, false, "%" }, - { 8, "fire_wound_immunity", "ui_inv_outfit_fire_wound_protection", 100.0f, false, "%" }, + //{ ALife::EHitType, "section", "caption", magnitude, sign_inverse, "unit" } + { ALife::eHitTypeRadiation, "radiation_immunity", "ui_inv_outfit_radiation_protection", 100.0f, false, "%" }, + { ALife::eHitTypeBurn, "burn_immunity", "ui_inv_outfit_burn_protection", 100.0f, false, "%" }, + { ALife::eHitTypeChemicalBurn, "chemical_burn_immunity", "ui_inv_outfit_chemical_burn_protection", 100.0f, false, "%" }, + { ALife::eHitTypeTelepatic, "telepatic_immunity", "ui_inv_outfit_telepatic_protection", 100.0f, false, "%" }, + { ALife::eHitTypeShock, "shock_immunity", "ui_inv_outfit_shock_protection", 100.0f, false, "%" }, + { ALife::eHitTypeStrike, "strike_immunity", "ui_inv_outfit_strike_protection", 100.0f, false, "%" }, + { ALife::eHitTypeWound, "wound_immunity", "ui_inv_outfit_wound_protection", 100.0f, false, "%" }, + { ALife::eHitTypeExplosion, "explosion_immunity", "ui_inv_outfit_explosion_protection", 100.0f, false, "%" }, + { ALife::eHitTypeFireWound, "fire_wound_immunity", "ui_inv_outfit_fire_wound_protection", 100.0f, false, "%" }, }; constexpr std::tuple af_restore[] = @@ -126,6 +126,7 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) const auto& af_section = pInvItem.object().cNameSect().c_str(); const auto& actor_sect = actor->cNameSect().c_str(); const auto& condition_sect = pSettings->read_if_exists(actor_sect, "condition_sect", actor_sect); + const auto& hit_absorbation_sect = pSettings->r_string(af_section, "hit_absorbation_sect"); float h = 0.0f; if (m_Prop_line) @@ -168,29 +169,24 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) setValue(m_restore_item[id], val); } + CHitImmunity immunities; + immunities.LoadImmunities(hit_absorbation_sect, pSettings); + for (auto [id, immunity_section, immunity_caption, magnitude, sign_inverse, unit] : af_immunity) { if (!m_immunity_item[id]) continue; - shared_str const& hit_absorbation_sect = pSettings->r_string(af_section, "hit_absorbation_sect"); - float val = pSettings->r_float(hit_absorbation_sect, immunity_section); + float val = immunities.GetHitImmunity(id); + if (fis_zero(val)) + continue; + val *= pInvItem.GetCondition(); if (is_cs_cop) { - if (fis_zero(val)) - continue; - const float max_val = actor->conditions().GetZoneMaxPower(static_cast(id)); - val = val * pInvItem.GetCondition() / max_val; - } - else // SOC - { - if (fsimilar(val, 1.0f)) - continue; - val = 1.0f - val * pInvItem.GetCondition(); - + const float max_val = actor->conditions().GetZoneMaxPower(id); + val /= max_val; } - setValue(m_immunity_item[id], val); } diff --git a/src/xrGame/ui/ui_af_params.h b/src/xrGame/ui/ui_af_params.h index f205a8f544a..6ebf85a6f71 100644 --- a/src/xrGame/ui/ui_af_params.h +++ b/src/xrGame/ui/ui_af_params.h @@ -23,7 +23,7 @@ class CUIArtefactParams final : public CUIWindow protected: UIArtefactParamItem* m_disp_condition{}; //Alundaio: Show AF Condition - UIArtefactParamItem* m_immunity_item[9]{}; + UIArtefactParamItem* m_immunity_item[ALife::eHitTypeMax - 3]{}; UIArtefactParamItem* m_restore_item[ALife::eRestoreTypeMax]{}; UIArtefactParamItem* m_additional_weight{}; From 7e4a7ee57023e2db7968333e1f85f8356160f784 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 18:49:36 +0300 Subject: [PATCH 107/261] Talk dialog now properly works in SOC (#392) --- src/xrGame/ui/UICharacterInfo.cpp | 6 +++++- src/xrGame/ui/UITalkDialogWnd.cpp | 3 ++- src/xrGame/ui/UITalkWnd.cpp | 8 +++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/xrGame/ui/UICharacterInfo.cpp b/src/xrGame/ui/UICharacterInfo.cpp index 7e36c5f2536..667e56d47ec 100644 --- a/src/xrGame/ui/UICharacterInfo.cpp +++ b/src/xrGame/ui/UICharacterInfo.cpp @@ -42,7 +42,11 @@ void CUICharacterInfo::InitCharacterInfo(Fvector2 pos, Fvector2 size, CUIXml* xm Init_IconInfoItem(*xml_doc, "icon", eIcon); if (!m_icons[eIcon]) - Init_IconInfoItem(*xml_doc, "icon_static", eIcon); + { + Init_IconInfoItem(*xml_doc, "icon_static", eIcon); // SOC + if (m_icons[eIcon]) + m_icons[eIcon]->SetStretchTexture(true); + } m_original_color = m_icons[eIcon] ? m_icons[eIcon]->GetTextureColor() : color_xrgb(255, 255, 255); diff --git a/src/xrGame/ui/UITalkDialogWnd.cpp b/src/xrGame/ui/UITalkDialogWnd.cpp index 97478b884d9..cc70384591c 100644 --- a/src/xrGame/ui/UITalkDialogWnd.cpp +++ b/src/xrGame/ui/UITalkDialogWnd.cpp @@ -44,7 +44,8 @@ void CUITalkDialogWnd::InitTalkDialogWnd() m_uiXml = xr_new(); m_uiXml->Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, TALK_XML); - CUIXmlInit::InitWindow(*m_uiXml, "main", 0, this, false); + if (!CUIXmlInit::InitWindow(*m_uiXml, "main", 0, this, false)) + SetWndRect({ 0, 0, UI_BASE_WIDTH, UI_BASE_HEIGHT }); UIStaticTop = UIHelper::CreateStatic(*m_uiXml, "top_background", this, false); UIStaticBottom = UIHelper::CreateStatic(*m_uiXml, "bottom_background", this, false); diff --git a/src/xrGame/ui/UITalkWnd.cpp b/src/xrGame/ui/UITalkWnd.cpp index 69b277e88c0..c588241f25b 100644 --- a/src/xrGame/ui/UITalkWnd.cpp +++ b/src/xrGame/ui/UITalkWnd.cpp @@ -34,7 +34,7 @@ CUITalkWnd::CUITalkWnd() : CUIDialogWnd(CUITalkWnd::GetDebugType()) void CUITalkWnd::InitTalkWnd() { - inherited::SetWndRect(Frect().set(0, 0, UI_BASE_WIDTH, UI_BASE_HEIGHT)); + inherited::SetWndRect({ 0, 0, UI_BASE_WIDTH, UI_BASE_HEIGHT }); UITalkDialogWnd = xr_new(); UITalkDialogWnd->SetAutoDelete(true); @@ -59,8 +59,10 @@ void CUITalkWnd::InitTalkDialog() UITalkDialogWnd->UICharacterInfoLeft.InitCharacter(m_pOurInvOwner->object_id()); UITalkDialogWnd->UICharacterInfoRight.InitCharacter(m_pOthersInvOwner->object_id()); - //. UITalkDialogWnd->UIDialogFrame.UITitleText.SetText (m_pOthersInvOwner->Name()); - //. UITalkDialogWnd->UIOurPhrasesFrame.UITitleText.SetText (m_pOurInvOwner->Name()); + if (const auto title = UITalkDialogWnd->UIDialogFrame->GetTitleText()) + title->SetText(m_pOthersInvOwner->Name()); + if (const auto title = UITalkDialogWnd->UIOurPhrasesFrame->GetTitleText()) + title->SetText(m_pOurInvOwner->Name()); //очистить лог сообщений UITalkDialogWnd->ClearAll(); From 4a7d7e34425af350a43587c08b5093ccaa7bd33e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 18:49:36 +0300 Subject: [PATCH 108/261] Talk dialog now properly works in SOC (#392) --- src/xrGame/ui/UICharacterInfo.cpp | 6 +++++- src/xrGame/ui/UITalkDialogWnd.cpp | 3 ++- src/xrGame/ui/UITalkWnd.cpp | 8 +++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/xrGame/ui/UICharacterInfo.cpp b/src/xrGame/ui/UICharacterInfo.cpp index 7e36c5f2536..667e56d47ec 100644 --- a/src/xrGame/ui/UICharacterInfo.cpp +++ b/src/xrGame/ui/UICharacterInfo.cpp @@ -42,7 +42,11 @@ void CUICharacterInfo::InitCharacterInfo(Fvector2 pos, Fvector2 size, CUIXml* xm Init_IconInfoItem(*xml_doc, "icon", eIcon); if (!m_icons[eIcon]) - Init_IconInfoItem(*xml_doc, "icon_static", eIcon); + { + Init_IconInfoItem(*xml_doc, "icon_static", eIcon); // SOC + if (m_icons[eIcon]) + m_icons[eIcon]->SetStretchTexture(true); + } m_original_color = m_icons[eIcon] ? m_icons[eIcon]->GetTextureColor() : color_xrgb(255, 255, 255); diff --git a/src/xrGame/ui/UITalkDialogWnd.cpp b/src/xrGame/ui/UITalkDialogWnd.cpp index 97478b884d9..cc70384591c 100644 --- a/src/xrGame/ui/UITalkDialogWnd.cpp +++ b/src/xrGame/ui/UITalkDialogWnd.cpp @@ -44,7 +44,8 @@ void CUITalkDialogWnd::InitTalkDialogWnd() m_uiXml = xr_new(); m_uiXml->Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, TALK_XML); - CUIXmlInit::InitWindow(*m_uiXml, "main", 0, this, false); + if (!CUIXmlInit::InitWindow(*m_uiXml, "main", 0, this, false)) + SetWndRect({ 0, 0, UI_BASE_WIDTH, UI_BASE_HEIGHT }); UIStaticTop = UIHelper::CreateStatic(*m_uiXml, "top_background", this, false); UIStaticBottom = UIHelper::CreateStatic(*m_uiXml, "bottom_background", this, false); diff --git a/src/xrGame/ui/UITalkWnd.cpp b/src/xrGame/ui/UITalkWnd.cpp index 69b277e88c0..c588241f25b 100644 --- a/src/xrGame/ui/UITalkWnd.cpp +++ b/src/xrGame/ui/UITalkWnd.cpp @@ -34,7 +34,7 @@ CUITalkWnd::CUITalkWnd() : CUIDialogWnd(CUITalkWnd::GetDebugType()) void CUITalkWnd::InitTalkWnd() { - inherited::SetWndRect(Frect().set(0, 0, UI_BASE_WIDTH, UI_BASE_HEIGHT)); + inherited::SetWndRect({ 0, 0, UI_BASE_WIDTH, UI_BASE_HEIGHT }); UITalkDialogWnd = xr_new(); UITalkDialogWnd->SetAutoDelete(true); @@ -59,8 +59,10 @@ void CUITalkWnd::InitTalkDialog() UITalkDialogWnd->UICharacterInfoLeft.InitCharacter(m_pOurInvOwner->object_id()); UITalkDialogWnd->UICharacterInfoRight.InitCharacter(m_pOthersInvOwner->object_id()); - //. UITalkDialogWnd->UIDialogFrame.UITitleText.SetText (m_pOthersInvOwner->Name()); - //. UITalkDialogWnd->UIOurPhrasesFrame.UITitleText.SetText (m_pOurInvOwner->Name()); + if (const auto title = UITalkDialogWnd->UIDialogFrame->GetTitleText()) + title->SetText(m_pOthersInvOwner->Name()); + if (const auto title = UITalkDialogWnd->UIOurPhrasesFrame->GetTitleText()) + title->SetText(m_pOurInvOwner->Name()); //очистить лог сообщений UITalkDialogWnd->ClearAll(); From c669d82346521079b059e895fdaa74487c770335 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 20:33:56 +0300 Subject: [PATCH 109/261] Remove CUITextFrameLineWnd It's not needed since the functionality is added directly to CUIFrameLineWnd in d249fdb1f451d0d940b8a96cbf46a2876b85ad00. When I was adding CUITextFrameLineWnd I wanted to save some memory in CUIFrameLineWnd, but it doesn't cost too much and we want full fledged SOC support. --- src/xrGame/ui/UIActorInfo.cpp | 19 +++++++++------- src/xrGame/ui/UIActorInfo.h | 6 +++--- src/xrGame/ui/UIHelper.cpp | 20 ----------------- src/xrGame/ui/UIHelper.h | 1 - src/xrUICore/CMakeLists.txt | 2 -- src/xrUICore/Windows/UITextFrameLineWnd.cpp | 10 --------- src/xrUICore/Windows/UITextFrameLineWnd.h | 24 --------------------- src/xrUICore/XML/UIXmlInitBase.cpp | 10 --------- src/xrUICore/XML/UIXmlInitBase.h | 2 -- src/xrUICore/xrUICore.vcxproj | 2 -- src/xrUICore/xrUICore.vcxproj.filters | 6 ------ 11 files changed, 14 insertions(+), 88 deletions(-) delete mode 100644 src/xrUICore/Windows/UITextFrameLineWnd.cpp delete mode 100644 src/xrUICore/Windows/UITextFrameLineWnd.h diff --git a/src/xrGame/ui/UIActorInfo.cpp b/src/xrGame/ui/UIActorInfo.cpp index 72b1ae158ba..1710f8b2274 100644 --- a/src/xrGame/ui/UIActorInfo.cpp +++ b/src/xrGame/ui/UIActorInfo.cpp @@ -2,7 +2,7 @@ #include "UIActorInfo.h" #include "UIXmlInit.h" -#include "xrUICore/Windows/UITextFrameLineWnd.h" +#include "xrUICore/Windows/UIFrameLineWnd.h" #include "xrUICore/Static/UIAnimatedStatic.h" #include "Actor.h" @@ -34,9 +34,9 @@ bool CUIActorInfoWnd::Init() CUIXmlInit::InitFrameWindow(uiXml, "chicon_frame_window", 0, UICharIconFrame); AttachChild(UICharIconFrame); - UICharIconHeader = xr_new(); + UICharIconHeader = xr_new("Character icon header"); UICharIconHeader->SetAutoDelete(true); - CUIXmlInit::InitTextFrameLine(uiXml, "chicon_frame_line", 0, UICharIconHeader); + CUIXmlInit::InitFrameLine(uiXml, "chicon_frame_line", 0, UICharIconHeader); UICharIconFrame->AttachChild(UICharIconHeader); UIAnimatedIcon = xr_new(); @@ -49,9 +49,9 @@ bool CUIActorInfoWnd::Init() CUIXmlInit::InitFrameWindow(uiXml, "info_frame_window", 0, UIInfoFrame); AttachChild(UIInfoFrame); - UIInfoHeader = xr_new(); + UIInfoHeader = xr_new("Info header"); UIInfoHeader->SetAutoDelete(true); - CUIXmlInit::InitTextFrameLine(uiXml, "info_frame_line", 0, UIInfoHeader); + CUIXmlInit::InitFrameLine(uiXml, "info_frame_line", 0, UIInfoHeader); UIInfoFrame->AttachChild(UIInfoHeader); UIDetailList = xr_new(); @@ -87,7 +87,8 @@ void CUIActorInfoWnd::Show(bool status) if (!status) return; UICharacterInfo->InitCharacter(Actor()->ID()); - UICharIconHeader->SetText(Actor()->Name()); + if (UICharIconHeader->GetTitleText()) + UICharIconHeader->GetTitleText()->SetText(Actor()->Name()); FillPointsInfo(); } @@ -197,13 +198,15 @@ void CUIActorInfoWnd::FillPointsDetail(const shared_str& id) if (id == "reputation") //reputation { - UIInfoHeader->GetTitleStatic()->SetTextST("st_detail_list_for_community_relations"); + if (UICharIconHeader->GetTitleText()) + UICharIconHeader->GetTitleText()->SetTextST("st_detail_list_for_community_relations"); FillReputationDetails(&uiXml, path); return; } string256 str; xr_sprintf(str, "st_detail_list_for_%s", id.c_str()); - UIInfoHeader->GetTitleStatic()->SetTextST(str); + if (UICharIconHeader->GetTitleText()) + UICharIconHeader->GetTitleText()->SetTextST(str); SStatSectionData& section = Actor()->StatisticMgr().GetSection(id); vStatDetailData::const_iterator it = section.data.begin(); diff --git a/src/xrGame/ui/UIActorInfo.h b/src/xrGame/ui/UIActorInfo.h index f93fb45524d..2b757ce2730 100644 --- a/src/xrGame/ui/UIActorInfo.h +++ b/src/xrGame/ui/UIActorInfo.h @@ -3,7 +3,7 @@ #include "xrUICore/Windows/UIWindow.h" class CUIFrameWindow; -class CUITextFrameLineWnd; +class CUIFrameLineWnd; class CUIAnimatedStatic; class CUIStatic; class CUICharacterInfo; @@ -28,9 +28,9 @@ class CUIActorInfoWnd final : public CUIWindow protected: CUIFrameWindow* UIInfoFrame{}; - CUITextFrameLineWnd* UIInfoHeader{}; + CUIFrameLineWnd* UIInfoHeader{}; CUIFrameWindow* UICharIconFrame{}; - CUITextFrameLineWnd* UICharIconHeader{}; + CUIFrameLineWnd* UICharIconHeader{}; CUIAnimatedStatic* UIAnimatedIcon{}; CUIWindow* UICharacterWindow{}; diff --git a/src/xrGame/ui/UIHelper.cpp b/src/xrGame/ui/UIHelper.cpp index adf072de75c..e82a52c032d 100644 --- a/src/xrGame/ui/UIHelper.cpp +++ b/src/xrGame/ui/UIHelper.cpp @@ -12,7 +12,6 @@ #include "xrUICore/ProgressBar/UIProgressBar.h" #include "xrUICore/ProgressBar/UIProgressShape.h" #include "xrUICore/Windows/UIFrameLineWnd.h" -#include "xrUICore/Windows/UITextFrameLineWnd.h" #include "xrUICore/Windows/UIFrameWindow.h" #include "xrUICore/Buttons/UI3tButton.h" #include "xrUICore/Buttons/UICheckButton.h" @@ -160,25 +159,6 @@ CUIFrameLineWnd* UIHelper::CreateFrameLine(CUIXml& xml, LPCSTR ui_path, CUIWindo return ui; } -CUITextFrameLineWnd* UIHelper::CreateTextFrameLine(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical /*= true*/) -{ - // If it's not critical element, then don't crash if it doesn't exist - if (!critical && !xml.NavigateToNode(ui_path, 0)) - return nullptr; - - auto ui = xr_new(); - if (!CUIXmlInit::InitTextFrameLine(xml, ui_path, 0, ui, critical) && !critical) - { - xr_delete(ui); - } - if (ui && parent) - { - parent->AttachChild(ui); - ui->SetAutoDelete(true); - } - return ui; -} - CUIFrameWindow* UIHelper::CreateFrameWindow(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical) { // If it's not critical element, then don't crash if it doesn't exist diff --git a/src/xrGame/ui/UIHelper.h b/src/xrGame/ui/UIHelper.h index b450df3b91d..2dd29e6522f 100644 --- a/src/xrGame/ui/UIHelper.h +++ b/src/xrGame/ui/UIHelper.h @@ -36,7 +36,6 @@ class UIHelper static CUIProgressBar* CreateProgressBar(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUIProgressShape* CreateProgressShape(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUIFrameLineWnd* CreateFrameLine(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); - static CUITextFrameLineWnd* CreateTextFrameLine(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUIFrameWindow* CreateFrameWindow(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUI3tButton* Create3tButton(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUI3tButton* Create3tButton(CUIXml& xml, LPCSTR ui_path, int index, CUIWindow* parent, bool critical = true); diff --git a/src/xrUICore/CMakeLists.txt b/src/xrUICore/CMakeLists.txt index ae52d311ec3..84641507e18 100644 --- a/src/xrUICore/CMakeLists.txt +++ b/src/xrUICore/CMakeLists.txt @@ -112,8 +112,6 @@ target_sources(xrUICore PRIVATE Windows/UIFrameLineWnd.h Windows/UIFrameWindow.cpp Windows/UIFrameWindow.h - Windows/UITextFrameLineWnd.cpp - Windows/UITextFrameLineWnd.h Windows/UIWindow.cpp Windows/UIWindow.h XML/UITextureMaster.cpp diff --git a/src/xrUICore/Windows/UITextFrameLineWnd.cpp b/src/xrUICore/Windows/UITextFrameLineWnd.cpp deleted file mode 100644 index 6101ebb22ff..00000000000 --- a/src/xrUICore/Windows/UITextFrameLineWnd.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include "pch.hpp" -#include "UITextFrameLineWnd.h" - -CUITextFrameLineWnd::CUITextFrameLineWnd() - : CUIWindow(CUITextFrameLineWnd::GetDebugType()), - m_frameline("Frameline"), m_title("Title") -{ - AttachChild(&m_title); - AttachChild(&m_frameline); -} diff --git a/src/xrUICore/Windows/UITextFrameLineWnd.h b/src/xrUICore/Windows/UITextFrameLineWnd.h deleted file mode 100644 index 3064a53ae38..00000000000 --- a/src/xrUICore/Windows/UITextFrameLineWnd.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#include "xrUICore/Windows/UIWindow.h" -#include "xrUICore/Windows/UIFrameLineWnd.h" -#include "xrUICore/Static/UIStatic.h" - -class XRUICORE_API CUITextFrameLineWnd final : public CUIWindow -{ - using inherited = CUIWindow; - -public: - CUITextFrameLineWnd(); - - // Also we can display textual caption on the frame - CUIStatic* GetTitleStatic() { return &m_title; } - void SetText(pcstr text) { m_title.SetText(text); } - - pcstr GetDebugType() override { return "CUITextFrameLineWnd"; } - -protected: - friend class CUIXmlInitBase; - - CUIFrameLineWnd m_frameline; - CUIStatic m_title; -}; diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 065162a8c2c..6fbf252d10c 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -1,7 +1,6 @@ #include "pch.hpp" #include "UIXmlInitBase.h" #include "Windows/UIFrameWindow.h" -#include "Windows/UITextFrameLineWnd.h" #include "Buttons/UICheckButton.h" #include "SpinBox/UICustomSpin.h" #include "Buttons/UIRadioButton.h" @@ -810,15 +809,6 @@ bool CUIXmlInitBase::InitFrameLine(CUIXml& xml_doc, pcstr path, int index, CUIFr return result; } -bool CUIXmlInitBase::InitTextFrameLine(CUIXml& xml_doc, pcstr path, int index, CUITextFrameLineWnd* pWnd, bool fatal /*= true*/) -{ - string256 buf; - strconcat(buf, path, ":title"); - InitStatic(xml_doc, buf, index, &pWnd->m_title, false); - - return InitFrameLine(xml_doc, path, index, &pWnd->m_frameline, fatal); -} - bool CUIXmlInitBase::InitCustomEdit(CUIXml& xml_doc, pcstr path, int index, CUICustomEdit* pWnd, bool fatal /*= true*/) { if (!InitStatic(xml_doc, path, index, pWnd, fatal)) diff --git a/src/xrUICore/XML/UIXmlInitBase.h b/src/xrUICore/XML/UIXmlInitBase.h index cffb5e43122..4f4d16894f7 100644 --- a/src/xrUICore/XML/UIXmlInitBase.h +++ b/src/xrUICore/XML/UIXmlInitBase.h @@ -17,7 +17,6 @@ class CUIProgressBar; class CUIProgressShape; class CUITabControl; class CUIFrameLineWnd; -class CUITextFrameLineWnd; class CUIEditBoxEx; class CUIEditBox; class CUICustomEdit; @@ -42,7 +41,6 @@ class XRUICORE_API CUIXmlInitBase static bool InitWindow(CUIXml& xml_doc, pcstr path, int index, CUIWindow* pWnd, bool fatal = true); static bool InitFrameWindow(CUIXml& xml_doc, pcstr path, int index, CUIFrameWindow* pWnd, bool fatal = true); static bool InitFrameLine(CUIXml& xml_doc, pcstr path, int index, CUIFrameLineWnd* pWnd, bool fatal = true); - static bool InitTextFrameLine(CUIXml& xml_doc, pcstr path, int index, CUITextFrameLineWnd* pWnd, bool fatal = true); static bool InitCustomEdit(CUIXml& xml_doc, pcstr paht, int index, CUICustomEdit* pWnd, bool fatal = true); static bool InitEditBox(CUIXml& xml_doc, pcstr paht, int index, CUIEditBox* pWnd, bool fatal = true); static bool InitStatic(CUIXml& xml_doc, pcstr path, int index, CUIStatic* pWnd, bool fatal = true, bool textWnd = false); diff --git a/src/xrUICore/xrUICore.vcxproj b/src/xrUICore/xrUICore.vcxproj index 0b9e9b29c63..cb63a2bfb6b 100644 --- a/src/xrUICore/xrUICore.vcxproj +++ b/src/xrUICore/xrUICore.vcxproj @@ -114,7 +114,6 @@ - @@ -176,7 +175,6 @@ - diff --git a/src/xrUICore/xrUICore.vcxproj.filters b/src/xrUICore/xrUICore.vcxproj.filters index 75b334576b2..6f73fa12502 100644 --- a/src/xrUICore/xrUICore.vcxproj.filters +++ b/src/xrUICore/xrUICore.vcxproj.filters @@ -235,9 +235,6 @@ ListWnd - - Windows - @@ -413,9 +410,6 @@ ListWnd - - Windows - From ee3611a069c1189b85e6c4b6bc8afb98a5185d93 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 20:33:56 +0300 Subject: [PATCH 110/261] Remove CUITextFrameLineWnd It's not needed since the functionality is added directly to CUIFrameLineWnd in d249fdb1f451d0d940b8a96cbf46a2876b85ad00. When I was adding CUITextFrameLineWnd I wanted to save some memory in CUIFrameLineWnd, but it doesn't cost too much and we want full fledged SOC support. --- src/xrGame/ui/UIActorInfo.cpp | 19 +++++++++------- src/xrGame/ui/UIActorInfo.h | 6 +++--- src/xrGame/ui/UIHelper.cpp | 20 ----------------- src/xrGame/ui/UIHelper.h | 1 - src/xrUICore/CMakeLists.txt | 2 -- src/xrUICore/Windows/UITextFrameLineWnd.cpp | 10 --------- src/xrUICore/Windows/UITextFrameLineWnd.h | 24 --------------------- src/xrUICore/XML/UIXmlInitBase.cpp | 10 --------- src/xrUICore/XML/UIXmlInitBase.h | 2 -- src/xrUICore/xrUICore.vcxproj | 2 -- src/xrUICore/xrUICore.vcxproj.filters | 6 ------ 11 files changed, 14 insertions(+), 88 deletions(-) delete mode 100644 src/xrUICore/Windows/UITextFrameLineWnd.cpp delete mode 100644 src/xrUICore/Windows/UITextFrameLineWnd.h diff --git a/src/xrGame/ui/UIActorInfo.cpp b/src/xrGame/ui/UIActorInfo.cpp index 72b1ae158ba..1710f8b2274 100644 --- a/src/xrGame/ui/UIActorInfo.cpp +++ b/src/xrGame/ui/UIActorInfo.cpp @@ -2,7 +2,7 @@ #include "UIActorInfo.h" #include "UIXmlInit.h" -#include "xrUICore/Windows/UITextFrameLineWnd.h" +#include "xrUICore/Windows/UIFrameLineWnd.h" #include "xrUICore/Static/UIAnimatedStatic.h" #include "Actor.h" @@ -34,9 +34,9 @@ bool CUIActorInfoWnd::Init() CUIXmlInit::InitFrameWindow(uiXml, "chicon_frame_window", 0, UICharIconFrame); AttachChild(UICharIconFrame); - UICharIconHeader = xr_new(); + UICharIconHeader = xr_new("Character icon header"); UICharIconHeader->SetAutoDelete(true); - CUIXmlInit::InitTextFrameLine(uiXml, "chicon_frame_line", 0, UICharIconHeader); + CUIXmlInit::InitFrameLine(uiXml, "chicon_frame_line", 0, UICharIconHeader); UICharIconFrame->AttachChild(UICharIconHeader); UIAnimatedIcon = xr_new(); @@ -49,9 +49,9 @@ bool CUIActorInfoWnd::Init() CUIXmlInit::InitFrameWindow(uiXml, "info_frame_window", 0, UIInfoFrame); AttachChild(UIInfoFrame); - UIInfoHeader = xr_new(); + UIInfoHeader = xr_new("Info header"); UIInfoHeader->SetAutoDelete(true); - CUIXmlInit::InitTextFrameLine(uiXml, "info_frame_line", 0, UIInfoHeader); + CUIXmlInit::InitFrameLine(uiXml, "info_frame_line", 0, UIInfoHeader); UIInfoFrame->AttachChild(UIInfoHeader); UIDetailList = xr_new(); @@ -87,7 +87,8 @@ void CUIActorInfoWnd::Show(bool status) if (!status) return; UICharacterInfo->InitCharacter(Actor()->ID()); - UICharIconHeader->SetText(Actor()->Name()); + if (UICharIconHeader->GetTitleText()) + UICharIconHeader->GetTitleText()->SetText(Actor()->Name()); FillPointsInfo(); } @@ -197,13 +198,15 @@ void CUIActorInfoWnd::FillPointsDetail(const shared_str& id) if (id == "reputation") //reputation { - UIInfoHeader->GetTitleStatic()->SetTextST("st_detail_list_for_community_relations"); + if (UICharIconHeader->GetTitleText()) + UICharIconHeader->GetTitleText()->SetTextST("st_detail_list_for_community_relations"); FillReputationDetails(&uiXml, path); return; } string256 str; xr_sprintf(str, "st_detail_list_for_%s", id.c_str()); - UIInfoHeader->GetTitleStatic()->SetTextST(str); + if (UICharIconHeader->GetTitleText()) + UICharIconHeader->GetTitleText()->SetTextST(str); SStatSectionData& section = Actor()->StatisticMgr().GetSection(id); vStatDetailData::const_iterator it = section.data.begin(); diff --git a/src/xrGame/ui/UIActorInfo.h b/src/xrGame/ui/UIActorInfo.h index f93fb45524d..2b757ce2730 100644 --- a/src/xrGame/ui/UIActorInfo.h +++ b/src/xrGame/ui/UIActorInfo.h @@ -3,7 +3,7 @@ #include "xrUICore/Windows/UIWindow.h" class CUIFrameWindow; -class CUITextFrameLineWnd; +class CUIFrameLineWnd; class CUIAnimatedStatic; class CUIStatic; class CUICharacterInfo; @@ -28,9 +28,9 @@ class CUIActorInfoWnd final : public CUIWindow protected: CUIFrameWindow* UIInfoFrame{}; - CUITextFrameLineWnd* UIInfoHeader{}; + CUIFrameLineWnd* UIInfoHeader{}; CUIFrameWindow* UICharIconFrame{}; - CUITextFrameLineWnd* UICharIconHeader{}; + CUIFrameLineWnd* UICharIconHeader{}; CUIAnimatedStatic* UIAnimatedIcon{}; CUIWindow* UICharacterWindow{}; diff --git a/src/xrGame/ui/UIHelper.cpp b/src/xrGame/ui/UIHelper.cpp index adf072de75c..e82a52c032d 100644 --- a/src/xrGame/ui/UIHelper.cpp +++ b/src/xrGame/ui/UIHelper.cpp @@ -12,7 +12,6 @@ #include "xrUICore/ProgressBar/UIProgressBar.h" #include "xrUICore/ProgressBar/UIProgressShape.h" #include "xrUICore/Windows/UIFrameLineWnd.h" -#include "xrUICore/Windows/UITextFrameLineWnd.h" #include "xrUICore/Windows/UIFrameWindow.h" #include "xrUICore/Buttons/UI3tButton.h" #include "xrUICore/Buttons/UICheckButton.h" @@ -160,25 +159,6 @@ CUIFrameLineWnd* UIHelper::CreateFrameLine(CUIXml& xml, LPCSTR ui_path, CUIWindo return ui; } -CUITextFrameLineWnd* UIHelper::CreateTextFrameLine(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical /*= true*/) -{ - // If it's not critical element, then don't crash if it doesn't exist - if (!critical && !xml.NavigateToNode(ui_path, 0)) - return nullptr; - - auto ui = xr_new(); - if (!CUIXmlInit::InitTextFrameLine(xml, ui_path, 0, ui, critical) && !critical) - { - xr_delete(ui); - } - if (ui && parent) - { - parent->AttachChild(ui); - ui->SetAutoDelete(true); - } - return ui; -} - CUIFrameWindow* UIHelper::CreateFrameWindow(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical) { // If it's not critical element, then don't crash if it doesn't exist diff --git a/src/xrGame/ui/UIHelper.h b/src/xrGame/ui/UIHelper.h index b450df3b91d..2dd29e6522f 100644 --- a/src/xrGame/ui/UIHelper.h +++ b/src/xrGame/ui/UIHelper.h @@ -36,7 +36,6 @@ class UIHelper static CUIProgressBar* CreateProgressBar(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUIProgressShape* CreateProgressShape(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUIFrameLineWnd* CreateFrameLine(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); - static CUITextFrameLineWnd* CreateTextFrameLine(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUIFrameWindow* CreateFrameWindow(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUI3tButton* Create3tButton(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUI3tButton* Create3tButton(CUIXml& xml, LPCSTR ui_path, int index, CUIWindow* parent, bool critical = true); diff --git a/src/xrUICore/CMakeLists.txt b/src/xrUICore/CMakeLists.txt index ae52d311ec3..84641507e18 100644 --- a/src/xrUICore/CMakeLists.txt +++ b/src/xrUICore/CMakeLists.txt @@ -112,8 +112,6 @@ target_sources(xrUICore PRIVATE Windows/UIFrameLineWnd.h Windows/UIFrameWindow.cpp Windows/UIFrameWindow.h - Windows/UITextFrameLineWnd.cpp - Windows/UITextFrameLineWnd.h Windows/UIWindow.cpp Windows/UIWindow.h XML/UITextureMaster.cpp diff --git a/src/xrUICore/Windows/UITextFrameLineWnd.cpp b/src/xrUICore/Windows/UITextFrameLineWnd.cpp deleted file mode 100644 index 6101ebb22ff..00000000000 --- a/src/xrUICore/Windows/UITextFrameLineWnd.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include "pch.hpp" -#include "UITextFrameLineWnd.h" - -CUITextFrameLineWnd::CUITextFrameLineWnd() - : CUIWindow(CUITextFrameLineWnd::GetDebugType()), - m_frameline("Frameline"), m_title("Title") -{ - AttachChild(&m_title); - AttachChild(&m_frameline); -} diff --git a/src/xrUICore/Windows/UITextFrameLineWnd.h b/src/xrUICore/Windows/UITextFrameLineWnd.h deleted file mode 100644 index 3064a53ae38..00000000000 --- a/src/xrUICore/Windows/UITextFrameLineWnd.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#include "xrUICore/Windows/UIWindow.h" -#include "xrUICore/Windows/UIFrameLineWnd.h" -#include "xrUICore/Static/UIStatic.h" - -class XRUICORE_API CUITextFrameLineWnd final : public CUIWindow -{ - using inherited = CUIWindow; - -public: - CUITextFrameLineWnd(); - - // Also we can display textual caption on the frame - CUIStatic* GetTitleStatic() { return &m_title; } - void SetText(pcstr text) { m_title.SetText(text); } - - pcstr GetDebugType() override { return "CUITextFrameLineWnd"; } - -protected: - friend class CUIXmlInitBase; - - CUIFrameLineWnd m_frameline; - CUIStatic m_title; -}; diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 065162a8c2c..6fbf252d10c 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -1,7 +1,6 @@ #include "pch.hpp" #include "UIXmlInitBase.h" #include "Windows/UIFrameWindow.h" -#include "Windows/UITextFrameLineWnd.h" #include "Buttons/UICheckButton.h" #include "SpinBox/UICustomSpin.h" #include "Buttons/UIRadioButton.h" @@ -810,15 +809,6 @@ bool CUIXmlInitBase::InitFrameLine(CUIXml& xml_doc, pcstr path, int index, CUIFr return result; } -bool CUIXmlInitBase::InitTextFrameLine(CUIXml& xml_doc, pcstr path, int index, CUITextFrameLineWnd* pWnd, bool fatal /*= true*/) -{ - string256 buf; - strconcat(buf, path, ":title"); - InitStatic(xml_doc, buf, index, &pWnd->m_title, false); - - return InitFrameLine(xml_doc, path, index, &pWnd->m_frameline, fatal); -} - bool CUIXmlInitBase::InitCustomEdit(CUIXml& xml_doc, pcstr path, int index, CUICustomEdit* pWnd, bool fatal /*= true*/) { if (!InitStatic(xml_doc, path, index, pWnd, fatal)) diff --git a/src/xrUICore/XML/UIXmlInitBase.h b/src/xrUICore/XML/UIXmlInitBase.h index cffb5e43122..4f4d16894f7 100644 --- a/src/xrUICore/XML/UIXmlInitBase.h +++ b/src/xrUICore/XML/UIXmlInitBase.h @@ -17,7 +17,6 @@ class CUIProgressBar; class CUIProgressShape; class CUITabControl; class CUIFrameLineWnd; -class CUITextFrameLineWnd; class CUIEditBoxEx; class CUIEditBox; class CUICustomEdit; @@ -42,7 +41,6 @@ class XRUICORE_API CUIXmlInitBase static bool InitWindow(CUIXml& xml_doc, pcstr path, int index, CUIWindow* pWnd, bool fatal = true); static bool InitFrameWindow(CUIXml& xml_doc, pcstr path, int index, CUIFrameWindow* pWnd, bool fatal = true); static bool InitFrameLine(CUIXml& xml_doc, pcstr path, int index, CUIFrameLineWnd* pWnd, bool fatal = true); - static bool InitTextFrameLine(CUIXml& xml_doc, pcstr path, int index, CUITextFrameLineWnd* pWnd, bool fatal = true); static bool InitCustomEdit(CUIXml& xml_doc, pcstr paht, int index, CUICustomEdit* pWnd, bool fatal = true); static bool InitEditBox(CUIXml& xml_doc, pcstr paht, int index, CUIEditBox* pWnd, bool fatal = true); static bool InitStatic(CUIXml& xml_doc, pcstr path, int index, CUIStatic* pWnd, bool fatal = true, bool textWnd = false); diff --git a/src/xrUICore/xrUICore.vcxproj b/src/xrUICore/xrUICore.vcxproj index 0b9e9b29c63..cb63a2bfb6b 100644 --- a/src/xrUICore/xrUICore.vcxproj +++ b/src/xrUICore/xrUICore.vcxproj @@ -114,7 +114,6 @@ - @@ -176,7 +175,6 @@ - diff --git a/src/xrUICore/xrUICore.vcxproj.filters b/src/xrUICore/xrUICore.vcxproj.filters index 75b334576b2..6f73fa12502 100644 --- a/src/xrUICore/xrUICore.vcxproj.filters +++ b/src/xrUICore/xrUICore.vcxproj.filters @@ -235,9 +235,6 @@ ListWnd - - Windows - @@ -413,9 +410,6 @@ ListWnd - - Windows - From 93352b18f7f94011a3d6c107d62332df894df9c8 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 21:44:10 +0300 Subject: [PATCH 111/261] xrGame/ui/UIInventoryUtilities.h: return non-const shared_str instance Returning const instance may e.g. prevents optimizations. (const reference is a whole different case) Also, GetGameTimeAsString now accepts full_mode and passes it to GetTimeAsString --- src/xrGame/ui/UIInventoryUtilities.cpp | 16 ++++++++-------- src/xrGame/ui/UIInventoryUtilities.h | 13 ++++++------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/xrGame/ui/UIInventoryUtilities.cpp b/src/xrGame/ui/UIInventoryUtilities.cpp index b89d340cb3f..84c4ca3d6c6 100644 --- a/src/xrGame/ui/UIInventoryUtilities.cpp +++ b/src/xrGame/ui/UIInventoryUtilities.cpp @@ -215,19 +215,19 @@ const ui_shader& InventoryUtilities::GetWeaponUpgradeIconsShader() ////////////////////////////////////////////////////////////////////////// -const shared_str InventoryUtilities::GetGameDateAsString(EDatePrecision datePrec, char dateSeparator) +shared_str InventoryUtilities::GetGameDateAsString(EDatePrecision datePrec, char dateSeparator) { return GetDateAsString(Level().GetGameTime(), datePrec, dateSeparator); } ////////////////////////////////////////////////////////////////////////// -const shared_str InventoryUtilities::GetGameTimeAsString(ETimePrecision timePrec, char timeSeparator) +shared_str InventoryUtilities::GetGameTimeAsString(ETimePrecision timePrec, char timeSeparator, bool full_mode) { - return GetTimeAsString(Level().GetGameTime(), timePrec, timeSeparator); + return GetTimeAsString(Level().GetGameTime(), timePrec, timeSeparator, full_mode); } -const shared_str InventoryUtilities::GetTimeAndDateAsString(ALife::_TIME_ID time) +shared_str InventoryUtilities::GetTimeAndDateAsString(ALife::_TIME_ID time) { string256 buf; LPCSTR time_str = GetTimeAsString(time, etpTimeToMinutes).c_str(); @@ -236,14 +236,14 @@ const shared_str InventoryUtilities::GetTimeAndDateAsString(ALife::_TIME_ID time return buf; } -const shared_str InventoryUtilities::Get_GameTimeAndDate_AsString() +shared_str InventoryUtilities::Get_GameTimeAndDate_AsString() { return GetTimeAndDateAsString(Level().GetGameTime()); } ////////////////////////////////////////////////////////////////////////// -const shared_str InventoryUtilities::GetTimeAsString( +shared_str InventoryUtilities::GetTimeAsString( ALife::_TIME_ID time, ETimePrecision timePrec, char timeSeparator, bool full_mode) { string32 bufTime; @@ -296,7 +296,7 @@ const shared_str InventoryUtilities::GetTimeAsString( return bufTime; } -const shared_str InventoryUtilities::GetDateAsString(ALife::_TIME_ID date, EDatePrecision datePrec, char dateSeparator) +shared_str InventoryUtilities::GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator) { string64 bufDate; @@ -304,7 +304,7 @@ const shared_str InventoryUtilities::GetDateAsString(ALife::_TIME_ID date, EDate u32 year = 0, month = 0, day = 0, hours = 0, mins = 0, secs = 0, milisecs = 0; - split_time(date, year, month, day, hours, mins, secs, milisecs); + split_time(time, year, month, day, hours, mins, secs, milisecs); VERIFY(1 <= month && month <= 12); LPCSTR month_str = StringTable().translate(st_months[month - 1]).c_str(); diff --git a/src/xrGame/ui/UIInventoryUtilities.h b/src/xrGame/ui/UIInventoryUtilities.h index aec3cc63893..b0a63a2bb1b 100644 --- a/src/xrGame/ui/UIInventoryUtilities.h +++ b/src/xrGame/ui/UIInventoryUtilities.h @@ -64,13 +64,12 @@ enum EDatePrecision edpDateToYear }; -const shared_str GetGameDateAsString(EDatePrecision datePrec, char dateSeparator = ','); -const shared_str GetGameTimeAsString(ETimePrecision timePrec, char timeSeparator = ':'); -const shared_str GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator = ','); -const shared_str GetTimeAsString( - ALife::_TIME_ID time, ETimePrecision timePrec, char timeSeparator = ':', bool full_mode = true); -const shared_str GetTimeAndDateAsString(ALife::_TIME_ID time); -const shared_str Get_GameTimeAndDate_AsString(); +shared_str GetGameDateAsString(EDatePrecision datePrec, char dateSeparator = ','); +shared_str GetGameTimeAsString(ETimePrecision timePrec, char timeSeparator = ':', bool full_mode = true); +shared_str GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator = ','); +shared_str GetTimeAsString(ALife::_TIME_ID time, ETimePrecision timePrec, char timeSeparator = ':', bool full_mode = true); +shared_str GetTimeAndDateAsString(ALife::_TIME_ID time); +shared_str Get_GameTimeAndDate_AsString(); LPCSTR GetTimePeriodAsString(pstr _buff, u32 buff_sz, ALife::_TIME_ID _from, ALife::_TIME_ID _to); // Отобразить вес, который несет (*pInvOwner) From 1ae93e93fc06a906f592b9d42ebf44b98d7c8948 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 21:44:10 +0300 Subject: [PATCH 112/261] xrGame/ui/UIInventoryUtilities.h: return non-const shared_str instance Returning const instance may e.g. prevents optimizations. (const reference is a whole different case) Also, GetGameTimeAsString now accepts full_mode and passes it to GetTimeAsString --- src/xrGame/ui/UIInventoryUtilities.cpp | 16 ++++++++-------- src/xrGame/ui/UIInventoryUtilities.h | 13 ++++++------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/xrGame/ui/UIInventoryUtilities.cpp b/src/xrGame/ui/UIInventoryUtilities.cpp index b89d340cb3f..84c4ca3d6c6 100644 --- a/src/xrGame/ui/UIInventoryUtilities.cpp +++ b/src/xrGame/ui/UIInventoryUtilities.cpp @@ -215,19 +215,19 @@ const ui_shader& InventoryUtilities::GetWeaponUpgradeIconsShader() ////////////////////////////////////////////////////////////////////////// -const shared_str InventoryUtilities::GetGameDateAsString(EDatePrecision datePrec, char dateSeparator) +shared_str InventoryUtilities::GetGameDateAsString(EDatePrecision datePrec, char dateSeparator) { return GetDateAsString(Level().GetGameTime(), datePrec, dateSeparator); } ////////////////////////////////////////////////////////////////////////// -const shared_str InventoryUtilities::GetGameTimeAsString(ETimePrecision timePrec, char timeSeparator) +shared_str InventoryUtilities::GetGameTimeAsString(ETimePrecision timePrec, char timeSeparator, bool full_mode) { - return GetTimeAsString(Level().GetGameTime(), timePrec, timeSeparator); + return GetTimeAsString(Level().GetGameTime(), timePrec, timeSeparator, full_mode); } -const shared_str InventoryUtilities::GetTimeAndDateAsString(ALife::_TIME_ID time) +shared_str InventoryUtilities::GetTimeAndDateAsString(ALife::_TIME_ID time) { string256 buf; LPCSTR time_str = GetTimeAsString(time, etpTimeToMinutes).c_str(); @@ -236,14 +236,14 @@ const shared_str InventoryUtilities::GetTimeAndDateAsString(ALife::_TIME_ID time return buf; } -const shared_str InventoryUtilities::Get_GameTimeAndDate_AsString() +shared_str InventoryUtilities::Get_GameTimeAndDate_AsString() { return GetTimeAndDateAsString(Level().GetGameTime()); } ////////////////////////////////////////////////////////////////////////// -const shared_str InventoryUtilities::GetTimeAsString( +shared_str InventoryUtilities::GetTimeAsString( ALife::_TIME_ID time, ETimePrecision timePrec, char timeSeparator, bool full_mode) { string32 bufTime; @@ -296,7 +296,7 @@ const shared_str InventoryUtilities::GetTimeAsString( return bufTime; } -const shared_str InventoryUtilities::GetDateAsString(ALife::_TIME_ID date, EDatePrecision datePrec, char dateSeparator) +shared_str InventoryUtilities::GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator) { string64 bufDate; @@ -304,7 +304,7 @@ const shared_str InventoryUtilities::GetDateAsString(ALife::_TIME_ID date, EDate u32 year = 0, month = 0, day = 0, hours = 0, mins = 0, secs = 0, milisecs = 0; - split_time(date, year, month, day, hours, mins, secs, milisecs); + split_time(time, year, month, day, hours, mins, secs, milisecs); VERIFY(1 <= month && month <= 12); LPCSTR month_str = StringTable().translate(st_months[month - 1]).c_str(); diff --git a/src/xrGame/ui/UIInventoryUtilities.h b/src/xrGame/ui/UIInventoryUtilities.h index aec3cc63893..b0a63a2bb1b 100644 --- a/src/xrGame/ui/UIInventoryUtilities.h +++ b/src/xrGame/ui/UIInventoryUtilities.h @@ -64,13 +64,12 @@ enum EDatePrecision edpDateToYear }; -const shared_str GetGameDateAsString(EDatePrecision datePrec, char dateSeparator = ','); -const shared_str GetGameTimeAsString(ETimePrecision timePrec, char timeSeparator = ':'); -const shared_str GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator = ','); -const shared_str GetTimeAsString( - ALife::_TIME_ID time, ETimePrecision timePrec, char timeSeparator = ':', bool full_mode = true); -const shared_str GetTimeAndDateAsString(ALife::_TIME_ID time); -const shared_str Get_GameTimeAndDate_AsString(); +shared_str GetGameDateAsString(EDatePrecision datePrec, char dateSeparator = ','); +shared_str GetGameTimeAsString(ETimePrecision timePrec, char timeSeparator = ':', bool full_mode = true); +shared_str GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator = ','); +shared_str GetTimeAsString(ALife::_TIME_ID time, ETimePrecision timePrec, char timeSeparator = ':', bool full_mode = true); +shared_str GetTimeAndDateAsString(ALife::_TIME_ID time); +shared_str Get_GameTimeAndDate_AsString(); LPCSTR GetTimePeriodAsString(pstr _buff, u32 buff_sz, ALife::_TIME_ID _from, ALife::_TIME_ID _to); // Отобразить вес, который несет (*pInvOwner) From e59bc1ff3d3e3fd8dc68f055062e1e9253405a5e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 21:57:35 +0300 Subject: [PATCH 113/261] xrGame/ui/UIInventoryUtilities.h|cpp: ability to get the month in numeric format --- src/xrGame/ui/UIInventoryUtilities.cpp | 29 ++++++++++++++++++++------ src/xrGame/ui/UIInventoryUtilities.h | 4 ++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/xrGame/ui/UIInventoryUtilities.cpp b/src/xrGame/ui/UIInventoryUtilities.cpp index 84c4ca3d6c6..6f7f31378cb 100644 --- a/src/xrGame/ui/UIInventoryUtilities.cpp +++ b/src/xrGame/ui/UIInventoryUtilities.cpp @@ -215,9 +215,9 @@ const ui_shader& InventoryUtilities::GetWeaponUpgradeIconsShader() ////////////////////////////////////////////////////////////////////////// -shared_str InventoryUtilities::GetGameDateAsString(EDatePrecision datePrec, char dateSeparator) +shared_str InventoryUtilities::GetGameDateAsString(EDatePrecision datePrec, char dateSeparator, bool numericMonth) { - return GetDateAsString(Level().GetGameTime(), datePrec, dateSeparator); + return GetDateAsString(Level().GetGameTime(), datePrec, dateSeparator, numericMonth); } ////////////////////////////////////////////////////////////////////////// @@ -296,7 +296,7 @@ shared_str InventoryUtilities::GetTimeAsString( return bufTime; } -shared_str InventoryUtilities::GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator) +shared_str InventoryUtilities::GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator, bool numericMonth) { string64 bufDate; @@ -311,9 +311,26 @@ shared_str InventoryUtilities::GetDateAsString(ALife::_TIME_ID time, EDatePrecis // Date switch (datePrec) { - case edpDateToYear: xr_sprintf(bufDate, "%04i", year); break; - case edpDateToMonth: xr_sprintf(bufDate, "%s%c% 04i", month_str, dateSeparator, year); break; - case edpDateToDay: xr_sprintf(bufDate, "%s %d%c %04i", month_str, day, dateSeparator, year); break; + case edpDateToYear: + xr_sprintf(bufDate, "%04i", year); + break; + case edpDateToMonth: + if (numericMonth) + { + xr_sprintf(bufDate, "%02i%c%04i", month, dateSeparator, year); + break; + } + xr_sprintf(bufDate, "%s%c% 04i", month_str, dateSeparator, year); + break; + case edpDateToDay: + if (numericMonth) + { + xr_sprintf(bufDate, "%02i%c%02i%c%04i", day, dateSeparator, month, dateSeparator, year); + break; + } + xr_sprintf(bufDate, "%s %d%c %04i", month_str, day, dateSeparator, year); + break; + default: R_ASSERT(!"Unknown type of date precision"); } diff --git a/src/xrGame/ui/UIInventoryUtilities.h b/src/xrGame/ui/UIInventoryUtilities.h index b0a63a2bb1b..780d0763eaf 100644 --- a/src/xrGame/ui/UIInventoryUtilities.h +++ b/src/xrGame/ui/UIInventoryUtilities.h @@ -64,9 +64,9 @@ enum EDatePrecision edpDateToYear }; -shared_str GetGameDateAsString(EDatePrecision datePrec, char dateSeparator = ','); +shared_str GetGameDateAsString(EDatePrecision datePrec, char dateSeparator = ',', bool numericMonth = false); shared_str GetGameTimeAsString(ETimePrecision timePrec, char timeSeparator = ':', bool full_mode = true); -shared_str GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator = ','); +shared_str GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator = ',', bool numericMonth = false); shared_str GetTimeAsString(ALife::_TIME_ID time, ETimePrecision timePrec, char timeSeparator = ':', bool full_mode = true); shared_str GetTimeAndDateAsString(ALife::_TIME_ID time); shared_str Get_GameTimeAndDate_AsString(); From 0de2686d42b67ea40791c6abc30e60e52042c40d Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 21:57:35 +0300 Subject: [PATCH 114/261] xrGame/ui/UIInventoryUtilities.h|cpp: ability to get the month in numeric format --- src/xrGame/ui/UIInventoryUtilities.cpp | 29 ++++++++++++++++++++------ src/xrGame/ui/UIInventoryUtilities.h | 4 ++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/xrGame/ui/UIInventoryUtilities.cpp b/src/xrGame/ui/UIInventoryUtilities.cpp index 84c4ca3d6c6..6f7f31378cb 100644 --- a/src/xrGame/ui/UIInventoryUtilities.cpp +++ b/src/xrGame/ui/UIInventoryUtilities.cpp @@ -215,9 +215,9 @@ const ui_shader& InventoryUtilities::GetWeaponUpgradeIconsShader() ////////////////////////////////////////////////////////////////////////// -shared_str InventoryUtilities::GetGameDateAsString(EDatePrecision datePrec, char dateSeparator) +shared_str InventoryUtilities::GetGameDateAsString(EDatePrecision datePrec, char dateSeparator, bool numericMonth) { - return GetDateAsString(Level().GetGameTime(), datePrec, dateSeparator); + return GetDateAsString(Level().GetGameTime(), datePrec, dateSeparator, numericMonth); } ////////////////////////////////////////////////////////////////////////// @@ -296,7 +296,7 @@ shared_str InventoryUtilities::GetTimeAsString( return bufTime; } -shared_str InventoryUtilities::GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator) +shared_str InventoryUtilities::GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator, bool numericMonth) { string64 bufDate; @@ -311,9 +311,26 @@ shared_str InventoryUtilities::GetDateAsString(ALife::_TIME_ID time, EDatePrecis // Date switch (datePrec) { - case edpDateToYear: xr_sprintf(bufDate, "%04i", year); break; - case edpDateToMonth: xr_sprintf(bufDate, "%s%c% 04i", month_str, dateSeparator, year); break; - case edpDateToDay: xr_sprintf(bufDate, "%s %d%c %04i", month_str, day, dateSeparator, year); break; + case edpDateToYear: + xr_sprintf(bufDate, "%04i", year); + break; + case edpDateToMonth: + if (numericMonth) + { + xr_sprintf(bufDate, "%02i%c%04i", month, dateSeparator, year); + break; + } + xr_sprintf(bufDate, "%s%c% 04i", month_str, dateSeparator, year); + break; + case edpDateToDay: + if (numericMonth) + { + xr_sprintf(bufDate, "%02i%c%02i%c%04i", day, dateSeparator, month, dateSeparator, year); + break; + } + xr_sprintf(bufDate, "%s %d%c %04i", month_str, day, dateSeparator, year); + break; + default: R_ASSERT(!"Unknown type of date precision"); } diff --git a/src/xrGame/ui/UIInventoryUtilities.h b/src/xrGame/ui/UIInventoryUtilities.h index b0a63a2bb1b..780d0763eaf 100644 --- a/src/xrGame/ui/UIInventoryUtilities.h +++ b/src/xrGame/ui/UIInventoryUtilities.h @@ -64,9 +64,9 @@ enum EDatePrecision edpDateToYear }; -shared_str GetGameDateAsString(EDatePrecision datePrec, char dateSeparator = ','); +shared_str GetGameDateAsString(EDatePrecision datePrec, char dateSeparator = ',', bool numericMonth = false); shared_str GetGameTimeAsString(ETimePrecision timePrec, char timeSeparator = ':', bool full_mode = true); -shared_str GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator = ','); +shared_str GetDateAsString(ALife::_TIME_ID time, EDatePrecision datePrec, char dateSeparator = ',', bool numericMonth = false); shared_str GetTimeAsString(ALife::_TIME_ID time, ETimePrecision timePrec, char timeSeparator = ':', bool full_mode = true); shared_str GetTimeAndDateAsString(ALife::_TIME_ID time); shared_str Get_GameTimeAndDate_AsString(); From 2c0e4f888b3c9144004cf2a4ed8727b8d57e78ea Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 22:04:28 +0300 Subject: [PATCH 115/261] xrGame/ui/UIPdaWnd.cpp: work on SOC PDA (#392) --- src/xrGame/ui/UIPdaWnd.cpp | 59 ++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/src/xrGame/ui/UIPdaWnd.cpp b/src/xrGame/ui/UIPdaWnd.cpp index 1f17e502dfd..d77309df24f 100644 --- a/src/xrGame/ui/UIPdaWnd.cpp +++ b/src/xrGame/ui/UIPdaWnd.cpp @@ -79,9 +79,25 @@ void CUIPdaWnd::Init() CUIXmlInit::InitWindow(uiXml, "main", 0, this); UIMainPdaFrame = UIHelper::CreateStatic(uiXml, "background_static", this); - m_caption = UIHelper::CreateStatic(uiXml, "caption_static", this); - m_caption_const = (m_caption->GetText()); + + CUIXmlInit::InitAutoStaticGroup(uiXml, "", 0, this); + + m_caption = UIHelper::CreateStatic(uiXml, "caption_static", this, false); + m_caption_const = m_caption ? m_caption->GetText() : ""; + + // Main buttons background + CUIWindow* buttons_background = this; + if (UIHelper::CreateFrameLine(uiXml, "mbbackground_frame_line", UIMainPdaFrame, false)) + buttons_background = UIMainPdaFrame; // SOC + + // Timer background m_clock = UIHelper::CreateStatic(uiXml, "clock_wnd", this, false); + if (!m_clock) + { + const auto timer = UIHelper::CreateFrameLine(uiXml, "timer_frame_line", UIMainPdaFrame, false); + if (timer && timer->GetTitleText()) + m_clock = timer->GetTitleText(); // SOC + } if (uiXml.NavigateToNode("anim_static")) // XXX: Replace with UIHelper { @@ -91,11 +107,14 @@ void CUIPdaWnd::Init() CUIXmlInit::InitAnimatedStatic(uiXml, "anim_static", 0, anim_static); } - m_btn_close = UIHelper::Create3tButton(uiXml, "close_button", this); - m_btn_close->SetAccelerator(kUI_BACK, false, 2); - UI().Focus().UnregisterFocusable(m_btn_close); + m_btn_close = UIHelper::Create3tButton(uiXml, "close_button", this, false); + if (m_btn_close) + { + m_btn_close->SetAccelerator(kUI_BACK, false, 2); + UI().Focus().UnregisterFocusable(m_btn_close); + } - m_hint_wnd = UIHelper::CreateHint(uiXml, "hint_wnd"); + m_hint_wnd = UIHelper::CreateHint(uiXml, "hint_wnd", false); if (IsGameTypeSingle()) { @@ -126,7 +145,7 @@ void CUIPdaWnd::Init() UITabControl = xr_new(); UITabControl->SetAutoDelete(true); - AttachChild(UITabControl); + buttons_background->AttachChild(UITabControl); CUIXmlInit::InitTabControl(uiXml, "tab", 0, UITabControl, true, ShadowOfChernobylMode); UITabControl->SetMessageTarget(this); UITabControl->SetAcceleratorsMode(true); @@ -183,7 +202,7 @@ void CUIPdaWnd::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) } case BUTTON_CLICKED: { - if (pWnd == m_btn_close) + if (m_btn_close && pWnd == m_btn_close) { HideDialog(); } @@ -235,8 +254,15 @@ void CUIPdaWnd::Update() m_pActiveDialog->Update(); if (m_clock) - m_clock->SetText(GetGameTimeAsString(InventoryUtilities::etpTimeToMinutes).c_str()); - + { + auto time = GetGameTimeAsString(InventoryUtilities::etpTimeToMinutes); + if (m_clock->GetParent() != this) // SOC + { + const auto date = GetGameDateAsString(InventoryUtilities::edpDateToDay, '/', true); + time.printf("%s %s", time.c_str(), date.c_str()); + } + m_clock->SetText(time.c_str()); + } if (pUILogsWnd) Device.seqParallel.push_back(fastdelegate::FastDelegate0<>(pUILogsWnd, &CUILogsWnd::PerformWork)); } @@ -305,6 +331,9 @@ void CUIPdaWnd::SetActiveSubdialog(const shared_str& section) void CUIPdaWnd::SetActiveCaption() { + if (!m_caption) + return; + TABS_VECTOR* btn_vec = UITabControl->GetButtonsVector(); TABS_VECTOR::iterator it_b = btn_vec->begin(); TABS_VECTOR::iterator it_e = btn_vec->end(); @@ -369,7 +398,8 @@ void CUIPdaWnd::DrawHint() else if (m_pActiveDialog == pUIRankingWnd && pUIRankingWnd) pUIRankingWnd->DrawHint(); - m_hint_wnd->Draw(); + if (m_hint_wnd) + m_hint_wnd->Draw(); } bool CUIPdaWnd::NeedCursor() const @@ -415,7 +445,12 @@ void CUIPdaWnd::Reset() pUILogsWnd->ResetAll(); } -void CUIPdaWnd::SetCaption(pcstr text) { m_caption->SetText(text); } +void CUIPdaWnd::SetCaption(pcstr text) +{ + if (m_caption) + m_caption->SetText(text); +} + void RearrangeTabButtons(CUITabControl* pTab) { const auto& buttons = *pTab->GetButtonsVector(); From f2863591ce35abc2bd0401e94c4aa696ae572d9f Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 22:04:28 +0300 Subject: [PATCH 116/261] xrGame/ui/UIPdaWnd.cpp: work on SOC PDA (#392) --- src/xrGame/ui/UIPdaWnd.cpp | 59 ++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/src/xrGame/ui/UIPdaWnd.cpp b/src/xrGame/ui/UIPdaWnd.cpp index 1f17e502dfd..d77309df24f 100644 --- a/src/xrGame/ui/UIPdaWnd.cpp +++ b/src/xrGame/ui/UIPdaWnd.cpp @@ -79,9 +79,25 @@ void CUIPdaWnd::Init() CUIXmlInit::InitWindow(uiXml, "main", 0, this); UIMainPdaFrame = UIHelper::CreateStatic(uiXml, "background_static", this); - m_caption = UIHelper::CreateStatic(uiXml, "caption_static", this); - m_caption_const = (m_caption->GetText()); + + CUIXmlInit::InitAutoStaticGroup(uiXml, "", 0, this); + + m_caption = UIHelper::CreateStatic(uiXml, "caption_static", this, false); + m_caption_const = m_caption ? m_caption->GetText() : ""; + + // Main buttons background + CUIWindow* buttons_background = this; + if (UIHelper::CreateFrameLine(uiXml, "mbbackground_frame_line", UIMainPdaFrame, false)) + buttons_background = UIMainPdaFrame; // SOC + + // Timer background m_clock = UIHelper::CreateStatic(uiXml, "clock_wnd", this, false); + if (!m_clock) + { + const auto timer = UIHelper::CreateFrameLine(uiXml, "timer_frame_line", UIMainPdaFrame, false); + if (timer && timer->GetTitleText()) + m_clock = timer->GetTitleText(); // SOC + } if (uiXml.NavigateToNode("anim_static")) // XXX: Replace with UIHelper { @@ -91,11 +107,14 @@ void CUIPdaWnd::Init() CUIXmlInit::InitAnimatedStatic(uiXml, "anim_static", 0, anim_static); } - m_btn_close = UIHelper::Create3tButton(uiXml, "close_button", this); - m_btn_close->SetAccelerator(kUI_BACK, false, 2); - UI().Focus().UnregisterFocusable(m_btn_close); + m_btn_close = UIHelper::Create3tButton(uiXml, "close_button", this, false); + if (m_btn_close) + { + m_btn_close->SetAccelerator(kUI_BACK, false, 2); + UI().Focus().UnregisterFocusable(m_btn_close); + } - m_hint_wnd = UIHelper::CreateHint(uiXml, "hint_wnd"); + m_hint_wnd = UIHelper::CreateHint(uiXml, "hint_wnd", false); if (IsGameTypeSingle()) { @@ -126,7 +145,7 @@ void CUIPdaWnd::Init() UITabControl = xr_new(); UITabControl->SetAutoDelete(true); - AttachChild(UITabControl); + buttons_background->AttachChild(UITabControl); CUIXmlInit::InitTabControl(uiXml, "tab", 0, UITabControl, true, ShadowOfChernobylMode); UITabControl->SetMessageTarget(this); UITabControl->SetAcceleratorsMode(true); @@ -183,7 +202,7 @@ void CUIPdaWnd::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) } case BUTTON_CLICKED: { - if (pWnd == m_btn_close) + if (m_btn_close && pWnd == m_btn_close) { HideDialog(); } @@ -235,8 +254,15 @@ void CUIPdaWnd::Update() m_pActiveDialog->Update(); if (m_clock) - m_clock->SetText(GetGameTimeAsString(InventoryUtilities::etpTimeToMinutes).c_str()); - + { + auto time = GetGameTimeAsString(InventoryUtilities::etpTimeToMinutes); + if (m_clock->GetParent() != this) // SOC + { + const auto date = GetGameDateAsString(InventoryUtilities::edpDateToDay, '/', true); + time.printf("%s %s", time.c_str(), date.c_str()); + } + m_clock->SetText(time.c_str()); + } if (pUILogsWnd) Device.seqParallel.push_back(fastdelegate::FastDelegate0<>(pUILogsWnd, &CUILogsWnd::PerformWork)); } @@ -305,6 +331,9 @@ void CUIPdaWnd::SetActiveSubdialog(const shared_str& section) void CUIPdaWnd::SetActiveCaption() { + if (!m_caption) + return; + TABS_VECTOR* btn_vec = UITabControl->GetButtonsVector(); TABS_VECTOR::iterator it_b = btn_vec->begin(); TABS_VECTOR::iterator it_e = btn_vec->end(); @@ -369,7 +398,8 @@ void CUIPdaWnd::DrawHint() else if (m_pActiveDialog == pUIRankingWnd && pUIRankingWnd) pUIRankingWnd->DrawHint(); - m_hint_wnd->Draw(); + if (m_hint_wnd) + m_hint_wnd->Draw(); } bool CUIPdaWnd::NeedCursor() const @@ -415,7 +445,12 @@ void CUIPdaWnd::Reset() pUILogsWnd->ResetAll(); } -void CUIPdaWnd::SetCaption(pcstr text) { m_caption->SetText(text); } +void CUIPdaWnd::SetCaption(pcstr text) +{ + if (m_caption) + m_caption->SetText(text); +} + void RearrangeTabButtons(CUITabControl* pTab) { const auto& buttons = *pTab->GetButtonsVector(); From 1a2550d0c2a180d6e973f07b755aa6725739146e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 22:40:45 +0300 Subject: [PATCH 117/261] Proper talk dialog look in SOC (#392) Fixed crash in CS/COP introduced in bc4ae7055b98cd5285426d49ed82fb237b0253db --- src/xrGame/ui/UIHelper.cpp | 9 +++- src/xrGame/ui/UIHelper.h | 1 + src/xrGame/ui/UITalkDialogWnd.cpp | 70 +++++++++++++++---------------- src/xrGame/ui/UITalkDialogWnd.h | 12 ++---- src/xrGame/ui/UITalkWnd.cpp | 6 +-- 5 files changed, 48 insertions(+), 50 deletions(-) diff --git a/src/xrGame/ui/UIHelper.cpp b/src/xrGame/ui/UIHelper.cpp index e82a52c032d..a6524f67e03 100644 --- a/src/xrGame/ui/UIHelper.cpp +++ b/src/xrGame/ui/UIHelper.cpp @@ -141,13 +141,18 @@ CUIProgressShape* UIHelper::CreateProgressShape(CUIXml& xml, LPCSTR ui_path, CUI } CUIFrameLineWnd* UIHelper::CreateFrameLine(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical) +{ + return CreateFrameLine(xml, ui_path, 0, parent, critical); +} + +CUIFrameLineWnd* UIHelper::CreateFrameLine(CUIXml& xml, LPCSTR ui_path, int index, CUIWindow* parent, bool critical) { // If it's not critical element, then don't crash if it doesn't exist - if (!critical && !xml.NavigateToNode(ui_path, 0)) + if (!critical && !xml.NavigateToNode(ui_path, index)) return nullptr; auto ui = xr_new(ui_path); - if (!CUIXmlInit::InitFrameLine(xml, ui_path, 0, ui, critical) && !critical) + if (!CUIXmlInit::InitFrameLine(xml, ui_path, index, ui, critical) && !critical) { xr_delete(ui); } diff --git a/src/xrGame/ui/UIHelper.h b/src/xrGame/ui/UIHelper.h index 2dd29e6522f..426e68ccc98 100644 --- a/src/xrGame/ui/UIHelper.h +++ b/src/xrGame/ui/UIHelper.h @@ -36,6 +36,7 @@ class UIHelper static CUIProgressBar* CreateProgressBar(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUIProgressShape* CreateProgressShape(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUIFrameLineWnd* CreateFrameLine(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); + static CUIFrameLineWnd* CreateFrameLine(CUIXml& xml, LPCSTR ui_path, int index, CUIWindow* parent, bool critical = true); static CUIFrameWindow* CreateFrameWindow(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUI3tButton* Create3tButton(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUI3tButton* Create3tButton(CUIXml& xml, LPCSTR ui_path, int index, CUIWindow* parent, bool critical = true); diff --git a/src/xrGame/ui/UITalkDialogWnd.cpp b/src/xrGame/ui/UITalkDialogWnd.cpp index cc70384591c..aa510849c2d 100644 --- a/src/xrGame/ui/UITalkDialogWnd.cpp +++ b/src/xrGame/ui/UITalkDialogWnd.cpp @@ -21,8 +21,6 @@ CUITalkDialogWnd::CUITalkDialogWnd() m_pParent(nullptr), mechanic_mode(false), m_ClickedQuestionID(""), - UIDialogFrameTop(nullptr), - UIDialogFrameBottom(nullptr), m_btn_pos(), UIToExitButton(nullptr), UIOurIcon(nullptr), @@ -47,8 +45,8 @@ void CUITalkDialogWnd::InitTalkDialogWnd() if (!CUIXmlInit::InitWindow(*m_uiXml, "main", 0, this, false)) SetWndRect({ 0, 0, UI_BASE_WIDTH, UI_BASE_HEIGHT }); - UIStaticTop = UIHelper::CreateStatic(*m_uiXml, "top_background", this, false); - UIStaticBottom = UIHelper::CreateStatic(*m_uiXml, "bottom_background", this, false); + std::ignore = UIHelper::CreateStatic(*m_uiXml, "top_background", this, false); + std::ignore = UIHelper::CreateStatic(*m_uiXml, "bottom_background", this, false); pcstr ourTag = "right_character_icon"; pcstr othersTag = "left_character_icon"; @@ -71,43 +69,26 @@ void CUITalkDialogWnd::InitTalkDialogWnd() TALK_CHARACTER_XML, TRADE_CHARACTER_XML); } - CUIWindow* answersParent = this; - CUIWindow* questionsParent = this; - - // Our phrases frame + // CS/COP order: + // 1. Our phrases frame + // 2. Main dialog frame UIDialogFrameBottom = UIHelper::CreateStatic(*m_uiXml, "frame_bottom", this, false); - if (UIDialogFrameBottom) - questionsParent = UIDialogFrameBottom; - else - { - if (m_uiXml->NavigateToNode("frame_line_window", 1)) - { - // XXX: Don't replace this with UI helper, until it's missing needed functionality to select the index - UIOurPhrasesFrame = xr_new("Our phrases frame"); - UIOurPhrasesFrame->SetAutoDelete(true); - AttachChild(UIOurPhrasesFrame); - CUIXmlInitBase::InitFrameLine(*m_uiXml, "frame_line_window", 1, UIOurPhrasesFrame); // index for field is 1 (one) !!! - questionsParent = UIOurPhrasesFrame; - } - } - - // Main dialog frame UIDialogFrameTop = UIHelper::CreateStatic(*m_uiXml, "frame_top", this, false); - if (UIDialogFrameTop) - answersParent = UIDialogFrameTop; - else - { - UIDialogFrame = UIHelper::CreateFrameLine(*m_uiXml, "frame_line_window", this, false); - if (UIDialogFrame) - answersParent = UIDialogFrame; - } + + // SOC order: + // 1. Main dialog frame + // 2. Our phrases frame + if (!UIDialogFrameTop) + UIDialogFrameTop = UIHelper::CreateFrameLine(*m_uiXml, "frame_line_window", 0, this, false); + if (!UIDialogFrameBottom) + UIDialogFrameBottom = UIHelper::CreateFrameLine(*m_uiXml, "frame_line_window", 1, this, false); // Answers - UIAnswersList = UIHelper::CreateScrollView(*m_uiXml, "answers_list", answersParent); + UIAnswersList = UIHelper::CreateScrollView(*m_uiXml, "answers_list", UIDialogFrameTop ? UIDialogFrameTop : this); UIAnswersList->SetWindowName("---UIAnswersList"); // Questions - UIQuestionsList = UIHelper::CreateScrollView(*m_uiXml, "questions_list", questionsParent); + UIQuestionsList = UIHelper::CreateScrollView(*m_uiXml, "questions_list", UIDialogFrameBottom ? UIDialogFrameBottom : this); UIQuestionsList->SetWindowName("---UIQuestionsList"); //кнопка перехода в режим торговли @@ -204,6 +185,25 @@ void CUITalkDialogWnd::ClearAll() } void CUITalkDialogWnd::ClearQuestions() { UIQuestionsList->Clear(); } + +void CUITalkDialogWnd::SetOurName(pcstr name) +{ + const auto frameline = smart_cast(UIDialogFrameBottom); + if (!frameline) + return; + if (const auto title = frameline->GetTitleText()) + title->SetText(name); +} + +void CUITalkDialogWnd::SetOthersName(pcstr name) +{ + const auto frameline = smart_cast(UIDialogFrameTop); + if (!frameline) + return; + if (const auto title = frameline->GetTitleText()) + title->SetText(name); +} + void CUITalkDialogWnd::AddQuestion(LPCSTR str, LPCSTR value, int number, bool b_finalizer) { CUIQuestionItem* itm = xr_new(m_uiXml, "question_item"); @@ -301,8 +301,6 @@ void CUITalkDialogWnd::SetOsoznanieMode(bool b) if (UIDialogFrameTop) UIDialogFrameTop->Show(!b); - else if (UIDialogFrame) - UIDialogFrame->Show(!b); UIToTradeButton.Show(!b); if (mechanic_mode) diff --git a/src/xrGame/ui/UITalkDialogWnd.h b/src/xrGame/ui/UITalkDialogWnd.h index ace31438e26..fd9b4e42520 100644 --- a/src/xrGame/ui/UITalkDialogWnd.h +++ b/src/xrGame/ui/UITalkDialogWnd.h @@ -38,14 +38,8 @@ class CUITalkDialogWnd final : public CUIWindow, public CUIWndCallback shared_str m_ClickedQuestionID; // Dialog interface elements - CUIStatic* UIStaticTop; - CUIStatic* UIStaticBottom; - - CUIFrameLineWnd* UIDialogFrame; - CUIFrameLineWnd* UIOurPhrasesFrame; - - CUIStatic* UIDialogFrameTop; - CUIStatic* UIDialogFrameBottom; + CUIWindow* UIDialogFrameTop{}; + CUIWindow* UIDialogFrameBottom{}; Fvector2 m_btn_pos[3]; CUI3tButton UIToTradeButton; @@ -64,6 +58,8 @@ class CUITalkDialogWnd final : public CUIWindow, public CUIWndCallback void ClearAll(); void ClearQuestions(); + void SetOurName(pcstr name); + void SetOthersName(pcstr name); void SetOsoznanieMode(bool b); void SetTradeMode(); void UpdateButtonsLayout(bool b_disable_break, bool trade_enabled); diff --git a/src/xrGame/ui/UITalkWnd.cpp b/src/xrGame/ui/UITalkWnd.cpp index c588241f25b..206f8e2daef 100644 --- a/src/xrGame/ui/UITalkWnd.cpp +++ b/src/xrGame/ui/UITalkWnd.cpp @@ -59,10 +59,8 @@ void CUITalkWnd::InitTalkDialog() UITalkDialogWnd->UICharacterInfoLeft.InitCharacter(m_pOurInvOwner->object_id()); UITalkDialogWnd->UICharacterInfoRight.InitCharacter(m_pOthersInvOwner->object_id()); - if (const auto title = UITalkDialogWnd->UIDialogFrame->GetTitleText()) - title->SetText(m_pOthersInvOwner->Name()); - if (const auto title = UITalkDialogWnd->UIOurPhrasesFrame->GetTitleText()) - title->SetText(m_pOurInvOwner->Name()); + UITalkDialogWnd->SetOurName(m_pOurInvOwner->Name()); + UITalkDialogWnd->SetOthersName(m_pOthersInvOwner->Name()); //очистить лог сообщений UITalkDialogWnd->ClearAll(); From c0c11ab8007e9a7a53a0d5ea6b6a41df86063787 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 22:40:45 +0300 Subject: [PATCH 118/261] Proper talk dialog look in SOC (#392) Fixed crash in CS/COP introduced in bc4ae7055b98cd5285426d49ed82fb237b0253db --- src/xrGame/ui/UIHelper.cpp | 9 +++- src/xrGame/ui/UIHelper.h | 1 + src/xrGame/ui/UITalkDialogWnd.cpp | 70 +++++++++++++++---------------- src/xrGame/ui/UITalkDialogWnd.h | 12 ++---- src/xrGame/ui/UITalkWnd.cpp | 6 +-- 5 files changed, 48 insertions(+), 50 deletions(-) diff --git a/src/xrGame/ui/UIHelper.cpp b/src/xrGame/ui/UIHelper.cpp index e82a52c032d..a6524f67e03 100644 --- a/src/xrGame/ui/UIHelper.cpp +++ b/src/xrGame/ui/UIHelper.cpp @@ -141,13 +141,18 @@ CUIProgressShape* UIHelper::CreateProgressShape(CUIXml& xml, LPCSTR ui_path, CUI } CUIFrameLineWnd* UIHelper::CreateFrameLine(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical) +{ + return CreateFrameLine(xml, ui_path, 0, parent, critical); +} + +CUIFrameLineWnd* UIHelper::CreateFrameLine(CUIXml& xml, LPCSTR ui_path, int index, CUIWindow* parent, bool critical) { // If it's not critical element, then don't crash if it doesn't exist - if (!critical && !xml.NavigateToNode(ui_path, 0)) + if (!critical && !xml.NavigateToNode(ui_path, index)) return nullptr; auto ui = xr_new(ui_path); - if (!CUIXmlInit::InitFrameLine(xml, ui_path, 0, ui, critical) && !critical) + if (!CUIXmlInit::InitFrameLine(xml, ui_path, index, ui, critical) && !critical) { xr_delete(ui); } diff --git a/src/xrGame/ui/UIHelper.h b/src/xrGame/ui/UIHelper.h index 2dd29e6522f..426e68ccc98 100644 --- a/src/xrGame/ui/UIHelper.h +++ b/src/xrGame/ui/UIHelper.h @@ -36,6 +36,7 @@ class UIHelper static CUIProgressBar* CreateProgressBar(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUIProgressShape* CreateProgressShape(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUIFrameLineWnd* CreateFrameLine(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); + static CUIFrameLineWnd* CreateFrameLine(CUIXml& xml, LPCSTR ui_path, int index, CUIWindow* parent, bool critical = true); static CUIFrameWindow* CreateFrameWindow(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUI3tButton* Create3tButton(CUIXml& xml, LPCSTR ui_path, CUIWindow* parent, bool critical = true); static CUI3tButton* Create3tButton(CUIXml& xml, LPCSTR ui_path, int index, CUIWindow* parent, bool critical = true); diff --git a/src/xrGame/ui/UITalkDialogWnd.cpp b/src/xrGame/ui/UITalkDialogWnd.cpp index cc70384591c..aa510849c2d 100644 --- a/src/xrGame/ui/UITalkDialogWnd.cpp +++ b/src/xrGame/ui/UITalkDialogWnd.cpp @@ -21,8 +21,6 @@ CUITalkDialogWnd::CUITalkDialogWnd() m_pParent(nullptr), mechanic_mode(false), m_ClickedQuestionID(""), - UIDialogFrameTop(nullptr), - UIDialogFrameBottom(nullptr), m_btn_pos(), UIToExitButton(nullptr), UIOurIcon(nullptr), @@ -47,8 +45,8 @@ void CUITalkDialogWnd::InitTalkDialogWnd() if (!CUIXmlInit::InitWindow(*m_uiXml, "main", 0, this, false)) SetWndRect({ 0, 0, UI_BASE_WIDTH, UI_BASE_HEIGHT }); - UIStaticTop = UIHelper::CreateStatic(*m_uiXml, "top_background", this, false); - UIStaticBottom = UIHelper::CreateStatic(*m_uiXml, "bottom_background", this, false); + std::ignore = UIHelper::CreateStatic(*m_uiXml, "top_background", this, false); + std::ignore = UIHelper::CreateStatic(*m_uiXml, "bottom_background", this, false); pcstr ourTag = "right_character_icon"; pcstr othersTag = "left_character_icon"; @@ -71,43 +69,26 @@ void CUITalkDialogWnd::InitTalkDialogWnd() TALK_CHARACTER_XML, TRADE_CHARACTER_XML); } - CUIWindow* answersParent = this; - CUIWindow* questionsParent = this; - - // Our phrases frame + // CS/COP order: + // 1. Our phrases frame + // 2. Main dialog frame UIDialogFrameBottom = UIHelper::CreateStatic(*m_uiXml, "frame_bottom", this, false); - if (UIDialogFrameBottom) - questionsParent = UIDialogFrameBottom; - else - { - if (m_uiXml->NavigateToNode("frame_line_window", 1)) - { - // XXX: Don't replace this with UI helper, until it's missing needed functionality to select the index - UIOurPhrasesFrame = xr_new("Our phrases frame"); - UIOurPhrasesFrame->SetAutoDelete(true); - AttachChild(UIOurPhrasesFrame); - CUIXmlInitBase::InitFrameLine(*m_uiXml, "frame_line_window", 1, UIOurPhrasesFrame); // index for field is 1 (one) !!! - questionsParent = UIOurPhrasesFrame; - } - } - - // Main dialog frame UIDialogFrameTop = UIHelper::CreateStatic(*m_uiXml, "frame_top", this, false); - if (UIDialogFrameTop) - answersParent = UIDialogFrameTop; - else - { - UIDialogFrame = UIHelper::CreateFrameLine(*m_uiXml, "frame_line_window", this, false); - if (UIDialogFrame) - answersParent = UIDialogFrame; - } + + // SOC order: + // 1. Main dialog frame + // 2. Our phrases frame + if (!UIDialogFrameTop) + UIDialogFrameTop = UIHelper::CreateFrameLine(*m_uiXml, "frame_line_window", 0, this, false); + if (!UIDialogFrameBottom) + UIDialogFrameBottom = UIHelper::CreateFrameLine(*m_uiXml, "frame_line_window", 1, this, false); // Answers - UIAnswersList = UIHelper::CreateScrollView(*m_uiXml, "answers_list", answersParent); + UIAnswersList = UIHelper::CreateScrollView(*m_uiXml, "answers_list", UIDialogFrameTop ? UIDialogFrameTop : this); UIAnswersList->SetWindowName("---UIAnswersList"); // Questions - UIQuestionsList = UIHelper::CreateScrollView(*m_uiXml, "questions_list", questionsParent); + UIQuestionsList = UIHelper::CreateScrollView(*m_uiXml, "questions_list", UIDialogFrameBottom ? UIDialogFrameBottom : this); UIQuestionsList->SetWindowName("---UIQuestionsList"); //кнопка перехода в режим торговли @@ -204,6 +185,25 @@ void CUITalkDialogWnd::ClearAll() } void CUITalkDialogWnd::ClearQuestions() { UIQuestionsList->Clear(); } + +void CUITalkDialogWnd::SetOurName(pcstr name) +{ + const auto frameline = smart_cast(UIDialogFrameBottom); + if (!frameline) + return; + if (const auto title = frameline->GetTitleText()) + title->SetText(name); +} + +void CUITalkDialogWnd::SetOthersName(pcstr name) +{ + const auto frameline = smart_cast(UIDialogFrameTop); + if (!frameline) + return; + if (const auto title = frameline->GetTitleText()) + title->SetText(name); +} + void CUITalkDialogWnd::AddQuestion(LPCSTR str, LPCSTR value, int number, bool b_finalizer) { CUIQuestionItem* itm = xr_new(m_uiXml, "question_item"); @@ -301,8 +301,6 @@ void CUITalkDialogWnd::SetOsoznanieMode(bool b) if (UIDialogFrameTop) UIDialogFrameTop->Show(!b); - else if (UIDialogFrame) - UIDialogFrame->Show(!b); UIToTradeButton.Show(!b); if (mechanic_mode) diff --git a/src/xrGame/ui/UITalkDialogWnd.h b/src/xrGame/ui/UITalkDialogWnd.h index ace31438e26..fd9b4e42520 100644 --- a/src/xrGame/ui/UITalkDialogWnd.h +++ b/src/xrGame/ui/UITalkDialogWnd.h @@ -38,14 +38,8 @@ class CUITalkDialogWnd final : public CUIWindow, public CUIWndCallback shared_str m_ClickedQuestionID; // Dialog interface elements - CUIStatic* UIStaticTop; - CUIStatic* UIStaticBottom; - - CUIFrameLineWnd* UIDialogFrame; - CUIFrameLineWnd* UIOurPhrasesFrame; - - CUIStatic* UIDialogFrameTop; - CUIStatic* UIDialogFrameBottom; + CUIWindow* UIDialogFrameTop{}; + CUIWindow* UIDialogFrameBottom{}; Fvector2 m_btn_pos[3]; CUI3tButton UIToTradeButton; @@ -64,6 +58,8 @@ class CUITalkDialogWnd final : public CUIWindow, public CUIWndCallback void ClearAll(); void ClearQuestions(); + void SetOurName(pcstr name); + void SetOthersName(pcstr name); void SetOsoznanieMode(bool b); void SetTradeMode(); void UpdateButtonsLayout(bool b_disable_break, bool trade_enabled); diff --git a/src/xrGame/ui/UITalkWnd.cpp b/src/xrGame/ui/UITalkWnd.cpp index c588241f25b..206f8e2daef 100644 --- a/src/xrGame/ui/UITalkWnd.cpp +++ b/src/xrGame/ui/UITalkWnd.cpp @@ -59,10 +59,8 @@ void CUITalkWnd::InitTalkDialog() UITalkDialogWnd->UICharacterInfoLeft.InitCharacter(m_pOurInvOwner->object_id()); UITalkDialogWnd->UICharacterInfoRight.InitCharacter(m_pOthersInvOwner->object_id()); - if (const auto title = UITalkDialogWnd->UIDialogFrame->GetTitleText()) - title->SetText(m_pOthersInvOwner->Name()); - if (const auto title = UITalkDialogWnd->UIOurPhrasesFrame->GetTitleText()) - title->SetText(m_pOurInvOwner->Name()); + UITalkDialogWnd->SetOurName(m_pOurInvOwner->Name()); + UITalkDialogWnd->SetOthersName(m_pOthersInvOwner->Name()); //очистить лог сообщений UITalkDialogWnd->ClearAll(); From 345870fa5a36e4cc0bda7c41d2e9f6a13bd698c1 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 23:53:30 +0300 Subject: [PATCH 119/261] xrRender/SkeletonAnimated.cpp: make assertion less annoying --- src/Layers/xrRender/SkeletonAnimated.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Layers/xrRender/SkeletonAnimated.cpp b/src/Layers/xrRender/SkeletonAnimated.cpp index 990799cb0ff..84ae9343262 100644 --- a/src/Layers/xrRender/SkeletonAnimated.cpp +++ b/src/Layers/xrRender/SkeletonAnimated.cpp @@ -406,13 +406,8 @@ CBlend* CKinematicsAnimated::PlayCycle( LPCSTR N, BOOL bMixIn, PlayCallback Callback, LPVOID CallbackParam, u8 channel /*= 0*/) { MotionID motion_ID = ID_Cycle(N); - if (motion_ID.valid()) - return PlayCycle(motion_ID, bMixIn, Callback, CallbackParam, channel); - else - { - xrDebug::Fatal(DEBUG_INFO, "! MODEL: can't find cycle: %s", N); - return nullptr; - } + R_ASSERT3_CURE(motion_ID.valid(), "! MODEL: can't find cycle:", N, { return nullptr; }); + return PlayCycle(motion_ID, bMixIn, Callback, CallbackParam, channel); } CBlend* CKinematicsAnimated::PlayCycle( MotionID motion_ID, BOOL bMixIn, PlayCallback Callback, LPVOID CallbackParam, u8 channel /*= 0*/) From 48b3c22d49cb5c12bec435e8c3a8339b90ab7ac6 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 25 Dec 2025 23:53:30 +0300 Subject: [PATCH 120/261] xrRender/SkeletonAnimated.cpp: make assertion less annoying --- src/Layers/xrRender/SkeletonAnimated.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Layers/xrRender/SkeletonAnimated.cpp b/src/Layers/xrRender/SkeletonAnimated.cpp index 990799cb0ff..84ae9343262 100644 --- a/src/Layers/xrRender/SkeletonAnimated.cpp +++ b/src/Layers/xrRender/SkeletonAnimated.cpp @@ -406,13 +406,8 @@ CBlend* CKinematicsAnimated::PlayCycle( LPCSTR N, BOOL bMixIn, PlayCallback Callback, LPVOID CallbackParam, u8 channel /*= 0*/) { MotionID motion_ID = ID_Cycle(N); - if (motion_ID.valid()) - return PlayCycle(motion_ID, bMixIn, Callback, CallbackParam, channel); - else - { - xrDebug::Fatal(DEBUG_INFO, "! MODEL: can't find cycle: %s", N); - return nullptr; - } + R_ASSERT3_CURE(motion_ID.valid(), "! MODEL: can't find cycle:", N, { return nullptr; }); + return PlayCycle(motion_ID, bMixIn, Callback, CallbackParam, channel); } CBlend* CKinematicsAnimated::PlayCycle( MotionID motion_ID, BOOL bMixIn, PlayCallback Callback, LPVOID CallbackParam, u8 channel /*= 0*/) From 9959e39174558c7e61ba0edd6ee443a99e06689e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Fri, 26 Dec 2025 03:15:31 +0300 Subject: [PATCH 121/261] Ability to debug CUIProgressBar and CUIDoubleProgressBar --- .../ProgressBar/UIDoubleProgressBar.cpp | 19 ++++- .../ProgressBar/UIDoubleProgressBar.h | 2 +- src/xrUICore/ProgressBar/UIProgressBar.cpp | 85 ++++++++++++++++--- src/xrUICore/ProgressBar/UIProgressBar.h | 29 ++++--- src/xrUICore/XML/UIXmlInitBase.cpp | 2 +- 5 files changed, 107 insertions(+), 30 deletions(-) diff --git a/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp b/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp index 9223036260c..594c4589eab 100644 --- a/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp +++ b/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp @@ -8,7 +8,6 @@ CUIDoubleProgressBar::CUIDoubleProgressBar() : CUIWindow("CUIDoubleProgressBar") AttachChild(&m_progress_two); } -CUIDoubleProgressBar::~CUIDoubleProgressBar() {} void CUIDoubleProgressBar::InitFromXml(CUIXml& xml_doc, LPCSTR path) { CUIXmlInitBase::InitProgressBar(xml_doc, path, 0, &m_progress_one); @@ -48,3 +47,21 @@ void CUIDoubleProgressBar::SetTwoPos(float cur_value, float compare_value) m_progress_two.SetProgressPos(cur_value); } } + +void CUIDoubleProgressBar::FillDebugInfo() +{ +#ifndef MASTER_GOLD + CUIWindow::FillDebugInfo(); + + if (!ImGui::CollapsingHeader(CUIDoubleProgressBar::GetDebugType())) + return; + + Fcolor color = m_less_color; + if (ImGui::ColorEdit4("Less color", reinterpret_cast(&color))) + m_less_color = color.get(); + + color = m_more_color; + if (ImGui::ColorEdit4("More color", reinterpret_cast(&color))) + m_less_color = color.get(); +#endif +} diff --git a/src/xrUICore/ProgressBar/UIDoubleProgressBar.h b/src/xrUICore/ProgressBar/UIDoubleProgressBar.h index a52de3c63f8..5817038be15 100644 --- a/src/xrUICore/ProgressBar/UIDoubleProgressBar.h +++ b/src/xrUICore/ProgressBar/UIDoubleProgressBar.h @@ -6,12 +6,12 @@ class XRUICORE_API CUIDoubleProgressBar final : public CUIWindow { public: // func CUIDoubleProgressBar(); - virtual ~CUIDoubleProgressBar(); void InitFromXml(CUIXml& xml_doc, LPCSTR path); void SetTwoPos(float cur_value, float compare_value); pcstr GetDebugType() override { return "CUIDoubleProgressBar"; } + void FillDebugInfo() override; protected: CUIProgressBar m_progress_one; diff --git a/src/xrUICore/ProgressBar/UIProgressBar.cpp b/src/xrUICore/ProgressBar/UIProgressBar.cpp index 60bf32da033..3e55849abed 100644 --- a/src/xrUICore/ProgressBar/UIProgressBar.cpp +++ b/src/xrUICore/ProgressBar/UIProgressBar.cpp @@ -4,22 +4,10 @@ CUIProgressBar::CUIProgressBar() : CUIWindow("CUIProgressBar"), m_UIProgressItem("Progress"), m_UIBackgroundItem("Background") { - m_MinPos = 1.0f; - m_MaxPos = 1.0f + EPS; - Enable(false); - m_bBackgroundPresent = false; - m_bUseColor = false; - m_bUseMiddleColor = false; - m_bUseGradient = true; - AttachChild(&m_UIBackgroundItem); AttachChild(&m_UIProgressItem); - m_ProgressPos.x = 0.0f; - m_ProgressPos.y = 0.0f; - m_inertion = 0.0f; - m_orient_mode = om_horz; } void CUIProgressBar::InitProgressBar(Fvector2 pos, Fvector2 size, EOrientMode mode) @@ -110,7 +98,7 @@ void CUIProgressBar::Draw() Frect rect; GetAbsoluteRect(rect); - if (m_bBackgroundPresent) + if (IsShownBackground()) { UI().PushScissor(rect); m_UIBackgroundItem.Draw(); @@ -150,3 +138,74 @@ void CUIProgressBar::Draw() UI().PopScissor(); } } + +void CUIProgressBar::FillDebugInfo() +{ +#ifndef MASTER_GOLD + CUIWindow::FillDebugInfo(); + + if (!ImGui::CollapsingHeader(CUIProgressBar::GetDebugType())) + return; + + ImGui::DragFloat2("Position (current vs destination)", reinterpret_cast(&m_ProgressPos)); + ImGui::DragFloat("Current length", &m_CurrentLength); + + bool update{}; + update |= ImGui::DragFloat("Min position", &m_MinPos); + update |= ImGui::DragFloat("Max position", &m_MaxPos); + update |= ImGui::DragFloat("Inertion", &m_inertion); + ImGui::Separator(); + + constexpr pcstr styles[om_count] = + { + "Horizontal (left to right)", + "Vertical (bottom to top)", + "Horizontal (right to left)", + "Vertical (top to bottom)", + "Horizontal (from center to sides)", + "Vertical (from center to sides)", + }; + int mode = m_orient_mode; + if (ImGui::SliderInt("Mode", &mode, 0, om_count - 1, styles[m_orient_mode])) + { + m_orient_mode = (EOrientMode)mode; + update |= true; + } + + update |= ImGui::Button("Update"); + + bool v = IsShownBackground(); + if (ImGui::Checkbox("Background", &v)) + ShowBackground(v); + + v = m_bUseColor; + if (ImGui::Checkbox("Color", &v)) + { + m_bUseColor = v; + update |= true; + } + + v = m_bUseMiddleColor; + if (ImGui::Checkbox("Middle color", &v)) + { + m_bUseMiddleColor = v; + update |= true; + } + + v = m_bUseGradient; + if (ImGui::Checkbox("Gradient", &v)) + { + m_bUseGradient = v; + update |= true; + } + + ImGui::Separator(); + + update |= ImGui::ColorEdit4("Min color", reinterpret_cast(&m_minColor)); + update |= ImGui::ColorEdit4("Middle color", reinterpret_cast(&m_middleColor)); + update |= ImGui::ColorEdit4("Max color", reinterpret_cast(&m_maxColor)); + + if (update) + UpdateProgressBar(); +#endif +} diff --git a/src/xrUICore/ProgressBar/UIProgressBar.h b/src/xrUICore/ProgressBar/UIProgressBar.h index bfc7b0e6022..02501cd2c66 100644 --- a/src/xrUICore/ProgressBar/UIProgressBar.h +++ b/src/xrUICore/ProgressBar/UIProgressBar.h @@ -5,6 +5,7 @@ class XRUICORE_API CUIProgressBar final : public CUIWindow { friend class CUIXmlInitBase; + friend class CUIDoubleProgressBar; protected: using inherited = CUIWindow; @@ -18,23 +19,22 @@ class XRUICORE_API CUIProgressBar final : public CUIWindow om_fromcenter = 4, om_vfromcenter = 5, om_count - } m_orient_mode; + } m_orient_mode{ om_horz }; - bool m_bBackgroundPresent : 1 {}; - bool m_bUseColor : 1 {}; - bool m_bUseMiddleColor : 1 {}; // Hrust: optional middle color for CS/SoC compatibility, without middle color it doesn't looks correctly + bool m_bUseColor : 1 { false }; + bool m_bUseMiddleColor : 1 { false }; // Hrust: optional middle color for CS/SoC compatibility, without middle color it doesn't looks correctly bool m_bUseGradient : 1 { true }; //Alundaio: if false then use only solid color with m_maxColor - Fvector2 m_ProgressPos; // x-current y-dest - float m_MinPos; - float m_MaxPos; + Fvector2 m_ProgressPos{}; // x-current y-dest + float m_MinPos{ 1.0f }; + float m_MaxPos{ 1.0f + EPS }; - float m_CurrentLength; + float m_CurrentLength{}; - Fcolor m_minColor; - Fcolor m_middleColor; - Fcolor m_maxColor; - float m_inertion; + Fcolor m_minColor{}; + Fcolor m_middleColor{}; + Fcolor m_maxColor{}; + float m_inertion{}; protected: void UpdateProgressBar(); @@ -66,12 +66,13 @@ class XRUICORE_API CUIProgressBar final : public CUIWindow void ForceSetProgressPos(float pos); [[nodiscard]] - bool IsShownBackground() const { return m_bBackgroundPresent; } - void ShowBackground(bool status) { m_bBackgroundPresent = status; } + bool IsShownBackground() const { return m_UIBackgroundItem.GetVisible(); } + void ShowBackground(bool status) { m_UIBackgroundItem.SetVisible(status); } void UseGradient(bool status) { m_bUseGradient = status; } pcstr GetDebugType() override { return "CUIProgressBar"; } + void FillDebugInfo() override; private: DECLARE_SCRIPT_REGISTER_FUNCTION(CUIWindow); diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 6fbf252d10c..a193b1ec2d7 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -510,7 +510,7 @@ bool CUIXmlInitBase::InitProgressBar(CUIXml& xml_doc, pcstr path, int index, CUI if (xml_doc.NavigateToNode(buf, index)) { InitStatic(xml_doc, buf, index, &pWnd->m_UIBackgroundItem); - pWnd->m_bBackgroundPresent = true; + pWnd->ShowBackground(true); pWnd->m_UIBackgroundItem.SetWndSize(pWnd->GetWndSize()); } From 7a920d371909d39ca157eef1ecc4601ad70bbb5a Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Fri, 26 Dec 2025 03:15:31 +0300 Subject: [PATCH 122/261] Ability to debug CUIProgressBar and CUIDoubleProgressBar --- .../ProgressBar/UIDoubleProgressBar.cpp | 19 ++++- .../ProgressBar/UIDoubleProgressBar.h | 2 +- src/xrUICore/ProgressBar/UIProgressBar.cpp | 85 ++++++++++++++++--- src/xrUICore/ProgressBar/UIProgressBar.h | 29 ++++--- src/xrUICore/XML/UIXmlInitBase.cpp | 2 +- 5 files changed, 107 insertions(+), 30 deletions(-) diff --git a/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp b/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp index 9223036260c..594c4589eab 100644 --- a/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp +++ b/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp @@ -8,7 +8,6 @@ CUIDoubleProgressBar::CUIDoubleProgressBar() : CUIWindow("CUIDoubleProgressBar") AttachChild(&m_progress_two); } -CUIDoubleProgressBar::~CUIDoubleProgressBar() {} void CUIDoubleProgressBar::InitFromXml(CUIXml& xml_doc, LPCSTR path) { CUIXmlInitBase::InitProgressBar(xml_doc, path, 0, &m_progress_one); @@ -48,3 +47,21 @@ void CUIDoubleProgressBar::SetTwoPos(float cur_value, float compare_value) m_progress_two.SetProgressPos(cur_value); } } + +void CUIDoubleProgressBar::FillDebugInfo() +{ +#ifndef MASTER_GOLD + CUIWindow::FillDebugInfo(); + + if (!ImGui::CollapsingHeader(CUIDoubleProgressBar::GetDebugType())) + return; + + Fcolor color = m_less_color; + if (ImGui::ColorEdit4("Less color", reinterpret_cast(&color))) + m_less_color = color.get(); + + color = m_more_color; + if (ImGui::ColorEdit4("More color", reinterpret_cast(&color))) + m_less_color = color.get(); +#endif +} diff --git a/src/xrUICore/ProgressBar/UIDoubleProgressBar.h b/src/xrUICore/ProgressBar/UIDoubleProgressBar.h index a52de3c63f8..5817038be15 100644 --- a/src/xrUICore/ProgressBar/UIDoubleProgressBar.h +++ b/src/xrUICore/ProgressBar/UIDoubleProgressBar.h @@ -6,12 +6,12 @@ class XRUICORE_API CUIDoubleProgressBar final : public CUIWindow { public: // func CUIDoubleProgressBar(); - virtual ~CUIDoubleProgressBar(); void InitFromXml(CUIXml& xml_doc, LPCSTR path); void SetTwoPos(float cur_value, float compare_value); pcstr GetDebugType() override { return "CUIDoubleProgressBar"; } + void FillDebugInfo() override; protected: CUIProgressBar m_progress_one; diff --git a/src/xrUICore/ProgressBar/UIProgressBar.cpp b/src/xrUICore/ProgressBar/UIProgressBar.cpp index 60bf32da033..3e55849abed 100644 --- a/src/xrUICore/ProgressBar/UIProgressBar.cpp +++ b/src/xrUICore/ProgressBar/UIProgressBar.cpp @@ -4,22 +4,10 @@ CUIProgressBar::CUIProgressBar() : CUIWindow("CUIProgressBar"), m_UIProgressItem("Progress"), m_UIBackgroundItem("Background") { - m_MinPos = 1.0f; - m_MaxPos = 1.0f + EPS; - Enable(false); - m_bBackgroundPresent = false; - m_bUseColor = false; - m_bUseMiddleColor = false; - m_bUseGradient = true; - AttachChild(&m_UIBackgroundItem); AttachChild(&m_UIProgressItem); - m_ProgressPos.x = 0.0f; - m_ProgressPos.y = 0.0f; - m_inertion = 0.0f; - m_orient_mode = om_horz; } void CUIProgressBar::InitProgressBar(Fvector2 pos, Fvector2 size, EOrientMode mode) @@ -110,7 +98,7 @@ void CUIProgressBar::Draw() Frect rect; GetAbsoluteRect(rect); - if (m_bBackgroundPresent) + if (IsShownBackground()) { UI().PushScissor(rect); m_UIBackgroundItem.Draw(); @@ -150,3 +138,74 @@ void CUIProgressBar::Draw() UI().PopScissor(); } } + +void CUIProgressBar::FillDebugInfo() +{ +#ifndef MASTER_GOLD + CUIWindow::FillDebugInfo(); + + if (!ImGui::CollapsingHeader(CUIProgressBar::GetDebugType())) + return; + + ImGui::DragFloat2("Position (current vs destination)", reinterpret_cast(&m_ProgressPos)); + ImGui::DragFloat("Current length", &m_CurrentLength); + + bool update{}; + update |= ImGui::DragFloat("Min position", &m_MinPos); + update |= ImGui::DragFloat("Max position", &m_MaxPos); + update |= ImGui::DragFloat("Inertion", &m_inertion); + ImGui::Separator(); + + constexpr pcstr styles[om_count] = + { + "Horizontal (left to right)", + "Vertical (bottom to top)", + "Horizontal (right to left)", + "Vertical (top to bottom)", + "Horizontal (from center to sides)", + "Vertical (from center to sides)", + }; + int mode = m_orient_mode; + if (ImGui::SliderInt("Mode", &mode, 0, om_count - 1, styles[m_orient_mode])) + { + m_orient_mode = (EOrientMode)mode; + update |= true; + } + + update |= ImGui::Button("Update"); + + bool v = IsShownBackground(); + if (ImGui::Checkbox("Background", &v)) + ShowBackground(v); + + v = m_bUseColor; + if (ImGui::Checkbox("Color", &v)) + { + m_bUseColor = v; + update |= true; + } + + v = m_bUseMiddleColor; + if (ImGui::Checkbox("Middle color", &v)) + { + m_bUseMiddleColor = v; + update |= true; + } + + v = m_bUseGradient; + if (ImGui::Checkbox("Gradient", &v)) + { + m_bUseGradient = v; + update |= true; + } + + ImGui::Separator(); + + update |= ImGui::ColorEdit4("Min color", reinterpret_cast(&m_minColor)); + update |= ImGui::ColorEdit4("Middle color", reinterpret_cast(&m_middleColor)); + update |= ImGui::ColorEdit4("Max color", reinterpret_cast(&m_maxColor)); + + if (update) + UpdateProgressBar(); +#endif +} diff --git a/src/xrUICore/ProgressBar/UIProgressBar.h b/src/xrUICore/ProgressBar/UIProgressBar.h index bfc7b0e6022..02501cd2c66 100644 --- a/src/xrUICore/ProgressBar/UIProgressBar.h +++ b/src/xrUICore/ProgressBar/UIProgressBar.h @@ -5,6 +5,7 @@ class XRUICORE_API CUIProgressBar final : public CUIWindow { friend class CUIXmlInitBase; + friend class CUIDoubleProgressBar; protected: using inherited = CUIWindow; @@ -18,23 +19,22 @@ class XRUICORE_API CUIProgressBar final : public CUIWindow om_fromcenter = 4, om_vfromcenter = 5, om_count - } m_orient_mode; + } m_orient_mode{ om_horz }; - bool m_bBackgroundPresent : 1 {}; - bool m_bUseColor : 1 {}; - bool m_bUseMiddleColor : 1 {}; // Hrust: optional middle color for CS/SoC compatibility, without middle color it doesn't looks correctly + bool m_bUseColor : 1 { false }; + bool m_bUseMiddleColor : 1 { false }; // Hrust: optional middle color for CS/SoC compatibility, without middle color it doesn't looks correctly bool m_bUseGradient : 1 { true }; //Alundaio: if false then use only solid color with m_maxColor - Fvector2 m_ProgressPos; // x-current y-dest - float m_MinPos; - float m_MaxPos; + Fvector2 m_ProgressPos{}; // x-current y-dest + float m_MinPos{ 1.0f }; + float m_MaxPos{ 1.0f + EPS }; - float m_CurrentLength; + float m_CurrentLength{}; - Fcolor m_minColor; - Fcolor m_middleColor; - Fcolor m_maxColor; - float m_inertion; + Fcolor m_minColor{}; + Fcolor m_middleColor{}; + Fcolor m_maxColor{}; + float m_inertion{}; protected: void UpdateProgressBar(); @@ -66,12 +66,13 @@ class XRUICORE_API CUIProgressBar final : public CUIWindow void ForceSetProgressPos(float pos); [[nodiscard]] - bool IsShownBackground() const { return m_bBackgroundPresent; } - void ShowBackground(bool status) { m_bBackgroundPresent = status; } + bool IsShownBackground() const { return m_UIBackgroundItem.GetVisible(); } + void ShowBackground(bool status) { m_UIBackgroundItem.SetVisible(status); } void UseGradient(bool status) { m_bUseGradient = status; } pcstr GetDebugType() override { return "CUIProgressBar"; } + void FillDebugInfo() override; private: DECLARE_SCRIPT_REGISTER_FUNCTION(CUIWindow); diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 6fbf252d10c..a193b1ec2d7 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -510,7 +510,7 @@ bool CUIXmlInitBase::InitProgressBar(CUIXml& xml_doc, pcstr path, int index, CUI if (xml_doc.NavigateToNode(buf, index)) { InitStatic(xml_doc, buf, index, &pWnd->m_UIBackgroundItem); - pWnd->m_bBackgroundPresent = true; + pWnd->ShowBackground(true); pWnd->m_UIBackgroundItem.SetWndSize(pWnd->GetWndSize()); } From b4b75a33d43e5a2626d5c08028808630858662dc Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Fri, 26 Dec 2025 04:11:31 +0300 Subject: [PATCH 123/261] Display differences between items in inventory in SOC (#392) This wasn't in original SOC, but it's nice to reuse new CS/COP features. --- src/xrGame/ui/UIActorMenu.cpp | 12 ++++++------ .../ProgressBar/UIDoubleProgressBar.cpp | 17 +++++++++++++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/xrGame/ui/UIActorMenu.cpp b/src/xrGame/ui/UIActorMenu.cpp index 484f27b0686..14f753e76b6 100644 --- a/src/xrGame/ui/UIActorMenu.cpp +++ b/src/xrGame/ui/UIActorMenu.cpp @@ -416,16 +416,16 @@ void CUIActorMenu::SetCurrentItem(CUICellItem* itm) pcstr trade_tip = nullptr; if (PIItem current_item = itm ? (PIItem)itm->m_pData : nullptr) { + u16 compare_slot = current_item->BaseSlot(); + if (compare_slot != NO_ACTIVE_SLOT) + { + compare_item = m_pActorInvOwner->inventory().ItemFromSlot(compare_slot); + } + if (GetMenuMode() != mmTrade) item_price = current_item->Cost(); else { - u16 compare_slot = current_item->BaseSlot(); - if (compare_slot != NO_ACTIVE_SLOT) - { - compare_item = m_pActorInvOwner->inventory().ItemFromSlot(compare_slot); - } - CInventoryOwner* item_owner = smart_cast(current_item->m_pInventory->GetOwner()); if (item_owner && item_owner == m_pActorInvOwner) diff --git a/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp b/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp index 594c4589eab..5070f6ab0dc 100644 --- a/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp +++ b/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp @@ -13,11 +13,20 @@ void CUIDoubleProgressBar::InitFromXml(CUIXml& xml_doc, LPCSTR path) CUIXmlInitBase::InitProgressBar(xml_doc, path, 0, &m_progress_one); CUIXmlInitBase::InitProgressBar(xml_doc, path, 0, &m_progress_two); + u32 less_color = color_rgba(255, 0, 0, 255); + u32 more_color = color_rgba(0, 255, 0, 255); + + if (m_progress_one.m_bUseColor) + { + less_color = m_progress_one.m_minColor.get(); + more_color = m_progress_one.m_maxColor.get(); + } + string256 buf; strconcat(sizeof(buf), buf, path, ":color_less"); - m_less_color = CUIXmlInitBase::GetColor(xml_doc, buf, 0, color_rgba(255, 0, 0, 255)); + m_less_color = CUIXmlInitBase::GetColor(xml_doc, buf, 0, less_color); strconcat(sizeof(buf), buf, path, ":color_more"); - m_more_color = CUIXmlInitBase::GetColor(xml_doc, buf, 0, color_rgba(0, 255, 0, 255)); + m_more_color = CUIXmlInitBase::GetColor(xml_doc, buf, 0, more_color); m_progress_one.SetRange(0.0f, 100.0f); m_progress_two.SetRange(0.0f, 100.0f); @@ -30,15 +39,15 @@ void CUIDoubleProgressBar::SetTwoPos(float cur_value, float compare_value) { if (cur_value < compare_value) // red { - m_progress_one.m_UIProgressItem.SetTextureColor(m_less_color); m_progress_one.SetProgressPos(compare_value); m_progress_two.SetProgressPos(cur_value); + m_progress_one.m_UIProgressItem.SetTextureColor(m_less_color); } else if (cur_value > compare_value) // green { - m_progress_one.m_UIProgressItem.SetTextureColor(m_more_color); m_progress_one.SetProgressPos(cur_value); m_progress_two.SetProgressPos(compare_value); + m_progress_one.m_UIProgressItem.SetTextureColor(m_more_color); } else { From dd7b65fe1aa2f8c02e3571567f8c7316ed109d8e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Fri, 26 Dec 2025 04:11:31 +0300 Subject: [PATCH 124/261] Display differences between items in inventory in SOC (#392) This wasn't in original SOC, but it's nice to reuse new CS/COP features. --- src/xrGame/ui/UIActorMenu.cpp | 12 ++++++------ .../ProgressBar/UIDoubleProgressBar.cpp | 17 +++++++++++++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/xrGame/ui/UIActorMenu.cpp b/src/xrGame/ui/UIActorMenu.cpp index 484f27b0686..14f753e76b6 100644 --- a/src/xrGame/ui/UIActorMenu.cpp +++ b/src/xrGame/ui/UIActorMenu.cpp @@ -416,16 +416,16 @@ void CUIActorMenu::SetCurrentItem(CUICellItem* itm) pcstr trade_tip = nullptr; if (PIItem current_item = itm ? (PIItem)itm->m_pData : nullptr) { + u16 compare_slot = current_item->BaseSlot(); + if (compare_slot != NO_ACTIVE_SLOT) + { + compare_item = m_pActorInvOwner->inventory().ItemFromSlot(compare_slot); + } + if (GetMenuMode() != mmTrade) item_price = current_item->Cost(); else { - u16 compare_slot = current_item->BaseSlot(); - if (compare_slot != NO_ACTIVE_SLOT) - { - compare_item = m_pActorInvOwner->inventory().ItemFromSlot(compare_slot); - } - CInventoryOwner* item_owner = smart_cast(current_item->m_pInventory->GetOwner()); if (item_owner && item_owner == m_pActorInvOwner) diff --git a/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp b/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp index 594c4589eab..5070f6ab0dc 100644 --- a/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp +++ b/src/xrUICore/ProgressBar/UIDoubleProgressBar.cpp @@ -13,11 +13,20 @@ void CUIDoubleProgressBar::InitFromXml(CUIXml& xml_doc, LPCSTR path) CUIXmlInitBase::InitProgressBar(xml_doc, path, 0, &m_progress_one); CUIXmlInitBase::InitProgressBar(xml_doc, path, 0, &m_progress_two); + u32 less_color = color_rgba(255, 0, 0, 255); + u32 more_color = color_rgba(0, 255, 0, 255); + + if (m_progress_one.m_bUseColor) + { + less_color = m_progress_one.m_minColor.get(); + more_color = m_progress_one.m_maxColor.get(); + } + string256 buf; strconcat(sizeof(buf), buf, path, ":color_less"); - m_less_color = CUIXmlInitBase::GetColor(xml_doc, buf, 0, color_rgba(255, 0, 0, 255)); + m_less_color = CUIXmlInitBase::GetColor(xml_doc, buf, 0, less_color); strconcat(sizeof(buf), buf, path, ":color_more"); - m_more_color = CUIXmlInitBase::GetColor(xml_doc, buf, 0, color_rgba(0, 255, 0, 255)); + m_more_color = CUIXmlInitBase::GetColor(xml_doc, buf, 0, more_color); m_progress_one.SetRange(0.0f, 100.0f); m_progress_two.SetRange(0.0f, 100.0f); @@ -30,15 +39,15 @@ void CUIDoubleProgressBar::SetTwoPos(float cur_value, float compare_value) { if (cur_value < compare_value) // red { - m_progress_one.m_UIProgressItem.SetTextureColor(m_less_color); m_progress_one.SetProgressPos(compare_value); m_progress_two.SetProgressPos(cur_value); + m_progress_one.m_UIProgressItem.SetTextureColor(m_less_color); } else if (cur_value > compare_value) // green { - m_progress_one.m_UIProgressItem.SetTextureColor(m_more_color); m_progress_one.SetProgressPos(cur_value); m_progress_two.SetProgressPos(compare_value); + m_progress_one.m_UIProgressItem.SetTextureColor(m_more_color); } else { From 840cb01e09bf75433c4f2675cde3a2a7ea99cdfa Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Fri, 26 Dec 2025 04:14:10 +0300 Subject: [PATCH 125/261] Don't screw immunities and damage calculations (#392) This commit partially reverts and reworks b81ed7c774506cf354d130de5b0a9b5c4afab702 --- src/xrGame/Artefact.cpp | 4 +++- src/xrGame/hit_immunity.cpp | 17 +++++------------ src/xrGame/hit_immunity.h | 4 ++-- src/xrGame/ui/ui_af_params.cpp | 8 ++++---- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/xrGame/Artefact.cpp b/src/xrGame/Artefact.cpp index c001bab8c41..a52cf72af5f 100644 --- a/src/xrGame/Artefact.cpp +++ b/src/xrGame/Artefact.cpp @@ -72,7 +72,9 @@ void CArtefact::Load(LPCSTR section) if (pSettings->section_exist(pSettings->r_string(section, "hit_absorbation_sect"))) { - m_ArtefactHitImmunities.LoadImmunities(pSettings->r_string(section, "hit_absorbation_sect"), pSettings); + // SOC vs CS/COP are inverted, convert to CS/COP format. + const bool is_soc = GMLib.GetLibraryVersion() <= GAMEMTL_VERSION_SOC; + m_ArtefactHitImmunities.LoadImmunities(pSettings->r_string(section, "hit_absorbation_sect"), pSettings, is_soc); } m_bCanSpawnZone = !!pSettings->line_exist("artefact_spawn_zones", section); m_af_rank = pSettings->read_if_exists(section, "af_rank", 0); diff --git a/src/xrGame/hit_immunity.cpp b/src/xrGame/hit_immunity.cpp index 83e67a37001..0052a0bdbbf 100644 --- a/src/xrGame/hit_immunity.cpp +++ b/src/xrGame/hit_immunity.cpp @@ -9,7 +9,7 @@ CHitImmunity::CHitImmunity() { m_HitImmunityKoefs.resize(ALife::eHitTypeMax); for (u32 i = 0; i < ALife::eHitTypeMax; i++) - m_HitImmunityKoefs[i] = 0.0f; + m_HitImmunityKoefs[i] = 1.0f; } constexpr std::tuple hit_immunities[] = @@ -28,40 +28,33 @@ constexpr std::tuple hit_immunities[] = { ALife::eHitTypePhysicStrike, "physic_strike_wound_immunity", true }, }; -void CHitImmunity::LoadImmunities(const char* imm_sect, const CInifile* ini) +void CHitImmunity::LoadImmunities(const char* imm_sect, const CInifile* ini, bool invert /*= false*/) { R_ASSERT2(ini->section_exist(imm_sect), imm_sect); - // SOC vs CS/COP are inverted, convert to CS/COP format. - const bool is_soc = GMLib.GetLibraryVersion() <= GAMEMTL_VERSION_SOC; - for (const auto& [hit_type, immunity_name, optional] : hit_immunities) { if (optional && !ini->line_exist(imm_sect, immunity_name)) continue; float immunity = ini->r_float(imm_sect, immunity_name); - if (is_soc) + if (invert) immunity = 1.0f - immunity; m_HitImmunityKoefs[hit_type] = immunity; } } -void CHitImmunity::AddImmunities(const char* imm_sect, const CInifile* ini) +void CHitImmunity::AddImmunities(const char* imm_sect, const CInifile* ini, bool invert /*= false*/) { R_ASSERT2(ini->section_exist(imm_sect), imm_sect); - // Consistency is the key. - // We want this to have SOC format in SOC, the same as LoadImmunities does. - const bool is_soc = GMLib.GetLibraryVersion() <= GAMEMTL_VERSION_SOC; - for (const auto& [hit_type, immunity_name, _] : hit_immunities) { if (!ini->line_exist(imm_sect, immunity_name)) continue; float immunity = ini->r_float(imm_sect, immunity_name); - if (is_soc) + if (invert) immunity = 1.0f - immunity; m_HitImmunityKoefs[hit_type] += immunity; } diff --git a/src/xrGame/hit_immunity.h b/src/xrGame/hit_immunity.h index 7c8c7211bd6..a5b5fdd9c99 100644 --- a/src/xrGame/hit_immunity.h +++ b/src/xrGame/hit_immunity.h @@ -21,8 +21,8 @@ class CHitImmunity CHitImmunity(); virtual ~CHitImmunity() = default; - void LoadImmunities(const char* section, const CInifile* ini); - void AddImmunities(const char* section, const CInifile* ini); + void LoadImmunities(const char* section, const CInifile* ini, bool invert = false); + void AddImmunities(const char* section, const CInifile* ini, bool invert = false); float GetHitImmunity(ALife::EHitType hit_type) const { return m_HitImmunityKoefs[hit_type]; } float AffectHit(float power, ALife::EHitType hit_type) const { return power * GetHitImmunity(hit_type); } }; diff --git a/src/xrGame/ui/ui_af_params.cpp b/src/xrGame/ui/ui_af_params.cpp index 5946146376d..d2b305834eb 100644 --- a/src/xrGame/ui/ui_af_params.cpp +++ b/src/xrGame/ui/ui_af_params.cpp @@ -149,7 +149,7 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) setValue(m_disp_condition, pInvItem.GetCondition()); //-Alundaio - const bool is_cs_cop = GMLib.GetLibraryVersion() > GAMEMTL_VERSION_SOC; + const bool is_soc = GMLib.GetLibraryVersion() <= GAMEMTL_VERSION_SOC; for (auto [id, restore_section, actor_condition, restore_caption, magnitude, sign_inverse, unit] : af_restore) { @@ -161,7 +161,7 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) continue; val = val * pInvItem.GetCondition(); - if (!is_cs_cop) + if (is_soc) { const float max_val = pSettings->r_float(condition_sect, actor_condition); val /= max_val; @@ -170,7 +170,7 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) } CHitImmunity immunities; - immunities.LoadImmunities(hit_absorbation_sect, pSettings); + immunities.LoadImmunities(hit_absorbation_sect, pSettings, is_soc); for (auto [id, immunity_section, immunity_caption, magnitude, sign_inverse, unit] : af_immunity) { @@ -182,7 +182,7 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) continue; val *= pInvItem.GetCondition(); - if (is_cs_cop) + if (!is_soc) { const float max_val = actor->conditions().GetZoneMaxPower(id); val /= max_val; From 9cc4e8d61edfa362c749045341e24748f9cf47ce Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Fri, 26 Dec 2025 04:14:10 +0300 Subject: [PATCH 126/261] Don't screw immunities and damage calculations (#392) This commit partially reverts and reworks b81ed7c774506cf354d130de5b0a9b5c4afab702 --- src/xrGame/Artefact.cpp | 4 +++- src/xrGame/hit_immunity.cpp | 17 +++++------------ src/xrGame/hit_immunity.h | 4 ++-- src/xrGame/ui/ui_af_params.cpp | 8 ++++---- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/xrGame/Artefact.cpp b/src/xrGame/Artefact.cpp index c001bab8c41..a52cf72af5f 100644 --- a/src/xrGame/Artefact.cpp +++ b/src/xrGame/Artefact.cpp @@ -72,7 +72,9 @@ void CArtefact::Load(LPCSTR section) if (pSettings->section_exist(pSettings->r_string(section, "hit_absorbation_sect"))) { - m_ArtefactHitImmunities.LoadImmunities(pSettings->r_string(section, "hit_absorbation_sect"), pSettings); + // SOC vs CS/COP are inverted, convert to CS/COP format. + const bool is_soc = GMLib.GetLibraryVersion() <= GAMEMTL_VERSION_SOC; + m_ArtefactHitImmunities.LoadImmunities(pSettings->r_string(section, "hit_absorbation_sect"), pSettings, is_soc); } m_bCanSpawnZone = !!pSettings->line_exist("artefact_spawn_zones", section); m_af_rank = pSettings->read_if_exists(section, "af_rank", 0); diff --git a/src/xrGame/hit_immunity.cpp b/src/xrGame/hit_immunity.cpp index 83e67a37001..0052a0bdbbf 100644 --- a/src/xrGame/hit_immunity.cpp +++ b/src/xrGame/hit_immunity.cpp @@ -9,7 +9,7 @@ CHitImmunity::CHitImmunity() { m_HitImmunityKoefs.resize(ALife::eHitTypeMax); for (u32 i = 0; i < ALife::eHitTypeMax; i++) - m_HitImmunityKoefs[i] = 0.0f; + m_HitImmunityKoefs[i] = 1.0f; } constexpr std::tuple hit_immunities[] = @@ -28,40 +28,33 @@ constexpr std::tuple hit_immunities[] = { ALife::eHitTypePhysicStrike, "physic_strike_wound_immunity", true }, }; -void CHitImmunity::LoadImmunities(const char* imm_sect, const CInifile* ini) +void CHitImmunity::LoadImmunities(const char* imm_sect, const CInifile* ini, bool invert /*= false*/) { R_ASSERT2(ini->section_exist(imm_sect), imm_sect); - // SOC vs CS/COP are inverted, convert to CS/COP format. - const bool is_soc = GMLib.GetLibraryVersion() <= GAMEMTL_VERSION_SOC; - for (const auto& [hit_type, immunity_name, optional] : hit_immunities) { if (optional && !ini->line_exist(imm_sect, immunity_name)) continue; float immunity = ini->r_float(imm_sect, immunity_name); - if (is_soc) + if (invert) immunity = 1.0f - immunity; m_HitImmunityKoefs[hit_type] = immunity; } } -void CHitImmunity::AddImmunities(const char* imm_sect, const CInifile* ini) +void CHitImmunity::AddImmunities(const char* imm_sect, const CInifile* ini, bool invert /*= false*/) { R_ASSERT2(ini->section_exist(imm_sect), imm_sect); - // Consistency is the key. - // We want this to have SOC format in SOC, the same as LoadImmunities does. - const bool is_soc = GMLib.GetLibraryVersion() <= GAMEMTL_VERSION_SOC; - for (const auto& [hit_type, immunity_name, _] : hit_immunities) { if (!ini->line_exist(imm_sect, immunity_name)) continue; float immunity = ini->r_float(imm_sect, immunity_name); - if (is_soc) + if (invert) immunity = 1.0f - immunity; m_HitImmunityKoefs[hit_type] += immunity; } diff --git a/src/xrGame/hit_immunity.h b/src/xrGame/hit_immunity.h index 7c8c7211bd6..a5b5fdd9c99 100644 --- a/src/xrGame/hit_immunity.h +++ b/src/xrGame/hit_immunity.h @@ -21,8 +21,8 @@ class CHitImmunity CHitImmunity(); virtual ~CHitImmunity() = default; - void LoadImmunities(const char* section, const CInifile* ini); - void AddImmunities(const char* section, const CInifile* ini); + void LoadImmunities(const char* section, const CInifile* ini, bool invert = false); + void AddImmunities(const char* section, const CInifile* ini, bool invert = false); float GetHitImmunity(ALife::EHitType hit_type) const { return m_HitImmunityKoefs[hit_type]; } float AffectHit(float power, ALife::EHitType hit_type) const { return power * GetHitImmunity(hit_type); } }; diff --git a/src/xrGame/ui/ui_af_params.cpp b/src/xrGame/ui/ui_af_params.cpp index 5946146376d..d2b305834eb 100644 --- a/src/xrGame/ui/ui_af_params.cpp +++ b/src/xrGame/ui/ui_af_params.cpp @@ -149,7 +149,7 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) setValue(m_disp_condition, pInvItem.GetCondition()); //-Alundaio - const bool is_cs_cop = GMLib.GetLibraryVersion() > GAMEMTL_VERSION_SOC; + const bool is_soc = GMLib.GetLibraryVersion() <= GAMEMTL_VERSION_SOC; for (auto [id, restore_section, actor_condition, restore_caption, magnitude, sign_inverse, unit] : af_restore) { @@ -161,7 +161,7 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) continue; val = val * pInvItem.GetCondition(); - if (!is_cs_cop) + if (is_soc) { const float max_val = pSettings->r_float(condition_sect, actor_condition); val /= max_val; @@ -170,7 +170,7 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) } CHitImmunity immunities; - immunities.LoadImmunities(hit_absorbation_sect, pSettings); + immunities.LoadImmunities(hit_absorbation_sect, pSettings, is_soc); for (auto [id, immunity_section, immunity_caption, magnitude, sign_inverse, unit] : af_immunity) { @@ -182,7 +182,7 @@ void CUIArtefactParams::SetInfo(const CInventoryItem& pInvItem) continue; val *= pInvItem.GetCondition(); - if (is_cs_cop) + if (!is_soc) { const float max_val = actor->conditions().GetZoneMaxPower(id); val /= max_val; From 1e211e04178f711b1bb4315269ac1e88c815a18b Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 27 Dec 2025 16:33:17 +0300 Subject: [PATCH 127/261] xrUICore/Windows/UIFrameLineWnd.cpp: remove ICF to fix possible linking problems --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index a76db3250f2..0c429afa3f1 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -68,7 +68,7 @@ void CUIFrameLineWnd::Draw() constexpr Fvector2 pt_offset = { -0.5f, -0.5f }; -ICF void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 clr, Fvector2 const& ts) +void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 clr, Fvector2 const& ts) { UI().AlignPixel(LTp.x); UI().AlignPixel(LTp.y); From e1144a0b754f8d2d4fff6ac0e81b267e0934ebc8 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 27 Dec 2025 16:33:17 +0300 Subject: [PATCH 128/261] xrUICore/Windows/UIFrameLineWnd.cpp: remove ICF to fix possible linking problems --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index a76db3250f2..0c429afa3f1 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -68,7 +68,7 @@ void CUIFrameLineWnd::Draw() constexpr Fvector2 pt_offset = { -0.5f, -0.5f }; -ICF void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 clr, Fvector2 const& ts) +void draw_rect(Fvector2 LTp, Fvector2 RBp, Fvector2 LTt, Fvector2 RBt, u32 clr, Fvector2 const& ts) { UI().AlignPixel(LTp.x); UI().AlignPixel(LTp.y); From fe67f720e3d96e6166fab6a8106660ea09f02428 Mon Sep 17 00:00:00 2001 From: Rybaev Ilya Mikhailovich <61088459+Aardes-Roiven@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:47:52 +0300 Subject: [PATCH 129/261] shared_str: add bool operator and prohibit direct comparison with nullptr (#1948) (#2021) --- src/Layers/xrRender/SkeletonAnimated.cpp | 6 +++--- src/xrCore/xrstring.h | 9 ++++++++- src/xrGame/Actor.cpp | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Layers/xrRender/SkeletonAnimated.cpp b/src/Layers/xrRender/SkeletonAnimated.cpp index 84ae9343262..84d1f56bf9d 100644 --- a/src/Layers/xrRender/SkeletonAnimated.cpp +++ b/src/Layers/xrRender/SkeletonAnimated.cpp @@ -177,7 +177,7 @@ u16 CKinematicsAnimated::LL_PartID(LPCSTR B) for (u16 id = 0; id < MAX_PARTS; id++) { CPartDef& P = (*m_Partition)[id]; - if (nullptr == P.Name) + if (!P.Name) continue; if (0 == xr_stricmp(B, P.Name.c_str())) return id; @@ -367,7 +367,7 @@ CBlend* CKinematicsAnimated::LL_PlayCycle(u16 part, MotionID motion_ID, BOOL bMi } if (part >= MAX_PARTS) return nullptr; - if (nullptr == m_Partition->part(part).Name) + if (!m_Partition->part(part).Name) return nullptr; // shared_motions* s_mots = &m_Motions[motion.slot]; @@ -514,7 +514,7 @@ void CKinematicsAnimated::LL_UpdateTracks(float dt, bool b_force, bool leave_ble // Cycles for (u16 part = 0; part < MAX_PARTS; part++) { - if (nullptr == m_Partition->part(part).Name) + if (!m_Partition->part(part).Name) continue; I = blend_cycles[part].begin(); E = blend_cycles[part].end(); diff --git a/src/xrCore/xrstring.h b/src/xrCore/xrstring.h index c53123b8e31..dc08b318729 100644 --- a/src/xrCore/xrstring.h +++ b/src/xrCore/xrstring.h @@ -141,7 +141,8 @@ class shared_str [[nodiscard]] bool operator!() const { return p_ == nullptr; } - + [[nodiscard]] + explicit operator bool() const { return p_ != nullptr; } [[nodiscard]] char operator[](size_t id) { return p_->value[id]; } [[nodiscard]] @@ -201,6 +202,12 @@ struct std::hash } }; +bool operator==(const shared_str&, std::nullptr_t) = delete; +bool operator!=(const shared_str&, std::nullptr_t) = delete; + +bool operator==(std::nullptr_t, const shared_str&) = delete; +bool operator!=(std::nullptr_t, const shared_str&) = delete; + // res_ptr == res_ptr // res_ptr != res_ptr // const res_ptr == ptr diff --git a/src/xrGame/Actor.cpp b/src/xrGame/Actor.cpp index cade69e8128..66c43f3f374 100644 --- a/src/xrGame/Actor.cpp +++ b/src/xrGame/Actor.cpp @@ -1500,7 +1500,7 @@ void CActor::shedule_Update(u32 DT) } else if (m_pVehicleWeLookingAt) { - m_sDefaultObjAction = m_pVehicleWeLookingAt->m_sUseAction != nullptr ? m_pVehicleWeLookingAt->m_sUseAction : m_sCarCharacterUseAction; + m_sDefaultObjAction = m_pVehicleWeLookingAt->m_sUseAction ? m_pVehicleWeLookingAt->m_sUseAction : m_sCarCharacterUseAction; } else if (m_pObjectWeLookingAt && m_pObjectWeLookingAt->cast_inventory_item() && m_pObjectWeLookingAt->cast_inventory_item()->CanTake()) From 7746c4b395e3165913946790a33a8d597ed009a6 Mon Sep 17 00:00:00 2001 From: Rybaev Ilya Mikhailovich <61088459+Aardes-Roiven@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:47:52 +0300 Subject: [PATCH 130/261] shared_str: add bool operator and prohibit direct comparison with nullptr (#1948) (#2021) --- src/Layers/xrRender/SkeletonAnimated.cpp | 6 +++--- src/xrCore/xrstring.h | 9 ++++++++- src/xrGame/Actor.cpp | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Layers/xrRender/SkeletonAnimated.cpp b/src/Layers/xrRender/SkeletonAnimated.cpp index 84ae9343262..84d1f56bf9d 100644 --- a/src/Layers/xrRender/SkeletonAnimated.cpp +++ b/src/Layers/xrRender/SkeletonAnimated.cpp @@ -177,7 +177,7 @@ u16 CKinematicsAnimated::LL_PartID(LPCSTR B) for (u16 id = 0; id < MAX_PARTS; id++) { CPartDef& P = (*m_Partition)[id]; - if (nullptr == P.Name) + if (!P.Name) continue; if (0 == xr_stricmp(B, P.Name.c_str())) return id; @@ -367,7 +367,7 @@ CBlend* CKinematicsAnimated::LL_PlayCycle(u16 part, MotionID motion_ID, BOOL bMi } if (part >= MAX_PARTS) return nullptr; - if (nullptr == m_Partition->part(part).Name) + if (!m_Partition->part(part).Name) return nullptr; // shared_motions* s_mots = &m_Motions[motion.slot]; @@ -514,7 +514,7 @@ void CKinematicsAnimated::LL_UpdateTracks(float dt, bool b_force, bool leave_ble // Cycles for (u16 part = 0; part < MAX_PARTS; part++) { - if (nullptr == m_Partition->part(part).Name) + if (!m_Partition->part(part).Name) continue; I = blend_cycles[part].begin(); E = blend_cycles[part].end(); diff --git a/src/xrCore/xrstring.h b/src/xrCore/xrstring.h index c53123b8e31..dc08b318729 100644 --- a/src/xrCore/xrstring.h +++ b/src/xrCore/xrstring.h @@ -141,7 +141,8 @@ class shared_str [[nodiscard]] bool operator!() const { return p_ == nullptr; } - + [[nodiscard]] + explicit operator bool() const { return p_ != nullptr; } [[nodiscard]] char operator[](size_t id) { return p_->value[id]; } [[nodiscard]] @@ -201,6 +202,12 @@ struct std::hash } }; +bool operator==(const shared_str&, std::nullptr_t) = delete; +bool operator!=(const shared_str&, std::nullptr_t) = delete; + +bool operator==(std::nullptr_t, const shared_str&) = delete; +bool operator!=(std::nullptr_t, const shared_str&) = delete; + // res_ptr == res_ptr // res_ptr != res_ptr // const res_ptr == ptr diff --git a/src/xrGame/Actor.cpp b/src/xrGame/Actor.cpp index cade69e8128..66c43f3f374 100644 --- a/src/xrGame/Actor.cpp +++ b/src/xrGame/Actor.cpp @@ -1500,7 +1500,7 @@ void CActor::shedule_Update(u32 DT) } else if (m_pVehicleWeLookingAt) { - m_sDefaultObjAction = m_pVehicleWeLookingAt->m_sUseAction != nullptr ? m_pVehicleWeLookingAt->m_sUseAction : m_sCarCharacterUseAction; + m_sDefaultObjAction = m_pVehicleWeLookingAt->m_sUseAction ? m_pVehicleWeLookingAt->m_sUseAction : m_sCarCharacterUseAction; } else if (m_pObjectWeLookingAt && m_pObjectWeLookingAt->cast_inventory_item() && m_pObjectWeLookingAt->cast_inventory_item()->CanTake()) From 5fc1dd5fa136b80c6ed846e6763a8f7569d8a404 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 27 Dec 2025 23:16:43 +0300 Subject: [PATCH 131/261] Make CS multiplayer UI generally work(#382) Only thing that's missing are captions from ui_custom_msgs.xml, and I'm hesitant to make workarounds for this. --- src/xrGame/UIGameAHunt.cpp | 32 ++++++++++++++++----- src/xrGame/UIGameAHunt.h | 5 +++- src/xrGame/UIGameCTA.cpp | 17 +++++++---- src/xrGame/UIGameDM.cpp | 2 +- src/xrGame/UIGameTDM.cpp | 14 +++++---- src/xrGame/game_cl_artefacthunt.cpp | 5 ++-- src/xrGame/game_cl_capture_the_artefact.cpp | 2 +- src/xrGame/ui/UIMoneyIndicator.cpp | 10 ++++--- src/xrGame/ui/UIMoneyIndicator.h | 2 +- src/xrGame/ui/UIRankIndicator.cpp | 6 ++-- src/xrGame/ui/UIRankIndicator.h | 2 +- src/xrUICore/XML/UIXmlInitBase.cpp | 3 ++ 12 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/xrGame/UIGameAHunt.cpp b/src/xrGame/UIGameAHunt.cpp index b2297f08313..9a284e90a11 100644 --- a/src/xrGame/UIGameAHunt.cpp +++ b/src/xrGame/UIGameAHunt.cpp @@ -5,6 +5,7 @@ #include "Level.h" #include "game_cl_artefacthunt.h" #include "xrUICore/Static/UIStatic.h" +#include "xrUICore/ProgressBar/UIProgressShape.h" #include "ui/UIXmlInit.h" #include "ui/UIMessageBoxEx.h" #include "ui/UIMoneyIndicator.h" @@ -27,23 +28,29 @@ void CUIGameAHunt::Init(int stage) { // unique m_pTeamPanels->Init(TEAM_PANELS_AHUNT_XML_NAME, "team_panels_wnd"); - CUIXml uiXml; + CUIXml uiXml, uiXml2; uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "ui_game_ahunt.xml"); + uiXml2.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "ui_game_dm.xml"); // CS - CUIXmlInit::InitWindow(uiXml, "global", 0, Window); - CUIXmlInit::InitStatic(uiXml, "fraglimit", 0, m_pFragLimitIndicator); + CUIXmlInit::InitWindow(uiXml, "global", 0, Window, false); - m_pReinforcementInidcator = xr_new("Reinforcement indicator"); + m_pReinforcementInidcator = UIHelper::CreateProgressShape(uiXml, "reinforcement", nullptr, false); // CS + if (!m_pReinforcementInidcator) + m_pReinforcementInidcator = UIHelper::CreateStatic(uiXml, "reinforcement", nullptr); m_pReinforcementInidcator->SetAutoDelete(true); - CUIXmlInit::InitStatic(uiXml, "reinforcement", 0, m_pReinforcementInidcator); + + if (!CUIXmlInit::InitStatic(uiXml, "fraglimit", 0, m_pFragLimitIndicator, false)) + CUIXmlInit::InitStatic(uiXml2, "fraglimit", 0, m_pFragLimitIndicator, false); // CS CUIXmlInit::InitStatic(uiXml, "team1_icon", 0, m_team1_icon); CUIXmlInit::InitStatic(uiXml, "team2_icon", 0, m_team2_icon); CUIXmlInit::InitStatic(uiXml, "team1_score", 0, m_team1_score); CUIXmlInit::InitStatic(uiXml, "team2_score", 0, m_team2_score); - m_pMoneyIndicator->InitFromXML(uiXml); - m_pRankIndicator->InitFromXml(uiXml); + if (!m_pMoneyIndicator->InitFromXML(uiXml)) + m_pMoneyIndicator->InitFromXML(uiXml2); // CS + if (!m_pRankIndicator->InitFromXml(uiXml)) + m_pRankIndicator->InitFromXml(uiXml2); // CS } if (stage == 2) { // after @@ -72,3 +79,14 @@ void CUIGameAHunt::SetClGame(game_cl_GameState* g) } void CUIGameAHunt::SetBuyMsgCaption(LPCSTR str) { m_buy_msg_caption->SetTextST(str); } + +void CUIGameAHunt::SetReinforcementTimes(int curTime, int maxTime) +{ + if (auto shape = smart_cast(m_pReinforcementInidcator)) + shape->SetPos(curTime, maxTime); + else + { + string128 _buff; + m_pReinforcementInidcator->SetText(xr_itoa(curTime, _buff, 10)); + } +} diff --git a/src/xrGame/UIGameAHunt.h b/src/xrGame/UIGameAHunt.h index 11bc150be3e..d43e982ca78 100644 --- a/src/xrGame/UIGameAHunt.h +++ b/src/xrGame/UIGameAHunt.h @@ -18,8 +18,9 @@ class CUIGameAHunt : public CUIGameTDM game_cl_ArtefactHunt* m_game; typedef CUIGameTDM inherited; -public: CUIStatic* m_pReinforcementInidcator; + +public: CUIMessageBoxEx* m_pBuySpawnMsgBox; public: @@ -31,6 +32,8 @@ class CUIGameAHunt : public CUIGameTDM virtual void UnLoad(); void SetBuyMsgCaption(LPCSTR str); + void SetReinforcementTimes(int curTime, int maxTime); + protected: CUIStatic* m_buy_msg_caption; }; diff --git a/src/xrGame/UIGameCTA.cpp b/src/xrGame/UIGameCTA.cpp index e111ea00f6a..3b1266ffc92 100644 --- a/src/xrGame/UIGameCTA.cpp +++ b/src/xrGame/UIGameCTA.cpp @@ -63,7 +63,7 @@ void CUIGameCTA::Init(int stage) CUIXml uiXml; uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, CTA_GAME_WND_XML); - CUIXmlInit::InitWindow(uiXml, "global", 0, Window); + CUIXmlInit::InitWindow(uiXml, "global", 0, Window, false); m_pMoneyIndicator = xr_new(); m_pMoneyIndicator->SetAutoDelete(true); @@ -73,9 +73,11 @@ void CUIGameCTA::Init(int stage) m_pRankIndicator->SetAutoDelete(true); m_pRankIndicator->InitFromXml(uiXml); - m_pReinforcementInidcator = xr_new("Reinforcement indicator"); + m_pReinforcementInidcator = UIHelper::CreateProgressShape(uiXml, "reinforcement", nullptr, false); + if (!m_pReinforcementInidcator) + m_pReinforcementInidcator = UIHelper::CreateStatic(uiXml, "reinforcement", nullptr); + m_pReinforcementInidcator->SetAutoDelete(true); - CUIXmlInit::InitStatic(uiXml, "reinforcement", 0, m_pReinforcementInidcator); m_team1_icon = xr_new("Team 1 icon"); m_team2_icon = xr_new("Team 2 icon"); @@ -626,8 +628,13 @@ s8 CUIGameCTA::GetSelectedSkinIndex() void CUIGameCTA::SetReinforcementTimes(u32 curTime, u32 maxTime) { - string128 _buff; - m_pReinforcementInidcator->SetText(xr_itoa(curTime / 1000, _buff, 10)); + if (auto shape = smart_cast(m_pReinforcementInidcator)) + shape->SetPos(curTime / 1000, maxTime / 1000); + else + { + string128 _buff; + m_pReinforcementInidcator->SetText(xr_itoa(curTime / 1000, _buff, 10)); + } } void CUIGameCTA::DisplayMoneyChange(LPCSTR deltaMoney) { m_pMoneyIndicator->SetMoneyChange(deltaMoney); } diff --git a/src/xrGame/UIGameDM.cpp b/src/xrGame/UIGameDM.cpp index c28ecb1eaa0..8390484a5b0 100644 --- a/src/xrGame/UIGameDM.cpp +++ b/src/xrGame/UIGameDM.cpp @@ -69,7 +69,7 @@ void CUIGameDM::Init(int stage) m_pTeamPanels->Init(TEAM_PANELS_DM_XML_NAME, "team_panels_wnd"); CUIXml uiXml; uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "ui_game_dm.xml"); - CUIXmlInit::InitWindow(uiXml, "global", 0, Window); + CUIXmlInit::InitWindow(uiXml, "global", 0, Window, false); m_pMoneyIndicator->InitFromXML(uiXml); m_pRankIndicator->InitFromXml(uiXml); CUIXmlInit::InitStatic(uiXml, "fraglimit", 0, m_pFragLimitIndicator); diff --git a/src/xrGame/UIGameTDM.cpp b/src/xrGame/UIGameTDM.cpp index a369762a6a7..bbe0a6fc179 100644 --- a/src/xrGame/UIGameTDM.cpp +++ b/src/xrGame/UIGameTDM.cpp @@ -46,18 +46,22 @@ void CUIGameTDM::Init(int stage) { // unique m_pTeamPanels->Init(TEAM_PANELS_TDM_XML_NAME, "team_panels_wnd"); - CUIXml uiXml, xml2; + CUIXml uiXml, uiXml2; uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "ui_game_tdm.xml"); + uiXml2.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "ui_game_dm.xml"); // CS - CUIXmlInit::InitWindow(uiXml, "global", 0, Window); + CUIXmlInit::InitWindow(uiXml, "global", 0, Window, false); CUIXmlInit::InitStatic(uiXml, "team1_icon", 0, m_team1_icon); CUIXmlInit::InitStatic(uiXml, "team2_icon", 0, m_team2_icon); CUIXmlInit::InitStatic(uiXml, "team1_score", 0, m_team1_score); CUIXmlInit::InitStatic(uiXml, "team2_score", 0, m_team2_score); - CUIXmlInit::InitStatic(uiXml, "fraglimit", 0, m_pFragLimitIndicator); + if (!CUIXmlInit::InitStatic(uiXml, "fraglimit", 0, m_pFragLimitIndicator, false)) + CUIXmlInit::InitStatic(uiXml2, "fraglimit", 0, m_pFragLimitIndicator, false); // CS - m_pMoneyIndicator->InitFromXML(uiXml); - m_pRankIndicator->InitFromXml(uiXml); + if (!m_pMoneyIndicator->InitFromXML(uiXml)) + m_pMoneyIndicator->InitFromXML(uiXml2); // CS + if (!m_pRankIndicator->InitFromXml(uiXml)) + m_pRankIndicator->InitFromXml(uiXml2); // CS } if (stage == 2) { // after diff --git a/src/xrGame/game_cl_artefacthunt.cpp b/src/xrGame/game_cl_artefacthunt.cpp index a1ba5335aa1..150584463d2 100644 --- a/src/xrGame/game_cl_artefacthunt.cpp +++ b/src/xrGame/game_cl_artefacthunt.cpp @@ -443,11 +443,10 @@ void game_cl_ArtefactHunt::shedule_Update(u32 dt) else dTime = iCeil(float(dReinforcementTime - CurTime) / 1000); - string128 _buff; - m_game_ui->m_pReinforcementInidcator->SetText(xr_itoa(dTime, _buff, 10)); + m_game_ui->SetReinforcementTimes(dTime, iReinforcementTime); } else - m_game_ui->m_pReinforcementInidcator->SetText("0"); + m_game_ui->SetReinforcementTimes(0, 1); s16 lt = local_player->team; if (lt >= 0) diff --git a/src/xrGame/game_cl_capture_the_artefact.cpp b/src/xrGame/game_cl_capture_the_artefact.cpp index 15eb265ce45..51f04674f79 100644 --- a/src/xrGame/game_cl_capture_the_artefact.cpp +++ b/src/xrGame/game_cl_capture_the_artefact.cpp @@ -168,7 +168,7 @@ void game_cl_CaptureTheArtefact::shedule_Update(u32 dt) } if (InWarmUp()) { - m_game_ui->SetReinforcementTimes(0, 0); + m_game_ui->SetReinforcementTimes(0, 1); } else { diff --git a/src/xrGame/ui/UIMoneyIndicator.cpp b/src/xrGame/ui/UIMoneyIndicator.cpp index 746e01caf70..cf9dedeac65 100644 --- a/src/xrGame/ui/UIMoneyIndicator.cpp +++ b/src/xrGame/ui/UIMoneyIndicator.cpp @@ -14,20 +14,22 @@ CUIMoneyIndicator::CUIMoneyIndicator() m_pBonusMoney->SetAutoDelete(true); } -void CUIMoneyIndicator::InitFromXML(CUIXml& xml_doc) +bool CUIMoneyIndicator::InitFromXML(CUIXml& xml_doc) { - CUIXmlInit::InitWindow(xml_doc, "money_wnd", 0, this); + if (!CUIXmlInit::InitWindow(xml_doc, "money_wnd", 0, this, false)) + return false; CUIXmlInit::InitStatic(xml_doc, "money_wnd:money_indicator", 0, &m_back); CUIXmlInit::InitStatic(xml_doc, "money_wnd:money_indicator:total_money", 0, &m_money_amount); CUIXmlInit::InitStatic(xml_doc, "money_wnd:money_change", 0, &m_money_change); CUIXmlInit::InitScrollView(xml_doc, "money_wnd:money_bonus_list", 0, m_pBonusMoney); - CGameFont* pF; - u32 color; + CGameFont* pF{}; + u32 color{ u32(-1) }; CUIXmlInit::InitFont(xml_doc, "money_wnd:money_bonus_list:font", 0, color, pF); m_pBonusMoney->SetTextAtrib(pF, color); m_money_change.SetVisible(false); m_money_change.SetColorAnimation("ui_mp_chat", LA_ONLYALPHA | LA_TEXTCOLOR); + return true; } void CUIMoneyIndicator::SetMoneyAmount(pcstr money) { m_money_amount.SetText(money); } diff --git a/src/xrGame/ui/UIMoneyIndicator.h b/src/xrGame/ui/UIMoneyIndicator.h index 1b33a5837c3..2ac59bec41e 100644 --- a/src/xrGame/ui/UIMoneyIndicator.h +++ b/src/xrGame/ui/UIMoneyIndicator.h @@ -10,7 +10,7 @@ class CUIMoneyIndicator final : public CUIWindow { public: CUIMoneyIndicator(); - void InitFromXML(CUIXml& xml_doc); + bool InitFromXML(CUIXml& xml_doc); void SetMoneyAmount(pcstr money); void SetMoneyChange(pcstr money); void AddBonusMoney(KillMessageStruct& msg); diff --git a/src/xrGame/ui/UIRankIndicator.cpp b/src/xrGame/ui/UIRankIndicator.cpp index e5d652021b0..ccd86cb4336 100644 --- a/src/xrGame/ui/UIRankIndicator.cpp +++ b/src/xrGame/ui/UIRankIndicator.cpp @@ -12,9 +12,10 @@ CUIRankIndicator::~CUIRankIndicator() xr_delete(m_ranks[i]); } -void CUIRankIndicator::InitFromXml(CUIXml& xml_doc) +bool CUIRankIndicator::InitFromXml(CUIXml& xml_doc) { - CUIXmlInit::InitWindow(xml_doc, "rank_wnd", 0, this); + if (!CUIXmlInit::InitWindow(xml_doc, "rank_wnd", 0, this, false)) + return false; string256 str; for (u8 i = 0; i < max_rank; ++i) { @@ -27,6 +28,7 @@ void CUIRankIndicator::InitFromXml(CUIXml& xml_doc) back->SetAutoDelete(true); CUIXmlInit::InitStatic(xml_doc, "rank_wnd:background", 0, back); AttachChild(back); + return true; } void CUIRankIndicator::SetRank(u8 team, u8 rank) diff --git a/src/xrGame/ui/UIRankIndicator.h b/src/xrGame/ui/UIRankIndicator.h index d89d908a721..acbe84fe9f1 100644 --- a/src/xrGame/ui/UIRankIndicator.h +++ b/src/xrGame/ui/UIRankIndicator.h @@ -18,7 +18,7 @@ class CUIRankIndicator final : public CUIWindow CUIRankIndicator(); ~CUIRankIndicator() override; - void InitFromXml(CUIXml& xml_doc); + bool InitFromXml(CUIXml& xml_doc); void SetRank(u8 team, u8 rank); pcstr GetDebugType() override { return "CUIRankIndicator"; } diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index a193b1ec2d7..37f66c8c81c 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -564,6 +564,9 @@ bool CUIXmlInitBase::InitProgressShape(CUIXml& xml_doc, pcstr path, int index, C InitStatic(xml_doc, _path, index, pWnd->m_pTexture); } + if (!pWnd->m_pTexture && !pWnd->GetShader()->inited()) + return false; // Can't work without a texture + pWnd->m_sectorCount = xml_doc.ReadAttribInt(path, index, "sector_count", 8); pWnd->m_bClockwise = xml_doc.ReadAttribInt(path, index, "clockwise") ? true : false; From b22db624d22f4f9e34e58b49928bfb79893da523 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sat, 27 Dec 2025 23:16:43 +0300 Subject: [PATCH 132/261] Make CS multiplayer UI generally work(#382) Only thing that's missing are captions from ui_custom_msgs.xml, and I'm hesitant to make workarounds for this. --- src/xrGame/UIGameAHunt.cpp | 32 ++++++++++++++++----- src/xrGame/UIGameAHunt.h | 5 +++- src/xrGame/UIGameCTA.cpp | 17 +++++++---- src/xrGame/UIGameDM.cpp | 2 +- src/xrGame/UIGameTDM.cpp | 14 +++++---- src/xrGame/game_cl_artefacthunt.cpp | 5 ++-- src/xrGame/game_cl_capture_the_artefact.cpp | 2 +- src/xrGame/ui/UIMoneyIndicator.cpp | 10 ++++--- src/xrGame/ui/UIMoneyIndicator.h | 2 +- src/xrGame/ui/UIRankIndicator.cpp | 6 ++-- src/xrGame/ui/UIRankIndicator.h | 2 +- src/xrUICore/XML/UIXmlInitBase.cpp | 3 ++ 12 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/xrGame/UIGameAHunt.cpp b/src/xrGame/UIGameAHunt.cpp index b2297f08313..9a284e90a11 100644 --- a/src/xrGame/UIGameAHunt.cpp +++ b/src/xrGame/UIGameAHunt.cpp @@ -5,6 +5,7 @@ #include "Level.h" #include "game_cl_artefacthunt.h" #include "xrUICore/Static/UIStatic.h" +#include "xrUICore/ProgressBar/UIProgressShape.h" #include "ui/UIXmlInit.h" #include "ui/UIMessageBoxEx.h" #include "ui/UIMoneyIndicator.h" @@ -27,23 +28,29 @@ void CUIGameAHunt::Init(int stage) { // unique m_pTeamPanels->Init(TEAM_PANELS_AHUNT_XML_NAME, "team_panels_wnd"); - CUIXml uiXml; + CUIXml uiXml, uiXml2; uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "ui_game_ahunt.xml"); + uiXml2.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "ui_game_dm.xml"); // CS - CUIXmlInit::InitWindow(uiXml, "global", 0, Window); - CUIXmlInit::InitStatic(uiXml, "fraglimit", 0, m_pFragLimitIndicator); + CUIXmlInit::InitWindow(uiXml, "global", 0, Window, false); - m_pReinforcementInidcator = xr_new("Reinforcement indicator"); + m_pReinforcementInidcator = UIHelper::CreateProgressShape(uiXml, "reinforcement", nullptr, false); // CS + if (!m_pReinforcementInidcator) + m_pReinforcementInidcator = UIHelper::CreateStatic(uiXml, "reinforcement", nullptr); m_pReinforcementInidcator->SetAutoDelete(true); - CUIXmlInit::InitStatic(uiXml, "reinforcement", 0, m_pReinforcementInidcator); + + if (!CUIXmlInit::InitStatic(uiXml, "fraglimit", 0, m_pFragLimitIndicator, false)) + CUIXmlInit::InitStatic(uiXml2, "fraglimit", 0, m_pFragLimitIndicator, false); // CS CUIXmlInit::InitStatic(uiXml, "team1_icon", 0, m_team1_icon); CUIXmlInit::InitStatic(uiXml, "team2_icon", 0, m_team2_icon); CUIXmlInit::InitStatic(uiXml, "team1_score", 0, m_team1_score); CUIXmlInit::InitStatic(uiXml, "team2_score", 0, m_team2_score); - m_pMoneyIndicator->InitFromXML(uiXml); - m_pRankIndicator->InitFromXml(uiXml); + if (!m_pMoneyIndicator->InitFromXML(uiXml)) + m_pMoneyIndicator->InitFromXML(uiXml2); // CS + if (!m_pRankIndicator->InitFromXml(uiXml)) + m_pRankIndicator->InitFromXml(uiXml2); // CS } if (stage == 2) { // after @@ -72,3 +79,14 @@ void CUIGameAHunt::SetClGame(game_cl_GameState* g) } void CUIGameAHunt::SetBuyMsgCaption(LPCSTR str) { m_buy_msg_caption->SetTextST(str); } + +void CUIGameAHunt::SetReinforcementTimes(int curTime, int maxTime) +{ + if (auto shape = smart_cast(m_pReinforcementInidcator)) + shape->SetPos(curTime, maxTime); + else + { + string128 _buff; + m_pReinforcementInidcator->SetText(xr_itoa(curTime, _buff, 10)); + } +} diff --git a/src/xrGame/UIGameAHunt.h b/src/xrGame/UIGameAHunt.h index 11bc150be3e..d43e982ca78 100644 --- a/src/xrGame/UIGameAHunt.h +++ b/src/xrGame/UIGameAHunt.h @@ -18,8 +18,9 @@ class CUIGameAHunt : public CUIGameTDM game_cl_ArtefactHunt* m_game; typedef CUIGameTDM inherited; -public: CUIStatic* m_pReinforcementInidcator; + +public: CUIMessageBoxEx* m_pBuySpawnMsgBox; public: @@ -31,6 +32,8 @@ class CUIGameAHunt : public CUIGameTDM virtual void UnLoad(); void SetBuyMsgCaption(LPCSTR str); + void SetReinforcementTimes(int curTime, int maxTime); + protected: CUIStatic* m_buy_msg_caption; }; diff --git a/src/xrGame/UIGameCTA.cpp b/src/xrGame/UIGameCTA.cpp index e111ea00f6a..3b1266ffc92 100644 --- a/src/xrGame/UIGameCTA.cpp +++ b/src/xrGame/UIGameCTA.cpp @@ -63,7 +63,7 @@ void CUIGameCTA::Init(int stage) CUIXml uiXml; uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, CTA_GAME_WND_XML); - CUIXmlInit::InitWindow(uiXml, "global", 0, Window); + CUIXmlInit::InitWindow(uiXml, "global", 0, Window, false); m_pMoneyIndicator = xr_new(); m_pMoneyIndicator->SetAutoDelete(true); @@ -73,9 +73,11 @@ void CUIGameCTA::Init(int stage) m_pRankIndicator->SetAutoDelete(true); m_pRankIndicator->InitFromXml(uiXml); - m_pReinforcementInidcator = xr_new("Reinforcement indicator"); + m_pReinforcementInidcator = UIHelper::CreateProgressShape(uiXml, "reinforcement", nullptr, false); + if (!m_pReinforcementInidcator) + m_pReinforcementInidcator = UIHelper::CreateStatic(uiXml, "reinforcement", nullptr); + m_pReinforcementInidcator->SetAutoDelete(true); - CUIXmlInit::InitStatic(uiXml, "reinforcement", 0, m_pReinforcementInidcator); m_team1_icon = xr_new("Team 1 icon"); m_team2_icon = xr_new("Team 2 icon"); @@ -626,8 +628,13 @@ s8 CUIGameCTA::GetSelectedSkinIndex() void CUIGameCTA::SetReinforcementTimes(u32 curTime, u32 maxTime) { - string128 _buff; - m_pReinforcementInidcator->SetText(xr_itoa(curTime / 1000, _buff, 10)); + if (auto shape = smart_cast(m_pReinforcementInidcator)) + shape->SetPos(curTime / 1000, maxTime / 1000); + else + { + string128 _buff; + m_pReinforcementInidcator->SetText(xr_itoa(curTime / 1000, _buff, 10)); + } } void CUIGameCTA::DisplayMoneyChange(LPCSTR deltaMoney) { m_pMoneyIndicator->SetMoneyChange(deltaMoney); } diff --git a/src/xrGame/UIGameDM.cpp b/src/xrGame/UIGameDM.cpp index c28ecb1eaa0..8390484a5b0 100644 --- a/src/xrGame/UIGameDM.cpp +++ b/src/xrGame/UIGameDM.cpp @@ -69,7 +69,7 @@ void CUIGameDM::Init(int stage) m_pTeamPanels->Init(TEAM_PANELS_DM_XML_NAME, "team_panels_wnd"); CUIXml uiXml; uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "ui_game_dm.xml"); - CUIXmlInit::InitWindow(uiXml, "global", 0, Window); + CUIXmlInit::InitWindow(uiXml, "global", 0, Window, false); m_pMoneyIndicator->InitFromXML(uiXml); m_pRankIndicator->InitFromXml(uiXml); CUIXmlInit::InitStatic(uiXml, "fraglimit", 0, m_pFragLimitIndicator); diff --git a/src/xrGame/UIGameTDM.cpp b/src/xrGame/UIGameTDM.cpp index a369762a6a7..bbe0a6fc179 100644 --- a/src/xrGame/UIGameTDM.cpp +++ b/src/xrGame/UIGameTDM.cpp @@ -46,18 +46,22 @@ void CUIGameTDM::Init(int stage) { // unique m_pTeamPanels->Init(TEAM_PANELS_TDM_XML_NAME, "team_panels_wnd"); - CUIXml uiXml, xml2; + CUIXml uiXml, uiXml2; uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "ui_game_tdm.xml"); + uiXml2.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "ui_game_dm.xml"); // CS - CUIXmlInit::InitWindow(uiXml, "global", 0, Window); + CUIXmlInit::InitWindow(uiXml, "global", 0, Window, false); CUIXmlInit::InitStatic(uiXml, "team1_icon", 0, m_team1_icon); CUIXmlInit::InitStatic(uiXml, "team2_icon", 0, m_team2_icon); CUIXmlInit::InitStatic(uiXml, "team1_score", 0, m_team1_score); CUIXmlInit::InitStatic(uiXml, "team2_score", 0, m_team2_score); - CUIXmlInit::InitStatic(uiXml, "fraglimit", 0, m_pFragLimitIndicator); + if (!CUIXmlInit::InitStatic(uiXml, "fraglimit", 0, m_pFragLimitIndicator, false)) + CUIXmlInit::InitStatic(uiXml2, "fraglimit", 0, m_pFragLimitIndicator, false); // CS - m_pMoneyIndicator->InitFromXML(uiXml); - m_pRankIndicator->InitFromXml(uiXml); + if (!m_pMoneyIndicator->InitFromXML(uiXml)) + m_pMoneyIndicator->InitFromXML(uiXml2); // CS + if (!m_pRankIndicator->InitFromXml(uiXml)) + m_pRankIndicator->InitFromXml(uiXml2); // CS } if (stage == 2) { // after diff --git a/src/xrGame/game_cl_artefacthunt.cpp b/src/xrGame/game_cl_artefacthunt.cpp index a1ba5335aa1..150584463d2 100644 --- a/src/xrGame/game_cl_artefacthunt.cpp +++ b/src/xrGame/game_cl_artefacthunt.cpp @@ -443,11 +443,10 @@ void game_cl_ArtefactHunt::shedule_Update(u32 dt) else dTime = iCeil(float(dReinforcementTime - CurTime) / 1000); - string128 _buff; - m_game_ui->m_pReinforcementInidcator->SetText(xr_itoa(dTime, _buff, 10)); + m_game_ui->SetReinforcementTimes(dTime, iReinforcementTime); } else - m_game_ui->m_pReinforcementInidcator->SetText("0"); + m_game_ui->SetReinforcementTimes(0, 1); s16 lt = local_player->team; if (lt >= 0) diff --git a/src/xrGame/game_cl_capture_the_artefact.cpp b/src/xrGame/game_cl_capture_the_artefact.cpp index 15eb265ce45..51f04674f79 100644 --- a/src/xrGame/game_cl_capture_the_artefact.cpp +++ b/src/xrGame/game_cl_capture_the_artefact.cpp @@ -168,7 +168,7 @@ void game_cl_CaptureTheArtefact::shedule_Update(u32 dt) } if (InWarmUp()) { - m_game_ui->SetReinforcementTimes(0, 0); + m_game_ui->SetReinforcementTimes(0, 1); } else { diff --git a/src/xrGame/ui/UIMoneyIndicator.cpp b/src/xrGame/ui/UIMoneyIndicator.cpp index 746e01caf70..cf9dedeac65 100644 --- a/src/xrGame/ui/UIMoneyIndicator.cpp +++ b/src/xrGame/ui/UIMoneyIndicator.cpp @@ -14,20 +14,22 @@ CUIMoneyIndicator::CUIMoneyIndicator() m_pBonusMoney->SetAutoDelete(true); } -void CUIMoneyIndicator::InitFromXML(CUIXml& xml_doc) +bool CUIMoneyIndicator::InitFromXML(CUIXml& xml_doc) { - CUIXmlInit::InitWindow(xml_doc, "money_wnd", 0, this); + if (!CUIXmlInit::InitWindow(xml_doc, "money_wnd", 0, this, false)) + return false; CUIXmlInit::InitStatic(xml_doc, "money_wnd:money_indicator", 0, &m_back); CUIXmlInit::InitStatic(xml_doc, "money_wnd:money_indicator:total_money", 0, &m_money_amount); CUIXmlInit::InitStatic(xml_doc, "money_wnd:money_change", 0, &m_money_change); CUIXmlInit::InitScrollView(xml_doc, "money_wnd:money_bonus_list", 0, m_pBonusMoney); - CGameFont* pF; - u32 color; + CGameFont* pF{}; + u32 color{ u32(-1) }; CUIXmlInit::InitFont(xml_doc, "money_wnd:money_bonus_list:font", 0, color, pF); m_pBonusMoney->SetTextAtrib(pF, color); m_money_change.SetVisible(false); m_money_change.SetColorAnimation("ui_mp_chat", LA_ONLYALPHA | LA_TEXTCOLOR); + return true; } void CUIMoneyIndicator::SetMoneyAmount(pcstr money) { m_money_amount.SetText(money); } diff --git a/src/xrGame/ui/UIMoneyIndicator.h b/src/xrGame/ui/UIMoneyIndicator.h index 1b33a5837c3..2ac59bec41e 100644 --- a/src/xrGame/ui/UIMoneyIndicator.h +++ b/src/xrGame/ui/UIMoneyIndicator.h @@ -10,7 +10,7 @@ class CUIMoneyIndicator final : public CUIWindow { public: CUIMoneyIndicator(); - void InitFromXML(CUIXml& xml_doc); + bool InitFromXML(CUIXml& xml_doc); void SetMoneyAmount(pcstr money); void SetMoneyChange(pcstr money); void AddBonusMoney(KillMessageStruct& msg); diff --git a/src/xrGame/ui/UIRankIndicator.cpp b/src/xrGame/ui/UIRankIndicator.cpp index e5d652021b0..ccd86cb4336 100644 --- a/src/xrGame/ui/UIRankIndicator.cpp +++ b/src/xrGame/ui/UIRankIndicator.cpp @@ -12,9 +12,10 @@ CUIRankIndicator::~CUIRankIndicator() xr_delete(m_ranks[i]); } -void CUIRankIndicator::InitFromXml(CUIXml& xml_doc) +bool CUIRankIndicator::InitFromXml(CUIXml& xml_doc) { - CUIXmlInit::InitWindow(xml_doc, "rank_wnd", 0, this); + if (!CUIXmlInit::InitWindow(xml_doc, "rank_wnd", 0, this, false)) + return false; string256 str; for (u8 i = 0; i < max_rank; ++i) { @@ -27,6 +28,7 @@ void CUIRankIndicator::InitFromXml(CUIXml& xml_doc) back->SetAutoDelete(true); CUIXmlInit::InitStatic(xml_doc, "rank_wnd:background", 0, back); AttachChild(back); + return true; } void CUIRankIndicator::SetRank(u8 team, u8 rank) diff --git a/src/xrGame/ui/UIRankIndicator.h b/src/xrGame/ui/UIRankIndicator.h index d89d908a721..acbe84fe9f1 100644 --- a/src/xrGame/ui/UIRankIndicator.h +++ b/src/xrGame/ui/UIRankIndicator.h @@ -18,7 +18,7 @@ class CUIRankIndicator final : public CUIWindow CUIRankIndicator(); ~CUIRankIndicator() override; - void InitFromXml(CUIXml& xml_doc); + bool InitFromXml(CUIXml& xml_doc); void SetRank(u8 team, u8 rank); pcstr GetDebugType() override { return "CUIRankIndicator"; } diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index a193b1ec2d7..37f66c8c81c 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -564,6 +564,9 @@ bool CUIXmlInitBase::InitProgressShape(CUIXml& xml_doc, pcstr path, int index, C InitStatic(xml_doc, _path, index, pWnd->m_pTexture); } + if (!pWnd->m_pTexture && !pWnd->GetShader()->inited()) + return false; // Can't work without a texture + pWnd->m_sectorCount = xml_doc.ReadAttribInt(path, index, "sector_count", 8); pWnd->m_bClockwise = xml_doc.ReadAttribInt(path, index, "clockwise") ? true : false; From 7e27cb0e63f1455f454f6338cf1bb2b7817beffb Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sun, 28 Dec 2025 01:09:07 +0300 Subject: [PATCH 133/261] Fix crash when disconnecting from multiplayer Was introduced in a914f65531becd5e0b42fa5979fcd5ef78ecdd57 --- src/xrGame/ui/UIStatsIcon.cpp | 2 ++ src/xrGame/ui/UIStatsIcon.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/xrGame/ui/UIStatsIcon.cpp b/src/xrGame/ui/UIStatsIcon.cpp index bb1ce419449..57a23f67548 100644 --- a/src/xrGame/ui/UIStatsIcon.cpp +++ b/src/xrGame/ui/UIStatsIcon.cpp @@ -56,6 +56,8 @@ void CUIStatsIcon::InitTexInfo() void CUIStatsIcon::FreeTexInfo() { + if (!m_tex_info) + return; // ranks for (int i = RANK_0; i <= RANK_4; i++) { diff --git a/src/xrGame/ui/UIStatsIcon.h b/src/xrGame/ui/UIStatsIcon.h index bcf6f6714dd..781c4b47933 100644 --- a/src/xrGame/ui/UIStatsIcon.h +++ b/src/xrGame/ui/UIStatsIcon.h @@ -37,5 +37,5 @@ class CUIStatsIcon final : public CUIStatic using tex_info_data = std::array; - inline static tex_info_data* m_tex_info; + inline static tex_info_data* m_tex_info{}; }; From 3e05d60455390f93b569854c970a46507afeb189 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sun, 28 Dec 2025 01:09:07 +0300 Subject: [PATCH 134/261] Fix crash when disconnecting from multiplayer Was introduced in a914f65531becd5e0b42fa5979fcd5ef78ecdd57 --- src/xrGame/ui/UIStatsIcon.cpp | 2 ++ src/xrGame/ui/UIStatsIcon.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/xrGame/ui/UIStatsIcon.cpp b/src/xrGame/ui/UIStatsIcon.cpp index bb1ce419449..57a23f67548 100644 --- a/src/xrGame/ui/UIStatsIcon.cpp +++ b/src/xrGame/ui/UIStatsIcon.cpp @@ -56,6 +56,8 @@ void CUIStatsIcon::InitTexInfo() void CUIStatsIcon::FreeTexInfo() { + if (!m_tex_info) + return; // ranks for (int i = RANK_0; i <= RANK_4; i++) { diff --git a/src/xrGame/ui/UIStatsIcon.h b/src/xrGame/ui/UIStatsIcon.h index bcf6f6714dd..781c4b47933 100644 --- a/src/xrGame/ui/UIStatsIcon.h +++ b/src/xrGame/ui/UIStatsIcon.h @@ -37,5 +37,5 @@ class CUIStatsIcon final : public CUIStatic using tex_info_data = std::array; - inline static tex_info_data* m_tex_info; + inline static tex_info_data* m_tex_info{}; }; From 7311f627fff3bced23087b37e705407622778ec9 Mon Sep 17 00:00:00 2001 From: Sultan Luchezarniy Date: Mon, 29 Dec 2025 00:40:31 +0300 Subject: [PATCH 135/261] xrUICore/Windows/UIFrameLineWnd.cpp: fix build under GCC 10 and LCC --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index 0c429afa3f1..655760fd411 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -256,11 +256,10 @@ void CUIFrameLineWnd::FillDebugInfo() m_shader[segment]->GetBaseTextureResolution(ts); Frect& tex_rect = m_tex_rect[segment]; - Frect rect = - { - .lt = tex_rect.lt, - .rb = { tex_rect.width(), tex_rect.height() } - }; + Frect rect; + rect.lt = tex_rect.lt; + rect.rb = { tex_rect.width(), tex_rect.height() }; + if (ImGui::DragFloat4(label, reinterpret_cast(&rect), 1.0f, 0.0f, std::max(ts.x, ts.y))) { tex_rect.lt = rect.lt; From aa452d3340ffeaddc422dfb7dd9e510867e5d220 Mon Sep 17 00:00:00 2001 From: Sultan Luchezarniy Date: Mon, 29 Dec 2025 00:40:31 +0300 Subject: [PATCH 136/261] xrUICore/Windows/UIFrameLineWnd.cpp: fix build under GCC 10 and LCC --- src/xrUICore/Windows/UIFrameLineWnd.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/xrUICore/Windows/UIFrameLineWnd.cpp b/src/xrUICore/Windows/UIFrameLineWnd.cpp index 0c429afa3f1..655760fd411 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.cpp +++ b/src/xrUICore/Windows/UIFrameLineWnd.cpp @@ -256,11 +256,10 @@ void CUIFrameLineWnd::FillDebugInfo() m_shader[segment]->GetBaseTextureResolution(ts); Frect& tex_rect = m_tex_rect[segment]; - Frect rect = - { - .lt = tex_rect.lt, - .rb = { tex_rect.width(), tex_rect.height() } - }; + Frect rect; + rect.lt = tex_rect.lt; + rect.rb = { tex_rect.width(), tex_rect.height() }; + if (ImGui::DragFloat4(label, reinterpret_cast(&rect), 1.0f, 0.0f, std::max(ts.x, ts.y))) { tex_rect.lt = rect.lt; From e4d621c397aa55b4fe87bcde308009ab2e87b714 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sun, 28 Dec 2025 22:49:54 +0300 Subject: [PATCH 137/261] Return InitButton and InitAutoStaticGroup script functions (#382) --- src/xrGame/ScriptXMLInit.cpp | 12 ++++++++ src/xrUICore/XML/UIXmlInitBase.cpp | 45 ++++++++++++++++++++++++++++++ src/xrUICore/XML/UIXmlInitBase.h | 1 + 3 files changed, 58 insertions(+) diff --git a/src/xrGame/ScriptXMLInit.cpp b/src/xrGame/ScriptXMLInit.cpp index 1c90c2ee346..7de3d55ac36 100644 --- a/src/xrGame/ScriptXMLInit.cpp +++ b/src/xrGame/ScriptXMLInit.cpp @@ -289,6 +289,14 @@ void CScriptXmlInit::script_register(lua_State* luaState) .def("InitLabel", &CScriptXmlInit::InitStatic) .def("InitAnimStatic", &CScriptXmlInit::InitAnimStatic) .def("InitSleepStatic", &CScriptXmlInit::InitSleepStatic) + .def("InitButton", +[](CScriptXmlInit* self, pcstr path, CUIWindow* parent) + { + CUIButton* pWnd = xr_new(); + CUIXmlInit::InitButton(self->m_xml, path, 0, pWnd); + pWnd->SetAutoDelete(true); + _attach_child(pWnd, parent); + return pWnd; + }) .def("Init3tButton", &CScriptXmlInit::Init3tButton) .def("InitCheck", &CScriptXmlInit::InitCheck) .def("InitSpinNum", &CScriptXmlInit::InitSpinNum) @@ -307,6 +315,10 @@ void CScriptXmlInit::script_register(lua_State* luaState) .def("InitScrollView", &CScriptXmlInit::InitScrollView) .def("InitList", &CScriptXmlInit::InitListWnd) .def("InitListBox", &CScriptXmlInit::InitListBox) + .def("InitAutoStaticGroup", +[](CScriptXmlInit* self, pcstr path, CUIWindow* pWnd) + { + CUIXmlInit::InitAutoStaticGroup(self->m_xml, path, 0, pWnd); + }) .def("InitProgressBar", &CScriptXmlInit::InitProgressBar) ]; } diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 37f66c8c81c..09ef2c554d1 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -431,6 +431,51 @@ bool CUIXmlInitBase::InitSound(const CUIXml& xml_doc, pcstr path, int index, CUI return true; } +bool CUIXmlInitBase::InitButton(CUIXml& xml_doc, pcstr path, int index, CUIButton* pWnd) +{ + R_ASSERT4(xml_doc.NavigateToNode(path, index), "XML node not found", path, xml_doc.m_xml_file_name); + + InitStatic(xml_doc, path, index, pWnd); + + const auto checkAccelerator = [&](pcstr attrName, size_t accelIdx) + { + if (cpcstr accel = xml_doc.ReadAttrib(path, index, attrName, nullptr)) + { + const int acc = KeynameToDik(accel, true); + if (acc != SDL_SCANCODE_UNKNOWN) + pWnd->SetAccelerator(acc, true, accelIdx); + else + { + const auto action = ActionNameToId(accel, true); + if (action != kNOTBINDED) + pWnd->SetAccelerator(static_cast(action), false, accelIdx); + else + { + Msg("~ [%s] has wrong '%s' attribute value[%s] for button[%s] with index[%d] - can't find such button or action", + xml_doc.m_xml_file_name, attrName, accel, path, index); + } + } + } + }; + + checkAccelerator("accel", 0); + checkAccelerator("accel_ext", 1); + + if (cpcstr text_hint = xml_doc.ReadAttrib(path, index, "hint", nullptr)) + pWnd->m_hint_text = StringTable().translate(text_hint); + + //float shadowOffsetX = xml_doc.ReadAttribFlt(path, index, "shadow_offset_x", 0); + //float shadowOffsetY = xml_doc.ReadAttribFlt(path, index, "shadow_offset_y", 0); + + //float pushOffsetX = xml_doc.ReadAttribFlt(path, index, "push_off_x", 2); + //float pushOffsetY = xml_doc.ReadAttribFlt(path, index, "push_off_y", 3); + + //pWnd->SetShadowOffset({ shadowOffsetX, shadowOffsetY }); + //pWnd->SetPushOffset({ pushOffsetX, pushOffsetY }); + + return true; +} + bool CUIXmlInitBase::InitProgressBar(CUIXml& xml_doc, pcstr path, int index, CUIProgressBar* pWnd, bool fatal /*= true*/) { const bool nodeExist = xml_doc.NavigateToNode(path, index); diff --git a/src/xrUICore/XML/UIXmlInitBase.h b/src/xrUICore/XML/UIXmlInitBase.h index 4f4d16894f7..cce5618fec9 100644 --- a/src/xrUICore/XML/UIXmlInitBase.h +++ b/src/xrUICore/XML/UIXmlInitBase.h @@ -47,6 +47,7 @@ class XRUICORE_API CUIXmlInitBase static bool InitCheck(CUIXml& xml_doc, pcstr path, int index, CUICheckButton* pWnd, bool fatal = true); static bool InitSpin(CUIXml& xml_doc, pcstr path, int index, CUICustomSpin* pWnd, bool fatal = true); static bool InitText(CUIXml& xml_doc, pcstr path, int index, CUILines* pLines); + static bool InitButton(CUIXml& xml_doc, LPCSTR path, int index, CUIButton* pWnd); static bool Init3tButton(CUIXml& xml_doc, pcstr path, int index, CUI3tButton* pWnd, bool fatal = true); static bool InitProgressBar(CUIXml& xml_doc, pcstr path, int index, CUIProgressBar* pWnd, bool fatal = true); static bool InitProgressShape(CUIXml& xml_doc, pcstr path, int index, CUIProgressShape* pWnd, bool fatal = true); From 1b7e57924730a9f1416cbe26a217f461d482460c Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Sun, 28 Dec 2025 22:49:54 +0300 Subject: [PATCH 138/261] Return InitButton and InitAutoStaticGroup script functions (#382) --- src/xrGame/ScriptXMLInit.cpp | 12 ++++++++ src/xrUICore/XML/UIXmlInitBase.cpp | 45 ++++++++++++++++++++++++++++++ src/xrUICore/XML/UIXmlInitBase.h | 1 + 3 files changed, 58 insertions(+) diff --git a/src/xrGame/ScriptXMLInit.cpp b/src/xrGame/ScriptXMLInit.cpp index 1c90c2ee346..7de3d55ac36 100644 --- a/src/xrGame/ScriptXMLInit.cpp +++ b/src/xrGame/ScriptXMLInit.cpp @@ -289,6 +289,14 @@ void CScriptXmlInit::script_register(lua_State* luaState) .def("InitLabel", &CScriptXmlInit::InitStatic) .def("InitAnimStatic", &CScriptXmlInit::InitAnimStatic) .def("InitSleepStatic", &CScriptXmlInit::InitSleepStatic) + .def("InitButton", +[](CScriptXmlInit* self, pcstr path, CUIWindow* parent) + { + CUIButton* pWnd = xr_new(); + CUIXmlInit::InitButton(self->m_xml, path, 0, pWnd); + pWnd->SetAutoDelete(true); + _attach_child(pWnd, parent); + return pWnd; + }) .def("Init3tButton", &CScriptXmlInit::Init3tButton) .def("InitCheck", &CScriptXmlInit::InitCheck) .def("InitSpinNum", &CScriptXmlInit::InitSpinNum) @@ -307,6 +315,10 @@ void CScriptXmlInit::script_register(lua_State* luaState) .def("InitScrollView", &CScriptXmlInit::InitScrollView) .def("InitList", &CScriptXmlInit::InitListWnd) .def("InitListBox", &CScriptXmlInit::InitListBox) + .def("InitAutoStaticGroup", +[](CScriptXmlInit* self, pcstr path, CUIWindow* pWnd) + { + CUIXmlInit::InitAutoStaticGroup(self->m_xml, path, 0, pWnd); + }) .def("InitProgressBar", &CScriptXmlInit::InitProgressBar) ]; } diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 37f66c8c81c..09ef2c554d1 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -431,6 +431,51 @@ bool CUIXmlInitBase::InitSound(const CUIXml& xml_doc, pcstr path, int index, CUI return true; } +bool CUIXmlInitBase::InitButton(CUIXml& xml_doc, pcstr path, int index, CUIButton* pWnd) +{ + R_ASSERT4(xml_doc.NavigateToNode(path, index), "XML node not found", path, xml_doc.m_xml_file_name); + + InitStatic(xml_doc, path, index, pWnd); + + const auto checkAccelerator = [&](pcstr attrName, size_t accelIdx) + { + if (cpcstr accel = xml_doc.ReadAttrib(path, index, attrName, nullptr)) + { + const int acc = KeynameToDik(accel, true); + if (acc != SDL_SCANCODE_UNKNOWN) + pWnd->SetAccelerator(acc, true, accelIdx); + else + { + const auto action = ActionNameToId(accel, true); + if (action != kNOTBINDED) + pWnd->SetAccelerator(static_cast(action), false, accelIdx); + else + { + Msg("~ [%s] has wrong '%s' attribute value[%s] for button[%s] with index[%d] - can't find such button or action", + xml_doc.m_xml_file_name, attrName, accel, path, index); + } + } + } + }; + + checkAccelerator("accel", 0); + checkAccelerator("accel_ext", 1); + + if (cpcstr text_hint = xml_doc.ReadAttrib(path, index, "hint", nullptr)) + pWnd->m_hint_text = StringTable().translate(text_hint); + + //float shadowOffsetX = xml_doc.ReadAttribFlt(path, index, "shadow_offset_x", 0); + //float shadowOffsetY = xml_doc.ReadAttribFlt(path, index, "shadow_offset_y", 0); + + //float pushOffsetX = xml_doc.ReadAttribFlt(path, index, "push_off_x", 2); + //float pushOffsetY = xml_doc.ReadAttribFlt(path, index, "push_off_y", 3); + + //pWnd->SetShadowOffset({ shadowOffsetX, shadowOffsetY }); + //pWnd->SetPushOffset({ pushOffsetX, pushOffsetY }); + + return true; +} + bool CUIXmlInitBase::InitProgressBar(CUIXml& xml_doc, pcstr path, int index, CUIProgressBar* pWnd, bool fatal /*= true*/) { const bool nodeExist = xml_doc.NavigateToNode(path, index); diff --git a/src/xrUICore/XML/UIXmlInitBase.h b/src/xrUICore/XML/UIXmlInitBase.h index 4f4d16894f7..cce5618fec9 100644 --- a/src/xrUICore/XML/UIXmlInitBase.h +++ b/src/xrUICore/XML/UIXmlInitBase.h @@ -47,6 +47,7 @@ class XRUICORE_API CUIXmlInitBase static bool InitCheck(CUIXml& xml_doc, pcstr path, int index, CUICheckButton* pWnd, bool fatal = true); static bool InitSpin(CUIXml& xml_doc, pcstr path, int index, CUICustomSpin* pWnd, bool fatal = true); static bool InitText(CUIXml& xml_doc, pcstr path, int index, CUILines* pLines); + static bool InitButton(CUIXml& xml_doc, LPCSTR path, int index, CUIButton* pWnd); static bool Init3tButton(CUIXml& xml_doc, pcstr path, int index, CUI3tButton* pWnd, bool fatal = true); static bool InitProgressBar(CUIXml& xml_doc, pcstr path, int index, CUIProgressBar* pWnd, bool fatal = true); static bool InitProgressShape(CUIXml& xml_doc, pcstr path, int index, CUIProgressShape* pWnd, bool fatal = true); From 1e694dd4cd145733604a1841233255c34c8102ee Mon Sep 17 00:00:00 2001 From: Sultan Luchezarniy Date: Mon, 29 Dec 2025 10:46:14 +0300 Subject: [PATCH 139/261] README.md: add me, and new financial supporters I'll try my best to describe other people's contributions to the project as well. --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7830c1125b8..616505d2ac1 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Thank you for your support! * [andrew-boyarshin](https://github.com/andrew-boyarshin) – work on the build system. * [CasualDev242](https://github.com/CasualDev242) (Swartz27) – work on renderer features. * [awdavies](https://github.com/awdavies) – project maintenance work. - * [Xottab_DUTY](https://github.com/Xottab-DUTY) – current project leader. + * [Xottab_DUTY](https://github.com/Xottab-DUTY) – current project leader, gathering a new team, creation of the community (GitHub, Discord, VK), defining project guiding principles and goals, working on many areas of tasks (core, renderering, AI, gameplay, UI), SOC/CS/COC support. * [intorr](https://github.com/intorr) – work on the project quality, memory leaks, refactoring and optimizations. * [eagleivg](https://github.com/eagleivg) – main part of the work on Linux port. * [q4a](https://github.com/q4a) – main part of the work on Linux port. @@ -174,7 +174,8 @@ Thank you for your support! * Valevicor, Nac, Midiy, Vadim Balashov, Jacob Arms, * CatWMuttonChops, Reed777, Interpreter_, nexusasx10, [Egor Olefirenko](https://github.com/olefirenque), * Igor Zharenko, SLF, Dmitriy Terletskiy, Alex Brodskiy, Neizvestniy Chelovek, - * LinuxNerd, [tyabus](https://github.com/tyabus), [Sevenfortyseven](https://github.com/Sevenfortyseven), 777yur0k, ItzVladik + * LinuxNerd, [tyabus](https://github.com/tyabus), [Sevenfortyseven](https://github.com/Sevenfortyseven), 777yur0k, ItzVladik, + * @psistore, @forealdo25, Tech Racoon * Particular projects and their contributors: * [Oxygen](https://github.com/xrOxygen) – for being our friends and giving tips and help with new features, optimizations, bug fixes, etc. * [Shoker Weapon Mod](https://github.com/ShokerStlk/xray-16-SWM) – for first introducing 3D (PiP) scopes and implementing new features to overall improve the weaponry of the game. From d56d8fe00821eec1e58a9d973654c5f187be1bb9 Mon Sep 17 00:00:00 2001 From: Sultan Luchezarniy Date: Mon, 29 Dec 2025 10:46:14 +0300 Subject: [PATCH 140/261] README.md: add me, and new financial supporters I'll try my best to describe other people's contributions to the project as well. --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7830c1125b8..616505d2ac1 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Thank you for your support! * [andrew-boyarshin](https://github.com/andrew-boyarshin) – work on the build system. * [CasualDev242](https://github.com/CasualDev242) (Swartz27) – work on renderer features. * [awdavies](https://github.com/awdavies) – project maintenance work. - * [Xottab_DUTY](https://github.com/Xottab-DUTY) – current project leader. + * [Xottab_DUTY](https://github.com/Xottab-DUTY) – current project leader, gathering a new team, creation of the community (GitHub, Discord, VK), defining project guiding principles and goals, working on many areas of tasks (core, renderering, AI, gameplay, UI), SOC/CS/COC support. * [intorr](https://github.com/intorr) – work on the project quality, memory leaks, refactoring and optimizations. * [eagleivg](https://github.com/eagleivg) – main part of the work on Linux port. * [q4a](https://github.com/q4a) – main part of the work on Linux port. @@ -174,7 +174,8 @@ Thank you for your support! * Valevicor, Nac, Midiy, Vadim Balashov, Jacob Arms, * CatWMuttonChops, Reed777, Interpreter_, nexusasx10, [Egor Olefirenko](https://github.com/olefirenque), * Igor Zharenko, SLF, Dmitriy Terletskiy, Alex Brodskiy, Neizvestniy Chelovek, - * LinuxNerd, [tyabus](https://github.com/tyabus), [Sevenfortyseven](https://github.com/Sevenfortyseven), 777yur0k, ItzVladik + * LinuxNerd, [tyabus](https://github.com/tyabus), [Sevenfortyseven](https://github.com/Sevenfortyseven), 777yur0k, ItzVladik, + * @psistore, @forealdo25, Tech Racoon * Particular projects and their contributors: * [Oxygen](https://github.com/xrOxygen) – for being our friends and giving tips and help with new features, optimizations, bug fixes, etc. * [Shoker Weapon Mod](https://github.com/ShokerStlk/xray-16-SWM) – for first introducing 3D (PiP) scopes and implementing new features to overall improve the weaponry of the game. From a34f0d1c513bbd8b8d758bb460335b2d10613cce Mon Sep 17 00:00:00 2001 From: Rybaev Ilya Mikhailovich <61088459+Aardes-Roiven@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:16:43 +0300 Subject: [PATCH 141/261] Replace shared_str::printf with standardized xr_sprintf (#1948) (#2022) --- src/Layers/xrRender/ETextureParams.cpp | 10 ++++++++-- src/xrCore/xrstring.h | 25 ++++++++++++------------- src/xrGame/game_cl_mp.cpp | 3 +-- src/xrGame/game_cl_mp_messages_menu.cpp | 4 ++-- src/xrGame/game_sv_mp.cpp | 12 ++++++------ src/xrGame/ui/ServerList.cpp | 4 ++-- src/xrGame/ui/UIActorMenuInventory.cpp | 12 ++++++------ src/xrGame/ui/UIPdaWnd.cpp | 2 +- 8 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/Layers/xrRender/ETextureParams.cpp b/src/Layers/xrRender/ETextureParams.cpp index efbae4f8a21..c31dacc97f3 100644 --- a/src/Layers/xrRender/ETextureParams.cpp +++ b/src/Layers/xrRender/ETextureParams.cpp @@ -161,8 +161,14 @@ void STextureParams::FillProp(LPCSTR base_name, PropItemVec& items, PropValue::T OnTypeChangeEvent = on_type_change; PropValue* P = PHelper().CreateToken32(items, "Type", (u32*)&type, ttype_token); P->OnChangeEvent.bind(this, &STextureParams::OnTypeChange); - PHelper().CreateCaption(items, "Source" DELIMITER "Width", shared_str().printf("%d", width)); - PHelper().CreateCaption(items, "Source" DELIMITER "Height", shared_str().printf("%d", height)); + + shared_str temp_str; + xr_sprintf(temp_str, "%d", width); + PHelper().CreateCaption(items, "Source" DELIMITER "Width", temp_str); + + xr_sprintf(temp_str, "%d", height); + PHelper().CreateCaption(items, "Source" DELIMITER "Height", v); + PHelper().CreateCaption(items, "Source" DELIMITER "Alpha", HasAlpha() ? "present" : "absent"); switch (type) { diff --git a/src/xrCore/xrstring.h b/src/xrCore/xrstring.h index dc08b318729..a03396575a4 100644 --- a/src/xrCore/xrstring.h +++ b/src/xrCore/xrstring.h @@ -176,21 +176,20 @@ class shared_str [[nodiscard]] bool equal(const shared_str& rhs) const { return (p_ == rhs.p_); } - - shared_str& __cdecl printf(const char* format, ...) - { - string4096 buf; - va_list p; - va_start(p, format); - int vs_sz = vsnprintf(buf, sizeof(buf) - 1, format, p); - buf[sizeof(buf) - 1] = 0; - va_end(p); - if (vs_sz) - _set(buf); - return (shared_str&)*this; - } }; +inline int __cdecl xr_sprintf(shared_str& destination, pcstr format_string, ...) +{ + string4096 buf; + va_list args; + va_start(args, format_string); + const int vs_sz = vsnprintf(buf, sizeof(buf) - 1, format_string, args); + buf[sizeof(buf) - 1] = 0; + va_end(args); + if (vs_sz >= 0) + destination = buf; + return vs_sz; +} template<> struct std::hash diff --git a/src/xrGame/game_cl_mp.cpp b/src/xrGame/game_cl_mp.cpp index 107ca9e52c9..231dc63208b 100644 --- a/src/xrGame/game_cl_mp.cpp +++ b/src/xrGame/game_cl_mp.cpp @@ -1244,8 +1244,7 @@ void game_cl_mp::OnEventMoneyChanged(NET_Packet& P) } case SKT_KIR: { - BName.printf("%d_kill_in_row", BonusKills); - + xr_sprintf(BName, "%d_kill_in_row", BonusKills); xr_sprintf(MoneyStr, sizeof(MoneyStr), "%d", BonusKills); BMS.m_killer.m_name = MoneyStr; BMS.m_killer.m_color = 0xffff0000; diff --git a/src/xrGame/game_cl_mp_messages_menu.cpp b/src/xrGame/game_cl_mp_messages_menu.cpp index cdb46954a24..d307ab6a2a3 100644 --- a/src/xrGame/game_cl_mp_messages_menu.cpp +++ b/src/xrGame/game_cl_mp_messages_menu.cpp @@ -22,7 +22,7 @@ void game_cl_mp::AddMessageMenu(LPCSTR menu_section, LPCSTR snd_path, LPCSTR tea for (u32 i = 0; i < 10; i++) { shared_str LineName; - LineName.printf("phrase_%d", i); + xr_sprintf(LineName, "phrase_%d", i); if (!pSettings->line_exist(menu_section, LineName.c_str())) break; //--------------------------------------------------------- @@ -89,7 +89,7 @@ void game_cl_mp::LoadMessagesMenu(LPCSTR menus_section) for (int i = 0; i < 10; i++) { shared_str LineName; - LineName.printf("menu_%d", i); + xr_sprintf(LineName, "menu_%d", i); if (!pSettings->line_exist(menus_section, LineName.c_str())) break; shared_str menu_section = pSettings->r_string(menus_section, LineName.c_str()); diff --git a/src/xrGame/game_sv_mp.cpp b/src/xrGame/game_sv_mp.cpp index e3953ccbfd7..3f6162c1892 100644 --- a/src/xrGame/game_sv_mp.cpp +++ b/src/xrGame/game_sv_mp.cpp @@ -1055,7 +1055,7 @@ void game_sv_mp::OnVoteStart(LPCSTR VoteCommand, ClientID sender) string256 WeatherTime = "", WeatherName = ""; sscanf(CommandParams, "%255s %255s", WeatherName, WeatherTime); - m_pVoteCommand.printf("%s %s", votecommands[i].command, WeatherTime); + xr_sprintf(m_pVoteCommand, "%s %s", votecommands[i].command, WeatherTime); xr_sprintf(resVoteCommand, "%s %s", votecommands[i].name, WeatherName); } else if (!xr_stricmp(votecommands[i].name, "changemap")) @@ -1080,11 +1080,11 @@ void game_sv_mp::OnVoteStart(LPCSTR VoteCommand, ClientID sender) IClient* tmp_client = m_server->FindClient(tmp_predicate); if (tmp_client) { - m_pVoteCommand.printf("sv_kick_id %u", tmp_client->ID.value()); + xr_sprintf(m_pVoteCommand, "sv_kick_id %u", tmp_client->ID.value()); } else { - m_pVoteCommand.printf("%s %s", votecommands[i].command, CommandParams); // backward compatibility + xr_sprintf(m_pVoteCommand, "%s %s", votecommands[i].command, CommandParams); // backward compatibility } xr_strcpy(resVoteCommand, VoteCommand); } @@ -1098,7 +1098,7 @@ void game_sv_mp::OnVoteStart(LPCSTR VoteCommand, ClientID sender) IClient* tmp_client = m_server->FindClient(tmp_predicate); if (tmp_client) { - m_pVoteCommand.printf("sv_banplayer %u %d", tmp_client->ID.value(), ban_time); + xr_sprintf(m_pVoteCommand, "sv_banplayer %u %d", tmp_client->ID.value(), ban_time); } else { @@ -1112,13 +1112,13 @@ void game_sv_mp::OnVoteStart(LPCSTR VoteCommand, ClientID sender) } else { - m_pVoteCommand.printf("%s %s", votecommands[i].command, CommandParams); + xr_sprintf(m_pVoteCommand, "%s %s", votecommands[i].command, CommandParams); xr_strcpy(resVoteCommand, VoteCommand); } } else { - m_pVoteCommand.printf("%s", VoteCommand + 1); + xr_sprintf(m_pVoteCommand, "%s", VoteCommand + 1); }; struct vote_status_setter diff --git a/src/xrGame/ui/ServerList.cpp b/src/xrGame/ui/ServerList.cpp index 14dcb3ba485..da2f632e718 100644 --- a/src/xrGame/ui/ServerList.cpp +++ b/src/xrGame/ui/ServerList.cpp @@ -868,8 +868,8 @@ void CServerList::SrvInfo2LstSrvInfo(const ServerInfo* pServerInfo) m_itemInfo.info.address = address.c_str(); m_itemInfo.info.map = pServerInfo->m_SessionName; m_itemInfo.info.game = GameTypeToStringEx(pServerInfo->m_GameType, true); - m_itemInfo.info.players.printf("%d/%d", pServerInfo->m_ServerNumPlayers, pServerInfo->m_ServerMaxPlayers); - m_itemInfo.info.ping.printf("%d", pServerInfo->m_Ping); + xr_sprintf(m_itemInfo.info.players, "%d/%d", pServerInfo->m_ServerNumPlayers, pServerInfo->m_ServerMaxPlayers); + xr_sprintf(m_itemInfo.info.ping, "%d", pServerInfo->m_Ping); m_itemInfo.info.version = pServerInfo->m_ServerVersion; m_itemInfo.info.icons.pass = pServerInfo->m_bPassword; m_itemInfo.info.icons.dedicated = pServerInfo->m_bDedicated; diff --git a/src/xrGame/ui/UIActorMenuInventory.cpp b/src/xrGame/ui/UIActorMenuInventory.cpp index 31968d4b3ec..0eaff19cf0f 100644 --- a/src/xrGame/ui/UIActorMenuInventory.cpp +++ b/src/xrGame/ui/UIActorMenuInventory.cpp @@ -1128,7 +1128,7 @@ void CUIActorMenu::PropertiesBoxForAddon(PIItem item, bool& b_show) if (item_in_slot_2 && item_in_slot_2->CanAttach(pScope)) { shared_str str = StringTable().translate("st_attach_scope_to_pistol"); - str.printf("%s %s", str.c_str(), item_in_slot_2->m_name.c_str()); + xr_sprintf(str, "%s %s", str.c_str(), item_in_slot_2->m_name.c_str()); m_UIPropertiesBox->AddItem(str.c_str(), (void*)item_in_slot_2, INVENTORY_ATTACH_ADDON); // m_UIPropertiesBox->AddItem( "st_attach_scope_to_pistol", (void*)item_in_slot_2, // INVENTORY_ATTACH_ADDON ); @@ -1137,7 +1137,7 @@ void CUIActorMenu::PropertiesBoxForAddon(PIItem item, bool& b_show) if (item_in_slot_3 && item_in_slot_3->CanAttach(pScope)) { shared_str str = StringTable().translate("st_attach_scope_to_pistol"); - str.printf("%s %s", str.c_str(), item_in_slot_3->m_name.c_str()); + xr_sprintf(str, "%s %s", str.c_str(), item_in_slot_3->m_name.c_str()); m_UIPropertiesBox->AddItem(str.c_str(), (void*)item_in_slot_3, INVENTORY_ATTACH_ADDON); // m_UIPropertiesBox->AddItem( "st_attach_scope_to_rifle", (void*)item_in_slot_3, // INVENTORY_ATTACH_ADDON ); @@ -1151,7 +1151,7 @@ void CUIActorMenu::PropertiesBoxForAddon(PIItem item, bool& b_show) if (item_in_slot_2 && item_in_slot_2->CanAttach(pSilencer)) { shared_str str = StringTable().translate("st_attach_silencer_to_pistol"); - str.printf("%s %s", str.c_str(), item_in_slot_2->m_name.c_str()); + xr_sprintf(str, "%s %s", str.c_str(), item_in_slot_2->m_name.c_str()); m_UIPropertiesBox->AddItem(str.c_str(), (void*)item_in_slot_2, INVENTORY_ATTACH_ADDON); // m_UIPropertiesBox->AddItem( "st_attach_silencer_to_pistol", (void*)item_in_slot_2, // INVENTORY_ATTACH_ADDON ); @@ -1160,7 +1160,7 @@ void CUIActorMenu::PropertiesBoxForAddon(PIItem item, bool& b_show) if (item_in_slot_3 && item_in_slot_3->CanAttach(pSilencer)) { shared_str str = StringTable().translate("st_attach_silencer_to_pistol"); - str.printf("%s %s", str.c_str(), item_in_slot_3->m_name.c_str()); + xr_sprintf(str, "%s %s", str.c_str(), item_in_slot_3->m_name.c_str()); m_UIPropertiesBox->AddItem(str.c_str(), (void*)item_in_slot_3, INVENTORY_ATTACH_ADDON); // m_UIPropertiesBox->AddItem( "st_attach_silencer_to_rifle", (void*)item_in_slot_3, // INVENTORY_ATTACH_ADDON ); @@ -1174,7 +1174,7 @@ void CUIActorMenu::PropertiesBoxForAddon(PIItem item, bool& b_show) if (item_in_slot_2 && item_in_slot_2->CanAttach(pGrenadeLauncher)) { shared_str str = StringTable().translate("st_attach_gl_to_rifle"); - str.printf("%s %s", str.c_str(), item_in_slot_2->m_name.c_str()); + xr_sprintf(str, "%s %s", str.c_str(), item_in_slot_2->m_name.c_str()); m_UIPropertiesBox->AddItem(str.c_str(), (void*)item_in_slot_2, INVENTORY_ATTACH_ADDON); // m_UIPropertiesBox->AddItem( "st_attach_gl_to_pistol", (void*)item_in_slot_2, //INVENTORY_ATTACH_ADDON @@ -1184,7 +1184,7 @@ void CUIActorMenu::PropertiesBoxForAddon(PIItem item, bool& b_show) if (item_in_slot_3 && item_in_slot_3->CanAttach(pGrenadeLauncher)) { shared_str str = StringTable().translate("st_attach_gl_to_rifle"); - str.printf("%s %s", str.c_str(), item_in_slot_3->m_name.c_str()); + xr_sprintf(str, "%s %s", str.c_str(), item_in_slot_3->m_name.c_str()); m_UIPropertiesBox->AddItem(str.c_str(), (void*)item_in_slot_3, INVENTORY_ATTACH_ADDON); // m_UIPropertiesBox->AddItem( "st_attach_gl_to_rifle", (void*)item_in_slot_3, //INVENTORY_ATTACH_ADDON diff --git a/src/xrGame/ui/UIPdaWnd.cpp b/src/xrGame/ui/UIPdaWnd.cpp index d77309df24f..9d6ba7fd9b0 100644 --- a/src/xrGame/ui/UIPdaWnd.cpp +++ b/src/xrGame/ui/UIPdaWnd.cpp @@ -259,7 +259,7 @@ void CUIPdaWnd::Update() if (m_clock->GetParent() != this) // SOC { const auto date = GetGameDateAsString(InventoryUtilities::edpDateToDay, '/', true); - time.printf("%s %s", time.c_str(), date.c_str()); + xr_sprintf(time, "%s %s", time.c_str(), date.c_str()); } m_clock->SetText(time.c_str()); } From 857e360aa6de415db0bb7682b4ea671198cb7793 Mon Sep 17 00:00:00 2001 From: Rybaev Ilya Mikhailovich <61088459+Aardes-Roiven@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:16:43 +0300 Subject: [PATCH 142/261] Replace shared_str::printf with standardized xr_sprintf (#1948) (#2022) --- src/Layers/xrRender/ETextureParams.cpp | 10 ++++++++-- src/xrCore/xrstring.h | 25 ++++++++++++------------- src/xrGame/game_cl_mp.cpp | 3 +-- src/xrGame/game_cl_mp_messages_menu.cpp | 4 ++-- src/xrGame/game_sv_mp.cpp | 12 ++++++------ src/xrGame/ui/ServerList.cpp | 4 ++-- src/xrGame/ui/UIActorMenuInventory.cpp | 12 ++++++------ src/xrGame/ui/UIPdaWnd.cpp | 2 +- 8 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/Layers/xrRender/ETextureParams.cpp b/src/Layers/xrRender/ETextureParams.cpp index efbae4f8a21..c31dacc97f3 100644 --- a/src/Layers/xrRender/ETextureParams.cpp +++ b/src/Layers/xrRender/ETextureParams.cpp @@ -161,8 +161,14 @@ void STextureParams::FillProp(LPCSTR base_name, PropItemVec& items, PropValue::T OnTypeChangeEvent = on_type_change; PropValue* P = PHelper().CreateToken32(items, "Type", (u32*)&type, ttype_token); P->OnChangeEvent.bind(this, &STextureParams::OnTypeChange); - PHelper().CreateCaption(items, "Source" DELIMITER "Width", shared_str().printf("%d", width)); - PHelper().CreateCaption(items, "Source" DELIMITER "Height", shared_str().printf("%d", height)); + + shared_str temp_str; + xr_sprintf(temp_str, "%d", width); + PHelper().CreateCaption(items, "Source" DELIMITER "Width", temp_str); + + xr_sprintf(temp_str, "%d", height); + PHelper().CreateCaption(items, "Source" DELIMITER "Height", v); + PHelper().CreateCaption(items, "Source" DELIMITER "Alpha", HasAlpha() ? "present" : "absent"); switch (type) { diff --git a/src/xrCore/xrstring.h b/src/xrCore/xrstring.h index dc08b318729..a03396575a4 100644 --- a/src/xrCore/xrstring.h +++ b/src/xrCore/xrstring.h @@ -176,21 +176,20 @@ class shared_str [[nodiscard]] bool equal(const shared_str& rhs) const { return (p_ == rhs.p_); } - - shared_str& __cdecl printf(const char* format, ...) - { - string4096 buf; - va_list p; - va_start(p, format); - int vs_sz = vsnprintf(buf, sizeof(buf) - 1, format, p); - buf[sizeof(buf) - 1] = 0; - va_end(p); - if (vs_sz) - _set(buf); - return (shared_str&)*this; - } }; +inline int __cdecl xr_sprintf(shared_str& destination, pcstr format_string, ...) +{ + string4096 buf; + va_list args; + va_start(args, format_string); + const int vs_sz = vsnprintf(buf, sizeof(buf) - 1, format_string, args); + buf[sizeof(buf) - 1] = 0; + va_end(args); + if (vs_sz >= 0) + destination = buf; + return vs_sz; +} template<> struct std::hash diff --git a/src/xrGame/game_cl_mp.cpp b/src/xrGame/game_cl_mp.cpp index 107ca9e52c9..231dc63208b 100644 --- a/src/xrGame/game_cl_mp.cpp +++ b/src/xrGame/game_cl_mp.cpp @@ -1244,8 +1244,7 @@ void game_cl_mp::OnEventMoneyChanged(NET_Packet& P) } case SKT_KIR: { - BName.printf("%d_kill_in_row", BonusKills); - + xr_sprintf(BName, "%d_kill_in_row", BonusKills); xr_sprintf(MoneyStr, sizeof(MoneyStr), "%d", BonusKills); BMS.m_killer.m_name = MoneyStr; BMS.m_killer.m_color = 0xffff0000; diff --git a/src/xrGame/game_cl_mp_messages_menu.cpp b/src/xrGame/game_cl_mp_messages_menu.cpp index cdb46954a24..d307ab6a2a3 100644 --- a/src/xrGame/game_cl_mp_messages_menu.cpp +++ b/src/xrGame/game_cl_mp_messages_menu.cpp @@ -22,7 +22,7 @@ void game_cl_mp::AddMessageMenu(LPCSTR menu_section, LPCSTR snd_path, LPCSTR tea for (u32 i = 0; i < 10; i++) { shared_str LineName; - LineName.printf("phrase_%d", i); + xr_sprintf(LineName, "phrase_%d", i); if (!pSettings->line_exist(menu_section, LineName.c_str())) break; //--------------------------------------------------------- @@ -89,7 +89,7 @@ void game_cl_mp::LoadMessagesMenu(LPCSTR menus_section) for (int i = 0; i < 10; i++) { shared_str LineName; - LineName.printf("menu_%d", i); + xr_sprintf(LineName, "menu_%d", i); if (!pSettings->line_exist(menus_section, LineName.c_str())) break; shared_str menu_section = pSettings->r_string(menus_section, LineName.c_str()); diff --git a/src/xrGame/game_sv_mp.cpp b/src/xrGame/game_sv_mp.cpp index e3953ccbfd7..3f6162c1892 100644 --- a/src/xrGame/game_sv_mp.cpp +++ b/src/xrGame/game_sv_mp.cpp @@ -1055,7 +1055,7 @@ void game_sv_mp::OnVoteStart(LPCSTR VoteCommand, ClientID sender) string256 WeatherTime = "", WeatherName = ""; sscanf(CommandParams, "%255s %255s", WeatherName, WeatherTime); - m_pVoteCommand.printf("%s %s", votecommands[i].command, WeatherTime); + xr_sprintf(m_pVoteCommand, "%s %s", votecommands[i].command, WeatherTime); xr_sprintf(resVoteCommand, "%s %s", votecommands[i].name, WeatherName); } else if (!xr_stricmp(votecommands[i].name, "changemap")) @@ -1080,11 +1080,11 @@ void game_sv_mp::OnVoteStart(LPCSTR VoteCommand, ClientID sender) IClient* tmp_client = m_server->FindClient(tmp_predicate); if (tmp_client) { - m_pVoteCommand.printf("sv_kick_id %u", tmp_client->ID.value()); + xr_sprintf(m_pVoteCommand, "sv_kick_id %u", tmp_client->ID.value()); } else { - m_pVoteCommand.printf("%s %s", votecommands[i].command, CommandParams); // backward compatibility + xr_sprintf(m_pVoteCommand, "%s %s", votecommands[i].command, CommandParams); // backward compatibility } xr_strcpy(resVoteCommand, VoteCommand); } @@ -1098,7 +1098,7 @@ void game_sv_mp::OnVoteStart(LPCSTR VoteCommand, ClientID sender) IClient* tmp_client = m_server->FindClient(tmp_predicate); if (tmp_client) { - m_pVoteCommand.printf("sv_banplayer %u %d", tmp_client->ID.value(), ban_time); + xr_sprintf(m_pVoteCommand, "sv_banplayer %u %d", tmp_client->ID.value(), ban_time); } else { @@ -1112,13 +1112,13 @@ void game_sv_mp::OnVoteStart(LPCSTR VoteCommand, ClientID sender) } else { - m_pVoteCommand.printf("%s %s", votecommands[i].command, CommandParams); + xr_sprintf(m_pVoteCommand, "%s %s", votecommands[i].command, CommandParams); xr_strcpy(resVoteCommand, VoteCommand); } } else { - m_pVoteCommand.printf("%s", VoteCommand + 1); + xr_sprintf(m_pVoteCommand, "%s", VoteCommand + 1); }; struct vote_status_setter diff --git a/src/xrGame/ui/ServerList.cpp b/src/xrGame/ui/ServerList.cpp index 14dcb3ba485..da2f632e718 100644 --- a/src/xrGame/ui/ServerList.cpp +++ b/src/xrGame/ui/ServerList.cpp @@ -868,8 +868,8 @@ void CServerList::SrvInfo2LstSrvInfo(const ServerInfo* pServerInfo) m_itemInfo.info.address = address.c_str(); m_itemInfo.info.map = pServerInfo->m_SessionName; m_itemInfo.info.game = GameTypeToStringEx(pServerInfo->m_GameType, true); - m_itemInfo.info.players.printf("%d/%d", pServerInfo->m_ServerNumPlayers, pServerInfo->m_ServerMaxPlayers); - m_itemInfo.info.ping.printf("%d", pServerInfo->m_Ping); + xr_sprintf(m_itemInfo.info.players, "%d/%d", pServerInfo->m_ServerNumPlayers, pServerInfo->m_ServerMaxPlayers); + xr_sprintf(m_itemInfo.info.ping, "%d", pServerInfo->m_Ping); m_itemInfo.info.version = pServerInfo->m_ServerVersion; m_itemInfo.info.icons.pass = pServerInfo->m_bPassword; m_itemInfo.info.icons.dedicated = pServerInfo->m_bDedicated; diff --git a/src/xrGame/ui/UIActorMenuInventory.cpp b/src/xrGame/ui/UIActorMenuInventory.cpp index 31968d4b3ec..0eaff19cf0f 100644 --- a/src/xrGame/ui/UIActorMenuInventory.cpp +++ b/src/xrGame/ui/UIActorMenuInventory.cpp @@ -1128,7 +1128,7 @@ void CUIActorMenu::PropertiesBoxForAddon(PIItem item, bool& b_show) if (item_in_slot_2 && item_in_slot_2->CanAttach(pScope)) { shared_str str = StringTable().translate("st_attach_scope_to_pistol"); - str.printf("%s %s", str.c_str(), item_in_slot_2->m_name.c_str()); + xr_sprintf(str, "%s %s", str.c_str(), item_in_slot_2->m_name.c_str()); m_UIPropertiesBox->AddItem(str.c_str(), (void*)item_in_slot_2, INVENTORY_ATTACH_ADDON); // m_UIPropertiesBox->AddItem( "st_attach_scope_to_pistol", (void*)item_in_slot_2, // INVENTORY_ATTACH_ADDON ); @@ -1137,7 +1137,7 @@ void CUIActorMenu::PropertiesBoxForAddon(PIItem item, bool& b_show) if (item_in_slot_3 && item_in_slot_3->CanAttach(pScope)) { shared_str str = StringTable().translate("st_attach_scope_to_pistol"); - str.printf("%s %s", str.c_str(), item_in_slot_3->m_name.c_str()); + xr_sprintf(str, "%s %s", str.c_str(), item_in_slot_3->m_name.c_str()); m_UIPropertiesBox->AddItem(str.c_str(), (void*)item_in_slot_3, INVENTORY_ATTACH_ADDON); // m_UIPropertiesBox->AddItem( "st_attach_scope_to_rifle", (void*)item_in_slot_3, // INVENTORY_ATTACH_ADDON ); @@ -1151,7 +1151,7 @@ void CUIActorMenu::PropertiesBoxForAddon(PIItem item, bool& b_show) if (item_in_slot_2 && item_in_slot_2->CanAttach(pSilencer)) { shared_str str = StringTable().translate("st_attach_silencer_to_pistol"); - str.printf("%s %s", str.c_str(), item_in_slot_2->m_name.c_str()); + xr_sprintf(str, "%s %s", str.c_str(), item_in_slot_2->m_name.c_str()); m_UIPropertiesBox->AddItem(str.c_str(), (void*)item_in_slot_2, INVENTORY_ATTACH_ADDON); // m_UIPropertiesBox->AddItem( "st_attach_silencer_to_pistol", (void*)item_in_slot_2, // INVENTORY_ATTACH_ADDON ); @@ -1160,7 +1160,7 @@ void CUIActorMenu::PropertiesBoxForAddon(PIItem item, bool& b_show) if (item_in_slot_3 && item_in_slot_3->CanAttach(pSilencer)) { shared_str str = StringTable().translate("st_attach_silencer_to_pistol"); - str.printf("%s %s", str.c_str(), item_in_slot_3->m_name.c_str()); + xr_sprintf(str, "%s %s", str.c_str(), item_in_slot_3->m_name.c_str()); m_UIPropertiesBox->AddItem(str.c_str(), (void*)item_in_slot_3, INVENTORY_ATTACH_ADDON); // m_UIPropertiesBox->AddItem( "st_attach_silencer_to_rifle", (void*)item_in_slot_3, // INVENTORY_ATTACH_ADDON ); @@ -1174,7 +1174,7 @@ void CUIActorMenu::PropertiesBoxForAddon(PIItem item, bool& b_show) if (item_in_slot_2 && item_in_slot_2->CanAttach(pGrenadeLauncher)) { shared_str str = StringTable().translate("st_attach_gl_to_rifle"); - str.printf("%s %s", str.c_str(), item_in_slot_2->m_name.c_str()); + xr_sprintf(str, "%s %s", str.c_str(), item_in_slot_2->m_name.c_str()); m_UIPropertiesBox->AddItem(str.c_str(), (void*)item_in_slot_2, INVENTORY_ATTACH_ADDON); // m_UIPropertiesBox->AddItem( "st_attach_gl_to_pistol", (void*)item_in_slot_2, //INVENTORY_ATTACH_ADDON @@ -1184,7 +1184,7 @@ void CUIActorMenu::PropertiesBoxForAddon(PIItem item, bool& b_show) if (item_in_slot_3 && item_in_slot_3->CanAttach(pGrenadeLauncher)) { shared_str str = StringTable().translate("st_attach_gl_to_rifle"); - str.printf("%s %s", str.c_str(), item_in_slot_3->m_name.c_str()); + xr_sprintf(str, "%s %s", str.c_str(), item_in_slot_3->m_name.c_str()); m_UIPropertiesBox->AddItem(str.c_str(), (void*)item_in_slot_3, INVENTORY_ATTACH_ADDON); // m_UIPropertiesBox->AddItem( "st_attach_gl_to_rifle", (void*)item_in_slot_3, //INVENTORY_ATTACH_ADDON diff --git a/src/xrGame/ui/UIPdaWnd.cpp b/src/xrGame/ui/UIPdaWnd.cpp index d77309df24f..9d6ba7fd9b0 100644 --- a/src/xrGame/ui/UIPdaWnd.cpp +++ b/src/xrGame/ui/UIPdaWnd.cpp @@ -259,7 +259,7 @@ void CUIPdaWnd::Update() if (m_clock->GetParent() != this) // SOC { const auto date = GetGameDateAsString(InventoryUtilities::edpDateToDay, '/', true); - time.printf("%s %s", time.c_str(), date.c_str()); + xr_sprintf(time, "%s %s", time.c_str(), date.c_str()); } m_clock->SetText(time.c_str()); } From 91b441885ac23a07c44ec5812b9be8e4625e9aed Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 29 Dec 2025 14:30:52 +0300 Subject: [PATCH 143/261] shared_str: use already calculated crc32 for std::hash overload Remove unused str_hash_function --- src/xrCore/xrstring.h | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/xrCore/xrstring.h b/src/xrCore/xrstring.h index a03396575a4..9384b91b4fa 100644 --- a/src/xrCore/xrstring.h +++ b/src/xrCore/xrstring.h @@ -25,11 +25,6 @@ struct XRCORE_API str_value_cmp IC bool operator()(const str_value* A, const str_value* B) const { return A->dwCRC < B->dwCRC; }; }; -struct XRCORE_API str_hash_function -{ - IC u32 operator()(str_value const* const value) const { return value->dwCRC; }; -}; - #pragma warning(pop) struct str_container_impl; @@ -196,8 +191,7 @@ struct std::hash { [[nodiscard]] size_t operator()(const shared_str& str) const noexcept { - const auto str_val = str._get(); - return std::hash{}(str_val ? str_val->value : nullptr); + return str ? str._get()->dwCRC : std::hash{}(nullptr); } }; From 6e5e74b3dbca520ed8bd41b47e75d35b01731ca0 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 29 Dec 2025 14:30:52 +0300 Subject: [PATCH 144/261] shared_str: use already calculated crc32 for std::hash overload Remove unused str_hash_function --- src/xrCore/xrstring.h | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/xrCore/xrstring.h b/src/xrCore/xrstring.h index a03396575a4..9384b91b4fa 100644 --- a/src/xrCore/xrstring.h +++ b/src/xrCore/xrstring.h @@ -25,11 +25,6 @@ struct XRCORE_API str_value_cmp IC bool operator()(const str_value* A, const str_value* B) const { return A->dwCRC < B->dwCRC; }; }; -struct XRCORE_API str_hash_function -{ - IC u32 operator()(str_value const* const value) const { return value->dwCRC; }; -}; - #pragma warning(pop) struct str_container_impl; @@ -196,8 +191,7 @@ struct std::hash { [[nodiscard]] size_t operator()(const shared_str& str) const noexcept { - const auto str_val = str._get(); - return std::hash{}(str_val ? str_val->value : nullptr); + return str ? str._get()->dwCRC : std::hash{}(nullptr); } }; From 3a0d13b84c76b10a2e0676bcc5f13617417def96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:46:41 +0300 Subject: [PATCH 145/261] build(deps): bump Externals/sse2neon from `ade5552` to `07fd1e9` (#2023) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2neon b/Externals/sse2neon index ade5552a328..07fd1e90bd6 160000 --- a/Externals/sse2neon +++ b/Externals/sse2neon @@ -1 +1 @@ -Subproject commit ade5552a32852422e4f34f0beaa51790ef9f4171 +Subproject commit 07fd1e90bd6b89e26337507acd73666f2cba883f From b9b320a4b157d0282c69fbfcf07f6d98a4109909 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:46:41 +0300 Subject: [PATCH 146/261] build(deps): bump Externals/sse2neon from `ade5552` to `07fd1e9` (#2023) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2neon b/Externals/sse2neon index ade5552a328..07fd1e90bd6 160000 --- a/Externals/sse2neon +++ b/Externals/sse2neon @@ -1 +1 @@ -Subproject commit ade5552a32852422e4f34f0beaa51790ef9f4171 +Subproject commit 07fd1e90bd6b89e26337507acd73666f2cba883f From e65d0e79cf540bea40a2d02429412da3c37aada5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:46:50 +0300 Subject: [PATCH 147/261] build(deps): bump Externals/imgui from `0a47833` to `396b33d` (#2024) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 0a4783309d8..396b33d0d01 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 0a4783309d85bf9a6a646cf21ed2f7bb526ddf2a +Subproject commit 396b33d0d011290f11915b6a2a2bf7737fb42dd7 From 72c9a6d7de445b343df7280250386ae04a37b98b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:46:50 +0300 Subject: [PATCH 148/261] build(deps): bump Externals/imgui from `0a47833` to `396b33d` (#2024) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 0a4783309d8..396b33d0d01 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 0a4783309d85bf9a6a646cf21ed2f7bb526ddf2a +Subproject commit 396b33d0d011290f11915b6a2a2bf7737fb42dd7 From 2f5611d7fe545694aed2001f90fd01c8b4942682 Mon Sep 17 00:00:00 2001 From: yohjimane Date: Mon, 12 Jan 2026 23:35:31 -0800 Subject: [PATCH 149/261] Update roles and contact information, add new ownership info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for your hard work through the years @Xottab-DUTY ❤️ --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 616505d2ac1..315762677e7 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ You may provide financial support for this project by donating via different way * BTC: 363ZUoWcQe9fDvRPK9Kee2YuPdyhSFQpr2 * ETH: 0x45a4fe8566e76946591e1eeabf190aa09b1cdb66 * TRX: TGx7QAhTPsRcwnb4mwCtNDU7NF6kuoACpt -* Please, contact @xottab_duty in [our Discord](discord.gg/sjRMQwv) if you would like to use another cryptocurrency. +* Please, contact @1yohji in [our Discord](discord.gg/sjRMQwv) if you would like to use another cryptocurrency. Thank you for your support! @@ -110,7 +110,7 @@ Thank you for your support! * [andrew-boyarshin](https://github.com/andrew-boyarshin) – work on the build system. * [CasualDev242](https://github.com/CasualDev242) (Swartz27) – work on renderer features. * [awdavies](https://github.com/awdavies) – project maintenance work. - * [Xottab_DUTY](https://github.com/Xottab-DUTY) – current project leader, gathering a new team, creation of the community (GitHub, Discord, VK), defining project guiding principles and goals, working on many areas of tasks (core, renderering, AI, gameplay, UI), SOC/CS/COC support. + * [Xottab_DUTY](https://github.com/Xottab-DUTY) – former project leader (2018-2026), gathering a new team, creation of the community (GitHub, Discord, VK), defining project guiding principles and goals, working on many areas of tasks (core, renderering, AI, gameplay, UI), SOC/CS/COC support. * [intorr](https://github.com/intorr) – work on the project quality, memory leaks, refactoring and optimizations. * [eagleivg](https://github.com/eagleivg) – main part of the work on Linux port. * [q4a](https://github.com/q4a) – main part of the work on Linux port. @@ -126,7 +126,7 @@ Thank you for your support! * [Zigatun](https://github.com/Zigatun) – work on ARM port. * [Masterkatze](https://github.com/Masterkatze) – work on the build system, bug fixing. * [Chugunov Roman](https://github.com/ChugunovRoman) – work on [porting Call of Chernobyl to latest OpenXRay](https://github.com/ChugunovRoman/xray-16), extending functionality for modmakers. - * [yohjimane](https://github.com/yohjimane) – work on introducing many new features, fixing original engine bugs. + * [yohjimane](https://github.com/yohjimane) – lead developer (2026-current), work on introducing many new features, fixing original engine bugs * Other contributors: * [alexgdi](https://github.com/alexgdi) – work on organizing project infrastructure, external dependencies. * [Shoker](https://github.com/ShokerStlk) – contributing new features, bug fixing. From 6d3019619514ab5b6e5592dd123fba59c8290734 Mon Sep 17 00:00:00 2001 From: yohjimane Date: Mon, 12 Jan 2026 23:35:31 -0800 Subject: [PATCH 150/261] Update roles and contact information, add new ownership info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for your hard work through the years @Xottab-DUTY ❤️ --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 616505d2ac1..315762677e7 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ You may provide financial support for this project by donating via different way * BTC: 363ZUoWcQe9fDvRPK9Kee2YuPdyhSFQpr2 * ETH: 0x45a4fe8566e76946591e1eeabf190aa09b1cdb66 * TRX: TGx7QAhTPsRcwnb4mwCtNDU7NF6kuoACpt -* Please, contact @xottab_duty in [our Discord](discord.gg/sjRMQwv) if you would like to use another cryptocurrency. +* Please, contact @1yohji in [our Discord](discord.gg/sjRMQwv) if you would like to use another cryptocurrency. Thank you for your support! @@ -110,7 +110,7 @@ Thank you for your support! * [andrew-boyarshin](https://github.com/andrew-boyarshin) – work on the build system. * [CasualDev242](https://github.com/CasualDev242) (Swartz27) – work on renderer features. * [awdavies](https://github.com/awdavies) – project maintenance work. - * [Xottab_DUTY](https://github.com/Xottab-DUTY) – current project leader, gathering a new team, creation of the community (GitHub, Discord, VK), defining project guiding principles and goals, working on many areas of tasks (core, renderering, AI, gameplay, UI), SOC/CS/COC support. + * [Xottab_DUTY](https://github.com/Xottab-DUTY) – former project leader (2018-2026), gathering a new team, creation of the community (GitHub, Discord, VK), defining project guiding principles and goals, working on many areas of tasks (core, renderering, AI, gameplay, UI), SOC/CS/COC support. * [intorr](https://github.com/intorr) – work on the project quality, memory leaks, refactoring and optimizations. * [eagleivg](https://github.com/eagleivg) – main part of the work on Linux port. * [q4a](https://github.com/q4a) – main part of the work on Linux port. @@ -126,7 +126,7 @@ Thank you for your support! * [Zigatun](https://github.com/Zigatun) – work on ARM port. * [Masterkatze](https://github.com/Masterkatze) – work on the build system, bug fixing. * [Chugunov Roman](https://github.com/ChugunovRoman) – work on [porting Call of Chernobyl to latest OpenXRay](https://github.com/ChugunovRoman/xray-16), extending functionality for modmakers. - * [yohjimane](https://github.com/yohjimane) – work on introducing many new features, fixing original engine bugs. + * [yohjimane](https://github.com/yohjimane) – lead developer (2026-current), work on introducing many new features, fixing original engine bugs * Other contributors: * [alexgdi](https://github.com/alexgdi) – work on organizing project infrastructure, external dependencies. * [Shoker](https://github.com/ShokerStlk) – contributing new features, bug fixing. From 7f5a4ee398e67563460760e9f16cdf754a05d558 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 29 Jan 2026 13:45:51 +0300 Subject: [PATCH 151/261] README.md: reflect some geographical changes in the funding section I also removed the crypto wallets, because I created them for OpenXRay back in the days, but I can't transfer them, so you should consider them as my personal since I'm not the leader anymore. --- README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 315762677e7..d6ee5064be6 100644 --- a/README.md +++ b/README.md @@ -87,16 +87,14 @@ The `dev` branch is the default and base branch for the project. It is used for The code base is based on X-Ray 1.6.02 that is used in S.T.A.L.K.E.R.: Call of Pripyat and it was greatly refactored. ### Funding -[![Financial Contributors](https://opencollective.com/openxray/tiers/badge.svg?label=Financial%20contributors)](https://opencollective.com/openxray) [![Sponsors](https://img.shields.io/github/sponsors/openxray?color=brightgreen&label=Sponsors)](https://github.com/sponsors/OpenXRay) [![Patreon](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F5950725&query=data.attributes.patron_count&suffix=%20Patrons&color=success&label=Patreon&style=flat)](https://patreon.com/openxray) +[![Sponsors](https://img.shields.io/github/sponsors/openxray?color=brightgreen&label=Sponsors)](https://github.com/sponsors/OpenXRay) [![Patreon](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F5950725&query=data.attributes.patron_count&suffix=%20Patrons&color=success&label=Patreon&style=flat)](https://patreon.com/openxray) [![Financial Contributors](https://opencollective.com/openxray/tiers/badge.svg?label=Financial%20contributors)](https://opencollective.com/openxray) You may provide financial support for this project by donating via different ways: -* [Boosty](https://boosty.to/openxray) – a large part of the team is located in Russia, if you have an ability to donate through Boosty, please use it, since we don't have an ability to withdraw funds from services like Patreon, etc. to our local Russian banking cards/accounts. -* [GitHub Sponsors](https://github.com/sponsors/OpenXRay), [Patreon](https://patreon.com/openxray), [Open Collective](https://opencollective.com/openxray) – funds raised from these services will be used to support our developers outside of Russia, and also we may use them to pay for paid services on GitHub, AppVeyor, etc. -* BTC: 363ZUoWcQe9fDvRPK9Kee2YuPdyhSFQpr2 -* ETH: 0x45a4fe8566e76946591e1eeabf190aa09b1cdb66 -* TRX: TGx7QAhTPsRcwnb4mwCtNDU7NF6kuoACpt -* Please, contact @1yohji in [our Discord](discord.gg/sjRMQwv) if you would like to use another cryptocurrency. +* [GitHub Sponsors](https://github.com/sponsors/OpenXRay), [Patreon](https://patreon.com/openxray), [Open Collective](https://opencollective.com/openxray) – many team members and contributors are located all over the world, and we provide a way to support of us worldwide. +* [Boosty](https://boosty.to/openxray) – a large part of the team and many contributors are located in Russia, and we provide a way to support us from Russia and Belarus. +* Please, contact @1yohji in [our Discord](discord.gg/sjRMQwv) if you would like to use cryptocurrency. +Raised funds will be used to support our developers and contributors, and also we may use them to pay for paid services on GitHub, etc. Thank you for your support! ## Thanks @@ -104,7 +102,7 @@ Thank you for your support! * Loxotron – for making the engine sources available. * [All the OpenXRay contributors](https://github.com/OpenXRay/xray-16/graphs/contributors) – for making the project what it is: * The OpenXRay team: - * [nitrocaster](https://github.com/nitrocaster) – original project founder. + * [nitrocaster](https://github.com/nitrocaster) – original project founder and former project leader (2014-2018). * [Kaffeine](https://github.com/Kaffeine) – initial work on the Linux port, refactoring, polishing. * [CrossVR](https://github.com/CrossVR) (Armada651) – creation of the OpenGL renderer, work on the build system, other project maintenance work. * [andrew-boyarshin](https://github.com/andrew-boyarshin) – work on the build system. From e70c278bdd05b924fa1ad1a538d092fbc514d40b Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 29 Jan 2026 13:45:51 +0300 Subject: [PATCH 152/261] README.md: reflect some geographical changes in the funding section I also removed the crypto wallets, because I created them for OpenXRay back in the days, but I can't transfer them, so you should consider them as my personal since I'm not the leader anymore. --- README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 315762677e7..d6ee5064be6 100644 --- a/README.md +++ b/README.md @@ -87,16 +87,14 @@ The `dev` branch is the default and base branch for the project. It is used for The code base is based on X-Ray 1.6.02 that is used in S.T.A.L.K.E.R.: Call of Pripyat and it was greatly refactored. ### Funding -[![Financial Contributors](https://opencollective.com/openxray/tiers/badge.svg?label=Financial%20contributors)](https://opencollective.com/openxray) [![Sponsors](https://img.shields.io/github/sponsors/openxray?color=brightgreen&label=Sponsors)](https://github.com/sponsors/OpenXRay) [![Patreon](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F5950725&query=data.attributes.patron_count&suffix=%20Patrons&color=success&label=Patreon&style=flat)](https://patreon.com/openxray) +[![Sponsors](https://img.shields.io/github/sponsors/openxray?color=brightgreen&label=Sponsors)](https://github.com/sponsors/OpenXRay) [![Patreon](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F5950725&query=data.attributes.patron_count&suffix=%20Patrons&color=success&label=Patreon&style=flat)](https://patreon.com/openxray) [![Financial Contributors](https://opencollective.com/openxray/tiers/badge.svg?label=Financial%20contributors)](https://opencollective.com/openxray) You may provide financial support for this project by donating via different ways: -* [Boosty](https://boosty.to/openxray) – a large part of the team is located in Russia, if you have an ability to donate through Boosty, please use it, since we don't have an ability to withdraw funds from services like Patreon, etc. to our local Russian banking cards/accounts. -* [GitHub Sponsors](https://github.com/sponsors/OpenXRay), [Patreon](https://patreon.com/openxray), [Open Collective](https://opencollective.com/openxray) – funds raised from these services will be used to support our developers outside of Russia, and also we may use them to pay for paid services on GitHub, AppVeyor, etc. -* BTC: 363ZUoWcQe9fDvRPK9Kee2YuPdyhSFQpr2 -* ETH: 0x45a4fe8566e76946591e1eeabf190aa09b1cdb66 -* TRX: TGx7QAhTPsRcwnb4mwCtNDU7NF6kuoACpt -* Please, contact @1yohji in [our Discord](discord.gg/sjRMQwv) if you would like to use another cryptocurrency. +* [GitHub Sponsors](https://github.com/sponsors/OpenXRay), [Patreon](https://patreon.com/openxray), [Open Collective](https://opencollective.com/openxray) – many team members and contributors are located all over the world, and we provide a way to support of us worldwide. +* [Boosty](https://boosty.to/openxray) – a large part of the team and many contributors are located in Russia, and we provide a way to support us from Russia and Belarus. +* Please, contact @1yohji in [our Discord](discord.gg/sjRMQwv) if you would like to use cryptocurrency. +Raised funds will be used to support our developers and contributors, and also we may use them to pay for paid services on GitHub, etc. Thank you for your support! ## Thanks @@ -104,7 +102,7 @@ Thank you for your support! * Loxotron – for making the engine sources available. * [All the OpenXRay contributors](https://github.com/OpenXRay/xray-16/graphs/contributors) – for making the project what it is: * The OpenXRay team: - * [nitrocaster](https://github.com/nitrocaster) – original project founder. + * [nitrocaster](https://github.com/nitrocaster) – original project founder and former project leader (2014-2018). * [Kaffeine](https://github.com/Kaffeine) – initial work on the Linux port, refactoring, polishing. * [CrossVR](https://github.com/CrossVR) (Armada651) – creation of the OpenGL renderer, work on the build system, other project maintenance work. * [andrew-boyarshin](https://github.com/andrew-boyarshin) – work on the build system. From ebb73b0eaca0f887618b1d6adfbf57b9be57a71d Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 1 Jan 2026 16:28:48 +0300 Subject: [PATCH 153/261] Support sending info portions when switching PDA tabs One from X-Ray extensions (#2028): https://github.com/KD87/xray-extensions/blob/master/3967_cop_1602/pda_fix.asm One from SOC (#392) --- src/xrGame/ui/UIPdaWnd.cpp | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/xrGame/ui/UIPdaWnd.cpp b/src/xrGame/ui/UIPdaWnd.cpp index 9d6ba7fd9b0..d6f434ad239 100644 --- a/src/xrGame/ui/UIPdaWnd.cpp +++ b/src/xrGame/ui/UIPdaWnd.cpp @@ -280,21 +280,23 @@ void CUIPdaWnd::SetActiveSubdialog(const shared_str& section) m_pActiveDialog->Show(false); } - const std::tuple availableWindowsList[] = + const std::tuple availableWindowsList[] = { - { "eptMap", pUIMapWnd }, - { "eptTasks", pUITaskWnd }, - { "eptFractionWar", pUIFactionWarWnd }, - { "eptStatistics", pUIActorInfo }, - { "eptRanking", pUIRankingWnd }, - { "eptLogs", pUILogsWnd }, + { "eptMap", nullptr, pUIMapWnd }, + { "eptTasks", nullptr, pUITaskWnd }, + { "eptFractionWar", nullptr, pUIFactionWarWnd }, + { "eptStatistics", "ui_pda_actor_info", pUIActorInfo }, + { "eptRanking", nullptr, pUIRankingWnd }, + { "eptLogs", nullptr, pUILogsWnd }, }; - for (const auto& [id, wnd] : availableWindowsList) + pcstr soc_infoportion{}; + for (const auto& [id, info, wnd] : availableWindowsList) { if (section == id && wnd) { m_pActiveDialog = wnd; + soc_infoportion = info; break; } } @@ -309,13 +311,12 @@ void CUIPdaWnd::SetActiveSubdialog(const shared_str& section) } } - if (m_pActiveDialog == pUIActorInfo && pUIActorInfo) - { - InventoryUtilities::SendInfoToActor("ui_pda_actor_info"); - } - if (m_pActiveDialog) { + InventoryUtilities::SendInfoToActor(section.c_str()); // X-Ray extensions + if (soc_infoportion) + InventoryUtilities::SendInfoToActor(soc_infoportion); // SOC + if (!UIMainPdaFrame->IsChild(m_pActiveDialog)) UIMainPdaFrame->AttachChild(m_pActiveDialog); UIMainPdaFrame->SetKeyboardCapture(m_pActiveDialog, true); From 257ec312a03c5ba8b34efd26c9719081cd885de8 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 1 Jan 2026 16:28:48 +0300 Subject: [PATCH 154/261] Support sending info portions when switching PDA tabs One from X-Ray extensions (#2028): https://github.com/KD87/xray-extensions/blob/master/3967_cop_1602/pda_fix.asm One from SOC (#392) --- src/xrGame/ui/UIPdaWnd.cpp | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/xrGame/ui/UIPdaWnd.cpp b/src/xrGame/ui/UIPdaWnd.cpp index 9d6ba7fd9b0..d6f434ad239 100644 --- a/src/xrGame/ui/UIPdaWnd.cpp +++ b/src/xrGame/ui/UIPdaWnd.cpp @@ -280,21 +280,23 @@ void CUIPdaWnd::SetActiveSubdialog(const shared_str& section) m_pActiveDialog->Show(false); } - const std::tuple availableWindowsList[] = + const std::tuple availableWindowsList[] = { - { "eptMap", pUIMapWnd }, - { "eptTasks", pUITaskWnd }, - { "eptFractionWar", pUIFactionWarWnd }, - { "eptStatistics", pUIActorInfo }, - { "eptRanking", pUIRankingWnd }, - { "eptLogs", pUILogsWnd }, + { "eptMap", nullptr, pUIMapWnd }, + { "eptTasks", nullptr, pUITaskWnd }, + { "eptFractionWar", nullptr, pUIFactionWarWnd }, + { "eptStatistics", "ui_pda_actor_info", pUIActorInfo }, + { "eptRanking", nullptr, pUIRankingWnd }, + { "eptLogs", nullptr, pUILogsWnd }, }; - for (const auto& [id, wnd] : availableWindowsList) + pcstr soc_infoportion{}; + for (const auto& [id, info, wnd] : availableWindowsList) { if (section == id && wnd) { m_pActiveDialog = wnd; + soc_infoportion = info; break; } } @@ -309,13 +311,12 @@ void CUIPdaWnd::SetActiveSubdialog(const shared_str& section) } } - if (m_pActiveDialog == pUIActorInfo && pUIActorInfo) - { - InventoryUtilities::SendInfoToActor("ui_pda_actor_info"); - } - if (m_pActiveDialog) { + InventoryUtilities::SendInfoToActor(section.c_str()); // X-Ray extensions + if (soc_infoportion) + InventoryUtilities::SendInfoToActor(soc_infoportion); // SOC + if (!UIMainPdaFrame->IsChild(m_pActiveDialog)) UIMainPdaFrame->AttachChild(m_pActiveDialog); UIMainPdaFrame->SetKeyboardCapture(m_pActiveDialog, true); From 2c431a97e8cdd204fc626d552aab1876e3c759f4 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 1 Jan 2026 19:51:51 +0300 Subject: [PATCH 155/261] xrEngine/xr_level_controller.cpp: make IsBinded and GetActionDik safe --- src/xrEngine/xr_level_controller.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/xrEngine/xr_level_controller.cpp b/src/xrEngine/xr_level_controller.cpp index f3f2444661d..e59a1154bf0 100644 --- a/src/xrEngine/xr_level_controller.cpp +++ b/src/xrEngine/xr_level_controller.cpp @@ -666,6 +666,9 @@ game_action* ActionNameToPtr(pcstr name, [[maybe_unused]] bool silent /*= false* bool IsBinded(EGameActions action_id, int dik, EKeyContext context /*= EKeyContext::Undefined*/) { + if (action_id >= kNOTBINDED) + return false; + key_binding* binding = &g_key_bindings[action_id]; for (u8 i = 0; i < bindtypes_count; ++i) { @@ -678,6 +681,9 @@ bool IsBinded(EGameActions action_id, int dik, EKeyContext context /*= EKeyConte int GetActionDik(EGameActions action_id, int idx) { + if (action_id >= kNOTBINDED) + return SDL_SCANCODE_UNKNOWN; + key_binding* binding = &g_key_bindings[action_id]; if (idx == -1) From 75ae7ed8302fd26fc69549fe15b8b7bdf4ab77cd Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 1 Jan 2026 19:51:51 +0300 Subject: [PATCH 156/261] xrEngine/xr_level_controller.cpp: make IsBinded and GetActionDik safe --- src/xrEngine/xr_level_controller.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/xrEngine/xr_level_controller.cpp b/src/xrEngine/xr_level_controller.cpp index f3f2444661d..e59a1154bf0 100644 --- a/src/xrEngine/xr_level_controller.cpp +++ b/src/xrEngine/xr_level_controller.cpp @@ -666,6 +666,9 @@ game_action* ActionNameToPtr(pcstr name, [[maybe_unused]] bool silent /*= false* bool IsBinded(EGameActions action_id, int dik, EKeyContext context /*= EKeyContext::Undefined*/) { + if (action_id >= kNOTBINDED) + return false; + key_binding* binding = &g_key_bindings[action_id]; for (u8 i = 0; i < bindtypes_count; ++i) { @@ -678,6 +681,9 @@ bool IsBinded(EGameActions action_id, int dik, EKeyContext context /*= EKeyConte int GetActionDik(EGameActions action_id, int idx) { + if (action_id >= kNOTBINDED) + return SDL_SCANCODE_UNKNOWN; + key_binding* binding = &g_key_bindings[action_id]; if (idx == -1) From bb64531b56283c8cceea6431ba31be01b309df09 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 1 Jan 2026 19:53:12 +0300 Subject: [PATCH 157/261] Port CS/COP X-Ray Extensions script functions (#2028) --- .../key_binding_registrator_script.cpp | 2 + src/xrGame/Actor.h | 29 ++++--------- src/xrGame/CarScript.cpp | 7 ++++ src/xrGame/game_object_space.h | 22 +++++----- src/xrGame/level_script.cpp | 42 +++++++++++++++++-- src/xrGame/script_game_object_script.cpp | 10 ++++- src/xrGame/script_game_object_script3.cpp | 1 + src/xrGame/ui/UIMainIngameWnd.cpp | 1 + src/xrGame/ui/UIMainIngameWnd.h | 2 + src/xrPhysics/PHWorldScript.cpp | 9 ++++ src/xrScriptEngine/ScriptEngineScript.cpp | 8 ++++ src/xrUICore/ui_export_script.cpp | 9 ++++ 12 files changed, 107 insertions(+), 35 deletions(-) diff --git a/src/xrEngine/key_binding_registrator_script.cpp b/src/xrEngine/key_binding_registrator_script.cpp index dde638ec05f..e3b167274df 100644 --- a/src/xrEngine/key_binding_registrator_script.cpp +++ b/src/xrEngine/key_binding_registrator_script.cpp @@ -15,6 +15,8 @@ void key_binding_registrator::script_register(lua_State* luaState) [ def("dik_to_bind", +[](int dik) -> int { return GetBindedAction(dik); }), def("dik_to_bind", +[](int dik, int ctx) -> int { return GetBindedAction(dik, (EKeyContext)ctx); }), + def("bind_to_dik", +[](int action) { return GetActionDik((EGameActions)action);}), + def("bind_to_dik", +[](int action, int idx) { return GetActionDik((EGameActions)action, idx);}), class_("key_bindings_context") .enum_("context") diff --git a/src/xrGame/Actor.h b/src/xrGame/Actor.h index 20df7cdd470..e0cbc5ca8a7 100644 --- a/src/xrGame/Actor.h +++ b/src/xrGame/Actor.h @@ -414,6 +414,15 @@ class CActor : public CEntityAlive, bool AnyMove() { return (mstate_real & mcAnyMove) != 0; }; bool is_jump(); u32 MovingState() const { return mstate_real; } + + [[nodiscard]] + u32 GetBodyState() const { return mstate_real; } + void SetBodyState(const u32 state) { mstate_real = state; } + + [[nodiscard]] + u32 GetWishfulBodyState() const { return mstate_wishful; } + void SetWishfulBodyState(const u32 state) { mstate_wishful = state; } + protected: u32 mstate_wishful; u32 mstate_old; @@ -786,26 +795,6 @@ class CActor : public CEntityAlive, bool m_inventory_disabled; // static CPhysicsShell *actor_camera_shell; - IC u32 get_state() const - { - return this->mstate_real; - } - - IC void set_state(u32 state) - { - mstate_real = state; - } - - IC u32 get_state_wishful() const - { - return this->mstate_wishful; - } - - IC void set_state_wishful(u32 state) - { - mstate_wishful = state; - } - private: DECLARE_SCRIPT_REGISTER_FUNCTION(CGameObject); }; diff --git a/src/xrGame/CarScript.cpp b/src/xrGame/CarScript.cpp index d828eb5302f..9b5ee8377e6 100644 --- a/src/xrGame/CarScript.cpp +++ b/src/xrGame/CarScript.cpp @@ -34,6 +34,13 @@ void CCar::script_register(lua_State* luaState) .def("SetExplodeTime", &CCar::SetExplodeTime) .def("ExplodeTime", &CCar::ExplodeTime) .def("CarExplode", &CCar::CarExplode) + // X-Ray Extensions: + .def("get_fuel", &CCar::GetfFuel) + .def("set_fuel", &CCar::SetfFuel) + .def("get_fuel_tank", &CCar::GetfFuelTank) + .def("set_fuel_tank", &CCar::SetfFuelTank) + .def("get_fuel_consumption", &CCar::GetfFuelConsumption) + .def("set_fuel_consumption", &CCar::SetfFuelConsumption) /***** added by Ray Twitty (aka Shadows) START *****/ .def("GetfFuel", &CCar::GetfFuel) .def("SetfFuel", &CCar::SetfFuel) diff --git a/src/xrGame/game_object_space.h b/src/xrGame/game_object_space.h index 0419aee13d0..4882ed5e237 100644 --- a/src/xrGame/game_object_space.h +++ b/src/xrGame/game_object_space.h @@ -58,16 +58,6 @@ enum ECallbackType : u32 eActorHudAnimationEnd, //AVO: custom callbacks - // Input - eKeyPress, - eKeyRelease, - eKeyHold, - eMouseMove, - eMouseWheel, - eControllerPress, - eControllerRelease, - eControllerHold, - eControllerAttitudeChange, // Inventory eItemToBelt, eItemToSlot, @@ -85,6 +75,18 @@ enum ECallbackType : u32 eUseVehicle, //-AVO + eControllerPress, + eControllerRelease, + eControllerHold, + eControllerAttitudeChange, + + // X-Ray Extensions: + eKeyPress = 123, + eKeyRelease = 124, + eKeyHold = 125, + eMouseWheel = 126, + eMouseMove = 127, + eDummy = u32(-1), }; }; diff --git a/src/xrGame/level_script.cpp b/src/xrGame/level_script.cpp index 204941cf8dd..b6fb3df8b08 100644 --- a/src/xrGame/level_script.cpp +++ b/src/xrGame/level_script.cpp @@ -37,6 +37,8 @@ #include "xrServer_Objects_ALife_Monsters.h" #include "HUDManager.h" #include "raypick.h" +#include "ui/UIMainIngameWnd.h" +#include "UIZoneMap.h" #include "xrCDB/xr_collide_defs.h" #include "xrNetServer/NET_Messages.h" @@ -730,11 +732,45 @@ void CLevel::script_register(lua_State* luaState) module(luaState, "level") [ + // X-Ray Extensions: + def("get_target_obj", &g_get_target_obj), + def("get_target_dist", &g_get_target_dist), + def("get_target_element", &g_get_target_element), // Can get bone cursor is targeting + def("get_actor_body_state", +[]() -> u32 + { + if (!g_actor) + return 0; + return g_actor->GetBodyState(); + }), + def("get_actor_body_state_wishful", +[]() -> u32 + { + if (!g_actor) + return 0; + return g_actor->GetWishfulBodyState(); + }), + def("get_fov", +[] + { + return g_fov; + }), + def("set_fov", +[](float fov) + { + g_fov = fov; + }), + def("show_minimap", +[] + { + CurrentGameUI()->UIMainIngameWnd->ShowZoneMap(true); + }), + def("hide_minimap", +[] + { + CurrentGameUI()->UIMainIngameWnd->ShowZoneMap(false); + }), + def("minimap_shown", +[] + { + return CurrentGameUI()->UIMainIngameWnd->IsZoneMapShown(); + }), + //Alundaio: Extend level namespace exports def("send", &g_send) , //allow the ability to send netpacket to level - def("get_target_obj", &g_get_target_obj), //intentionally named to what is in xray extensions - def("get_target_dist", &g_get_target_dist), - def("get_target_element", &g_get_target_element), //Can get bone cursor is targeting def("spawn_item", &spawn_section), def("get_active_cam", &get_active_cam), def("set_active_cam", &set_active_cam), diff --git a/src/xrGame/script_game_object_script.cpp b/src/xrGame/script_game_object_script.cpp index 001af6d19e1..926fb1da28d 100644 --- a/src/xrGame/script_game_object_script.cpp +++ b/src/xrGame/script_game_object_script.cpp @@ -72,6 +72,8 @@ void CScriptGameObject::script_register(lua_State* luaState) value("inventory_pda", int(GameObject::eInventoryPda)), value("inventory_info", int(GameObject::eInventoryInfo)), value("article_info", int(GameObject::eArticleInfo)), + value("task_state", int(GameObject::eTaskStateChange)), + value("map_location_added", int(GameObject::eMapLocationAdded)), value("use_object", int(GameObject::eUseObject)), value("hit", int(GameObject::eHit)), value("sound", int(GameObject::eSound)), @@ -88,7 +90,6 @@ void CScriptGameObject::script_register(lua_State* luaState) value("on_item_take", int(GameObject::eOnItemTake)), value("on_item_drop", int(GameObject::eOnItemDrop)), value("script_animation", int(GameObject::eScriptAnimation)), - value("task_state", int(GameObject::eTaskStateChange)), value("take_item_from_box", int(GameObject::eInvBoxItemTake)), value("weapon_no_ammo", int(GameObject::eWeaponNoAmmoAvailable)), @@ -121,7 +122,12 @@ void CScriptGameObject::script_register(lua_State* luaState) value("item_to_ruck", int(GameObject::eItemToRuck)), //-AVO - value("map_location_added", int(GameObject::eMapLocationAdded)) + // X-Ray Extensions: + value("on_key_press", int(GameObject::eKeyPress)), + value("on_key_release", int(GameObject::eKeyRelease)), + value("on_key_hold", int(GameObject::eKeyHold)), + value("on_mouse_wheel", int(GameObject::eMouseWheel)), + value("on_mouse_move", int(GameObject::eMouseMove)) ], def("buy_condition", (void (*)(CScriptIniFile*, pcstr))(&::buy_condition)), diff --git a/src/xrGame/script_game_object_script3.cpp b/src/xrGame/script_game_object_script3.cpp index 94f04eaa298..2f9ef7695dd 100644 --- a/src/xrGame/script_game_object_script3.cpp +++ b/src/xrGame/script_game_object_script3.cpp @@ -573,6 +573,7 @@ luabind::class_& script_register_game_object2(luabind::class_ .def("is_on_belt", &CScriptGameObject::IsOnBelt) .def("item_on_belt", &CScriptGameObject::ItemOnBelt) .def("belt_count", &CScriptGameObject::BeltSize) + .def("belt_object_count", &CScriptGameObject::BeltSize) .def("get_actor_max_weight", &CScriptGameObject::GetActorMaxWeight) .def("set_actor_max_weight", &CScriptGameObject::SetActorMaxWeight) diff --git a/src/xrGame/ui/UIMainIngameWnd.cpp b/src/xrGame/ui/UIMainIngameWnd.cpp index 633acc52355..d0c83f5c0fb 100644 --- a/src/xrGame/ui/UIMainIngameWnd.cpp +++ b/src/xrGame/ui/UIMainIngameWnd.cpp @@ -630,6 +630,7 @@ void CUIMainIngameWnd::reset_ui() } } +bool CUIMainIngameWnd::IsZoneMapShown() { return UIZoneMap->visible; } void CUIMainIngameWnd::ShowZoneMap(bool status) { UIZoneMap->visible = status; } void CUIMainIngameWnd::DrawZoneMap() { UIZoneMap->Render(); } void CUIMainIngameWnd::UpdateZoneMap() { UIZoneMap->Update(); } diff --git a/src/xrGame/ui/UIMainIngameWnd.h b/src/xrGame/ui/UIMainIngameWnd.h index b86844be5e6..9adcc214f94 100644 --- a/src/xrGame/ui/UIMainIngameWnd.h +++ b/src/xrGame/ui/UIMainIngameWnd.h @@ -55,6 +55,8 @@ class CUIMainIngameWnd final : public CUIWindow CUIArtefactPanel* UIArtefactPanel{}; + [[nodiscard]] + bool IsZoneMapShown(); void ShowZoneMap(bool status); void DrawZoneMap(); void UpdateZoneMap(); diff --git a/src/xrPhysics/PHWorldScript.cpp b/src/xrPhysics/PHWorldScript.cpp index d13f6b70e22..bb4e6bd0a01 100644 --- a/src/xrPhysics/PHWorldScript.cpp +++ b/src/xrPhysics/PHWorldScript.cpp @@ -23,6 +23,15 @@ void CPHWorld::script_register(lua_State* luaState) def("physics_world", +[] { return ph_world; + }), + // X-Ray Extensions: + def("get_ph_time_factor", +[] + { + return phTimefactor; + }), + def("set_ph_time_factor", +[](float time_factor) + { + return phTimefactor = time_factor; }) ]; } diff --git a/src/xrScriptEngine/ScriptEngineScript.cpp b/src/xrScriptEngine/ScriptEngineScript.cpp index 033aed09345..de681261b0f 100644 --- a/src/xrScriptEngine/ScriptEngineScript.cpp +++ b/src/xrScriptEngine/ScriptEngineScript.cpp @@ -50,8 +50,16 @@ void CScriptEngine::script_register(lua_State* luaState) module(luaState) [ def("log", &LuaLog), + def("log1", +[](pcstr message) // X-Ray Extensions + { + Msg("%s", message); + }), def("error_log", &ErrorLog), def("flush", &FlushLogs), + def("flush1", +[] // X-Ray Extensions + { + FlushLog(); + }), def("print_stack", +[]() { GEnv.ScriptEngine->print_stack(); diff --git a/src/xrUICore/ui_export_script.cpp b/src/xrUICore/ui_export_script.cpp index e271f0b4333..913957bbae1 100644 --- a/src/xrUICore/ui_export_script.cpp +++ b/src/xrUICore/ui_export_script.cpp @@ -273,6 +273,15 @@ void CUIWindow::script_register(lua_State* luaState) .def("SetFont", &CUIWindow::SetFont) .def("GetFont", &CUIWindow::GetFont) + .def("GetMouseX", +[](CUIWindow* self) // X-Ray Extensions + { + return self->cursor_pos.x; + }) + .def("GetMouseY", +[](CUIWindow* self) // X-Ray Extensions + { + return self->cursor_pos.y; + }) + .def("WindowName", +[](CUIWindow* self) -> pcstr { return self->WindowName().c_str(); }) .def("SetWindowName", &CUIWindow::SetWindowName), From ba1ad279dae6e5352e7c0df9e332beb35e359dc6 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 1 Jan 2026 19:53:12 +0300 Subject: [PATCH 158/261] Port CS/COP X-Ray Extensions script functions (#2028) --- .../key_binding_registrator_script.cpp | 2 + src/xrGame/Actor.h | 29 ++++--------- src/xrGame/CarScript.cpp | 7 ++++ src/xrGame/game_object_space.h | 22 +++++----- src/xrGame/level_script.cpp | 42 +++++++++++++++++-- src/xrGame/script_game_object_script.cpp | 10 ++++- src/xrGame/script_game_object_script3.cpp | 1 + src/xrGame/ui/UIMainIngameWnd.cpp | 1 + src/xrGame/ui/UIMainIngameWnd.h | 2 + src/xrPhysics/PHWorldScript.cpp | 9 ++++ src/xrScriptEngine/ScriptEngineScript.cpp | 8 ++++ src/xrUICore/ui_export_script.cpp | 9 ++++ 12 files changed, 107 insertions(+), 35 deletions(-) diff --git a/src/xrEngine/key_binding_registrator_script.cpp b/src/xrEngine/key_binding_registrator_script.cpp index dde638ec05f..e3b167274df 100644 --- a/src/xrEngine/key_binding_registrator_script.cpp +++ b/src/xrEngine/key_binding_registrator_script.cpp @@ -15,6 +15,8 @@ void key_binding_registrator::script_register(lua_State* luaState) [ def("dik_to_bind", +[](int dik) -> int { return GetBindedAction(dik); }), def("dik_to_bind", +[](int dik, int ctx) -> int { return GetBindedAction(dik, (EKeyContext)ctx); }), + def("bind_to_dik", +[](int action) { return GetActionDik((EGameActions)action);}), + def("bind_to_dik", +[](int action, int idx) { return GetActionDik((EGameActions)action, idx);}), class_("key_bindings_context") .enum_("context") diff --git a/src/xrGame/Actor.h b/src/xrGame/Actor.h index 20df7cdd470..e0cbc5ca8a7 100644 --- a/src/xrGame/Actor.h +++ b/src/xrGame/Actor.h @@ -414,6 +414,15 @@ class CActor : public CEntityAlive, bool AnyMove() { return (mstate_real & mcAnyMove) != 0; }; bool is_jump(); u32 MovingState() const { return mstate_real; } + + [[nodiscard]] + u32 GetBodyState() const { return mstate_real; } + void SetBodyState(const u32 state) { mstate_real = state; } + + [[nodiscard]] + u32 GetWishfulBodyState() const { return mstate_wishful; } + void SetWishfulBodyState(const u32 state) { mstate_wishful = state; } + protected: u32 mstate_wishful; u32 mstate_old; @@ -786,26 +795,6 @@ class CActor : public CEntityAlive, bool m_inventory_disabled; // static CPhysicsShell *actor_camera_shell; - IC u32 get_state() const - { - return this->mstate_real; - } - - IC void set_state(u32 state) - { - mstate_real = state; - } - - IC u32 get_state_wishful() const - { - return this->mstate_wishful; - } - - IC void set_state_wishful(u32 state) - { - mstate_wishful = state; - } - private: DECLARE_SCRIPT_REGISTER_FUNCTION(CGameObject); }; diff --git a/src/xrGame/CarScript.cpp b/src/xrGame/CarScript.cpp index d828eb5302f..9b5ee8377e6 100644 --- a/src/xrGame/CarScript.cpp +++ b/src/xrGame/CarScript.cpp @@ -34,6 +34,13 @@ void CCar::script_register(lua_State* luaState) .def("SetExplodeTime", &CCar::SetExplodeTime) .def("ExplodeTime", &CCar::ExplodeTime) .def("CarExplode", &CCar::CarExplode) + // X-Ray Extensions: + .def("get_fuel", &CCar::GetfFuel) + .def("set_fuel", &CCar::SetfFuel) + .def("get_fuel_tank", &CCar::GetfFuelTank) + .def("set_fuel_tank", &CCar::SetfFuelTank) + .def("get_fuel_consumption", &CCar::GetfFuelConsumption) + .def("set_fuel_consumption", &CCar::SetfFuelConsumption) /***** added by Ray Twitty (aka Shadows) START *****/ .def("GetfFuel", &CCar::GetfFuel) .def("SetfFuel", &CCar::SetfFuel) diff --git a/src/xrGame/game_object_space.h b/src/xrGame/game_object_space.h index 0419aee13d0..4882ed5e237 100644 --- a/src/xrGame/game_object_space.h +++ b/src/xrGame/game_object_space.h @@ -58,16 +58,6 @@ enum ECallbackType : u32 eActorHudAnimationEnd, //AVO: custom callbacks - // Input - eKeyPress, - eKeyRelease, - eKeyHold, - eMouseMove, - eMouseWheel, - eControllerPress, - eControllerRelease, - eControllerHold, - eControllerAttitudeChange, // Inventory eItemToBelt, eItemToSlot, @@ -85,6 +75,18 @@ enum ECallbackType : u32 eUseVehicle, //-AVO + eControllerPress, + eControllerRelease, + eControllerHold, + eControllerAttitudeChange, + + // X-Ray Extensions: + eKeyPress = 123, + eKeyRelease = 124, + eKeyHold = 125, + eMouseWheel = 126, + eMouseMove = 127, + eDummy = u32(-1), }; }; diff --git a/src/xrGame/level_script.cpp b/src/xrGame/level_script.cpp index 204941cf8dd..b6fb3df8b08 100644 --- a/src/xrGame/level_script.cpp +++ b/src/xrGame/level_script.cpp @@ -37,6 +37,8 @@ #include "xrServer_Objects_ALife_Monsters.h" #include "HUDManager.h" #include "raypick.h" +#include "ui/UIMainIngameWnd.h" +#include "UIZoneMap.h" #include "xrCDB/xr_collide_defs.h" #include "xrNetServer/NET_Messages.h" @@ -730,11 +732,45 @@ void CLevel::script_register(lua_State* luaState) module(luaState, "level") [ + // X-Ray Extensions: + def("get_target_obj", &g_get_target_obj), + def("get_target_dist", &g_get_target_dist), + def("get_target_element", &g_get_target_element), // Can get bone cursor is targeting + def("get_actor_body_state", +[]() -> u32 + { + if (!g_actor) + return 0; + return g_actor->GetBodyState(); + }), + def("get_actor_body_state_wishful", +[]() -> u32 + { + if (!g_actor) + return 0; + return g_actor->GetWishfulBodyState(); + }), + def("get_fov", +[] + { + return g_fov; + }), + def("set_fov", +[](float fov) + { + g_fov = fov; + }), + def("show_minimap", +[] + { + CurrentGameUI()->UIMainIngameWnd->ShowZoneMap(true); + }), + def("hide_minimap", +[] + { + CurrentGameUI()->UIMainIngameWnd->ShowZoneMap(false); + }), + def("minimap_shown", +[] + { + return CurrentGameUI()->UIMainIngameWnd->IsZoneMapShown(); + }), + //Alundaio: Extend level namespace exports def("send", &g_send) , //allow the ability to send netpacket to level - def("get_target_obj", &g_get_target_obj), //intentionally named to what is in xray extensions - def("get_target_dist", &g_get_target_dist), - def("get_target_element", &g_get_target_element), //Can get bone cursor is targeting def("spawn_item", &spawn_section), def("get_active_cam", &get_active_cam), def("set_active_cam", &set_active_cam), diff --git a/src/xrGame/script_game_object_script.cpp b/src/xrGame/script_game_object_script.cpp index 001af6d19e1..926fb1da28d 100644 --- a/src/xrGame/script_game_object_script.cpp +++ b/src/xrGame/script_game_object_script.cpp @@ -72,6 +72,8 @@ void CScriptGameObject::script_register(lua_State* luaState) value("inventory_pda", int(GameObject::eInventoryPda)), value("inventory_info", int(GameObject::eInventoryInfo)), value("article_info", int(GameObject::eArticleInfo)), + value("task_state", int(GameObject::eTaskStateChange)), + value("map_location_added", int(GameObject::eMapLocationAdded)), value("use_object", int(GameObject::eUseObject)), value("hit", int(GameObject::eHit)), value("sound", int(GameObject::eSound)), @@ -88,7 +90,6 @@ void CScriptGameObject::script_register(lua_State* luaState) value("on_item_take", int(GameObject::eOnItemTake)), value("on_item_drop", int(GameObject::eOnItemDrop)), value("script_animation", int(GameObject::eScriptAnimation)), - value("task_state", int(GameObject::eTaskStateChange)), value("take_item_from_box", int(GameObject::eInvBoxItemTake)), value("weapon_no_ammo", int(GameObject::eWeaponNoAmmoAvailable)), @@ -121,7 +122,12 @@ void CScriptGameObject::script_register(lua_State* luaState) value("item_to_ruck", int(GameObject::eItemToRuck)), //-AVO - value("map_location_added", int(GameObject::eMapLocationAdded)) + // X-Ray Extensions: + value("on_key_press", int(GameObject::eKeyPress)), + value("on_key_release", int(GameObject::eKeyRelease)), + value("on_key_hold", int(GameObject::eKeyHold)), + value("on_mouse_wheel", int(GameObject::eMouseWheel)), + value("on_mouse_move", int(GameObject::eMouseMove)) ], def("buy_condition", (void (*)(CScriptIniFile*, pcstr))(&::buy_condition)), diff --git a/src/xrGame/script_game_object_script3.cpp b/src/xrGame/script_game_object_script3.cpp index 94f04eaa298..2f9ef7695dd 100644 --- a/src/xrGame/script_game_object_script3.cpp +++ b/src/xrGame/script_game_object_script3.cpp @@ -573,6 +573,7 @@ luabind::class_& script_register_game_object2(luabind::class_ .def("is_on_belt", &CScriptGameObject::IsOnBelt) .def("item_on_belt", &CScriptGameObject::ItemOnBelt) .def("belt_count", &CScriptGameObject::BeltSize) + .def("belt_object_count", &CScriptGameObject::BeltSize) .def("get_actor_max_weight", &CScriptGameObject::GetActorMaxWeight) .def("set_actor_max_weight", &CScriptGameObject::SetActorMaxWeight) diff --git a/src/xrGame/ui/UIMainIngameWnd.cpp b/src/xrGame/ui/UIMainIngameWnd.cpp index 633acc52355..d0c83f5c0fb 100644 --- a/src/xrGame/ui/UIMainIngameWnd.cpp +++ b/src/xrGame/ui/UIMainIngameWnd.cpp @@ -630,6 +630,7 @@ void CUIMainIngameWnd::reset_ui() } } +bool CUIMainIngameWnd::IsZoneMapShown() { return UIZoneMap->visible; } void CUIMainIngameWnd::ShowZoneMap(bool status) { UIZoneMap->visible = status; } void CUIMainIngameWnd::DrawZoneMap() { UIZoneMap->Render(); } void CUIMainIngameWnd::UpdateZoneMap() { UIZoneMap->Update(); } diff --git a/src/xrGame/ui/UIMainIngameWnd.h b/src/xrGame/ui/UIMainIngameWnd.h index b86844be5e6..9adcc214f94 100644 --- a/src/xrGame/ui/UIMainIngameWnd.h +++ b/src/xrGame/ui/UIMainIngameWnd.h @@ -55,6 +55,8 @@ class CUIMainIngameWnd final : public CUIWindow CUIArtefactPanel* UIArtefactPanel{}; + [[nodiscard]] + bool IsZoneMapShown(); void ShowZoneMap(bool status); void DrawZoneMap(); void UpdateZoneMap(); diff --git a/src/xrPhysics/PHWorldScript.cpp b/src/xrPhysics/PHWorldScript.cpp index d13f6b70e22..bb4e6bd0a01 100644 --- a/src/xrPhysics/PHWorldScript.cpp +++ b/src/xrPhysics/PHWorldScript.cpp @@ -23,6 +23,15 @@ void CPHWorld::script_register(lua_State* luaState) def("physics_world", +[] { return ph_world; + }), + // X-Ray Extensions: + def("get_ph_time_factor", +[] + { + return phTimefactor; + }), + def("set_ph_time_factor", +[](float time_factor) + { + return phTimefactor = time_factor; }) ]; } diff --git a/src/xrScriptEngine/ScriptEngineScript.cpp b/src/xrScriptEngine/ScriptEngineScript.cpp index 033aed09345..de681261b0f 100644 --- a/src/xrScriptEngine/ScriptEngineScript.cpp +++ b/src/xrScriptEngine/ScriptEngineScript.cpp @@ -50,8 +50,16 @@ void CScriptEngine::script_register(lua_State* luaState) module(luaState) [ def("log", &LuaLog), + def("log1", +[](pcstr message) // X-Ray Extensions + { + Msg("%s", message); + }), def("error_log", &ErrorLog), def("flush", &FlushLogs), + def("flush1", +[] // X-Ray Extensions + { + FlushLog(); + }), def("print_stack", +[]() { GEnv.ScriptEngine->print_stack(); diff --git a/src/xrUICore/ui_export_script.cpp b/src/xrUICore/ui_export_script.cpp index e271f0b4333..913957bbae1 100644 --- a/src/xrUICore/ui_export_script.cpp +++ b/src/xrUICore/ui_export_script.cpp @@ -273,6 +273,15 @@ void CUIWindow::script_register(lua_State* luaState) .def("SetFont", &CUIWindow::SetFont) .def("GetFont", &CUIWindow::GetFont) + .def("GetMouseX", +[](CUIWindow* self) // X-Ray Extensions + { + return self->cursor_pos.x; + }) + .def("GetMouseY", +[](CUIWindow* self) // X-Ray Extensions + { + return self->cursor_pos.y; + }) + .def("WindowName", +[](CUIWindow* self) -> pcstr { return self->WindowName().c_str(); }) .def("SetWindowName", &CUIWindow::SetWindowName), From 6eb834894b12494e0c47616a0b4e405405aec06e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 29 Jan 2026 13:18:54 +0300 Subject: [PATCH 159/261] xrCore/_color.h: make few functions noexcept --- src/xrCore/_color.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xrCore/_color.h b/src/xrCore/_color.h index 75e16a5a21b..e70cff7eb98 100644 --- a/src/xrCore/_color.h +++ b/src/xrCore/_color.h @@ -45,14 +45,14 @@ ICF u32 color_rgba_f(f32 r, f32 g, f32 b, f32 a) noexcept return color_argb_f(a, r, g, b); } -constexpr u32 color_xrgb(u32 r, u32 g, u32 b) { return color_argb(0xff, r, g, b); } +constexpr u32 color_xrgb(u32 r, u32 g, u32 b) noexcept { return color_argb(0xff, r, g, b); } constexpr u32 color_get_R(u32 rgba) noexcept { return ((rgba >> 16) & 0xff); } constexpr u32 color_get_G(u32 rgba) noexcept { return ((rgba >> 8) & 0xff); } constexpr u32 color_get_B(u32 rgba) noexcept { return (rgba & 0xff); } constexpr u32 color_get_A(u32 rgba) noexcept { return (rgba >> 24); } constexpr u32 subst_alpha(u32 rgba, u32 a) { return (rgba & ~color_rgba(0, 0, 0, 0xff)) | color_rgba(0, 0, 0, a); } -constexpr u32 bgr2rgb(u32 bgr) { return color_rgba(color_get_B(bgr), color_get_G(bgr), color_get_R(bgr), 0); } -constexpr u32 rgb2bgr(u32 rgb) { return bgr2rgb(rgb); } +constexpr u32 bgr2rgb(u32 bgr) noexcept { return color_rgba(color_get_B(bgr), color_get_G(bgr), color_get_R(bgr), 0); } +constexpr u32 rgb2bgr(u32 rgb) noexcept { return bgr2rgb(rgb); } struct Fcolor { From c5703f716d374de8bb522ce46de40b2c57982e43 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 29 Jan 2026 13:18:54 +0300 Subject: [PATCH 160/261] xrCore/_color.h: make few functions noexcept --- src/xrCore/_color.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xrCore/_color.h b/src/xrCore/_color.h index 75e16a5a21b..e70cff7eb98 100644 --- a/src/xrCore/_color.h +++ b/src/xrCore/_color.h @@ -45,14 +45,14 @@ ICF u32 color_rgba_f(f32 r, f32 g, f32 b, f32 a) noexcept return color_argb_f(a, r, g, b); } -constexpr u32 color_xrgb(u32 r, u32 g, u32 b) { return color_argb(0xff, r, g, b); } +constexpr u32 color_xrgb(u32 r, u32 g, u32 b) noexcept { return color_argb(0xff, r, g, b); } constexpr u32 color_get_R(u32 rgba) noexcept { return ((rgba >> 16) & 0xff); } constexpr u32 color_get_G(u32 rgba) noexcept { return ((rgba >> 8) & 0xff); } constexpr u32 color_get_B(u32 rgba) noexcept { return (rgba & 0xff); } constexpr u32 color_get_A(u32 rgba) noexcept { return (rgba >> 24); } constexpr u32 subst_alpha(u32 rgba, u32 a) { return (rgba & ~color_rgba(0, 0, 0, 0xff)) | color_rgba(0, 0, 0, a); } -constexpr u32 bgr2rgb(u32 bgr) { return color_rgba(color_get_B(bgr), color_get_G(bgr), color_get_R(bgr), 0); } -constexpr u32 rgb2bgr(u32 rgb) { return bgr2rgb(rgb); } +constexpr u32 bgr2rgb(u32 bgr) noexcept { return color_rgba(color_get_B(bgr), color_get_G(bgr), color_get_R(bgr), 0); } +constexpr u32 rgb2bgr(u32 rgb) noexcept { return bgr2rgb(rgb); } struct Fcolor { From ba9a2ea852610f5262321739253b98c06b8316af Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 29 Jan 2026 13:19:40 +0300 Subject: [PATCH 161/261] Fix missing closing tag in pol/openxray.xml (fixes #2034) --- res/gamedata/configs/text/pol/openxray.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/gamedata/configs/text/pol/openxray.xml b/res/gamedata/configs/text/pol/openxray.xml index a308924956f..30348892cdd 100644 --- a/res/gamedata/configs/text/pol/openxray.xml +++ b/res/gamedata/configs/text/pol/openxray.xml @@ -95,7 +95,7 @@ Czeski (Czech) - Angielski (English) From abc69d77c02bc6dc4eceda0d630dbfb19b5146fd Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Thu, 29 Jan 2026 13:19:40 +0300 Subject: [PATCH 162/261] Fix missing closing tag in pol/openxray.xml (fixes #2034) --- res/gamedata/configs/text/pol/openxray.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/gamedata/configs/text/pol/openxray.xml b/res/gamedata/configs/text/pol/openxray.xml index a308924956f..30348892cdd 100644 --- a/res/gamedata/configs/text/pol/openxray.xml +++ b/res/gamedata/configs/text/pol/openxray.xml @@ -95,7 +95,7 @@ Czeski (Czech) - Angielski (English) From d2980d18e5fb85225932dbf2182da75cd19ceea3 Mon Sep 17 00:00:00 2001 From: Igor Anpilogov Date: Thu, 29 Jan 2026 11:59:02 +0100 Subject: [PATCH 163/261] Fixup VSCode tasks (#2026) --- .vscode/launch.json | 6 +++--- .vscode/tasks.json | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c1cd7c3db28..f9e31d4f402 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -103,10 +103,10 @@ "options": [ "Debug", "Release", - "MinSizeRel", - "RelWithDebInfo" + "Mixed", + "ReleaseMasterGold" ], - "default": "Debug" + "default": "ReleaseMasterGold" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 08472cb6c7a..785d5c4bc77 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -51,7 +51,7 @@ "options": { "cwd": "${workspaceFolder}/bin" }, - "command": "cmake --build . -j${CPU_COUNT}", + "command": "cmake --build . -j$(nproc --all)", "dependsOn": [ "Configure" ] @@ -101,10 +101,10 @@ "options": [ "Debug", "Release", - "MinSizeRel", - "RelWithDebInfo" + "Mixed", + "ReleaseMasterGold" ], - "default": "Debug" + "default": "ReleaseMasterGold" } ] } From bc847d19bcc7c6dc50b455417881f9365090fdf0 Mon Sep 17 00:00:00 2001 From: Igor Anpilogov Date: Thu, 29 Jan 2026 11:59:02 +0100 Subject: [PATCH 164/261] Fixup VSCode tasks (#2026) --- .vscode/launch.json | 6 +++--- .vscode/tasks.json | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c1cd7c3db28..f9e31d4f402 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -103,10 +103,10 @@ "options": [ "Debug", "Release", - "MinSizeRel", - "RelWithDebInfo" + "Mixed", + "ReleaseMasterGold" ], - "default": "Debug" + "default": "ReleaseMasterGold" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 08472cb6c7a..785d5c4bc77 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -51,7 +51,7 @@ "options": { "cwd": "${workspaceFolder}/bin" }, - "command": "cmake --build . -j${CPU_COUNT}", + "command": "cmake --build . -j$(nproc --all)", "dependsOn": [ "Configure" ] @@ -101,10 +101,10 @@ "options": [ "Debug", "Release", - "MinSizeRel", - "RelWithDebInfo" + "Mixed", + "ReleaseMasterGold" ], - "default": "Debug" + "default": "ReleaseMasterGold" } ] } From 02cc8d56a9510ed6479b449298107c926ae8dbc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:59:29 +0300 Subject: [PATCH 165/261] build(deps): bump Externals/sse2neon from `07fd1e9` to `f1bc16e` (#2036) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2neon b/Externals/sse2neon index 07fd1e90bd6..f1bc16e4b10 160000 --- a/Externals/sse2neon +++ b/Externals/sse2neon @@ -1 +1 @@ -Subproject commit 07fd1e90bd6b89e26337507acd73666f2cba883f +Subproject commit f1bc16e4b107f368a6098edd0d423803657837bd From 8d4fa609a046f7c39168554dfc054c4ca2748270 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:59:29 +0300 Subject: [PATCH 166/261] build(deps): bump Externals/sse2neon from `07fd1e9` to `f1bc16e` (#2036) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2neon b/Externals/sse2neon index 07fd1e90bd6..f1bc16e4b10 160000 --- a/Externals/sse2neon +++ b/Externals/sse2neon @@ -1 +1 @@ -Subproject commit 07fd1e90bd6b89e26337507acd73666f2cba883f +Subproject commit f1bc16e4b107f368a6098edd0d423803657837bd From d84e8021b3c3f123ea2280f52bd7f8b71a4828f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:59:41 +0300 Subject: [PATCH 167/261] build(deps): bump Externals/imgui from `396b33d` to `be2cfc2` (#2038) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 396b33d0d01..be2cfc26c9a 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 396b33d0d011290f11915b6a2a2bf7737fb42dd7 +Subproject commit be2cfc26c9ae77de99e6eed8d5e97a5a99b0cf3f From f75ec640dd68139c17295887c0f388565c8f2606 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:59:41 +0300 Subject: [PATCH 168/261] build(deps): bump Externals/imgui from `396b33d` to `be2cfc2` (#2038) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 396b33d0d01..be2cfc26c9a 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 396b33d0d011290f11915b6a2a2bf7737fb42dd7 +Subproject commit be2cfc26c9ae77de99e6eed8d5e97a5a99b0cf3f From dbde60b787be8bd11461d68ef54200e00f6915e1 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Sun, 1 Feb 2026 16:20:09 +0100 Subject: [PATCH 169/261] fix: Mark rsqrt function as noexcept (#2041) --- src/utils/xrMiscMath/vector.cpp | 2 +- src/xrCore/_vector3d.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/xrMiscMath/vector.cpp b/src/utils/xrMiscMath/vector.cpp index 686a52ea0aa..01f6bbfc292 100644 --- a/src/utils/xrMiscMath/vector.cpp +++ b/src/utils/xrMiscMath/vector.cpp @@ -140,7 +140,7 @@ float angle_inertion_var(float src, float tgt, float min_speed, float max_speed, return src; } -double rsqrt(double v) { return 1.0 / _sqrt(v); } +double rsqrt(double v) noexcept { return 1.0 / _sqrt(v); } ////////////////////////////////////////////////////////////////// diff --git a/src/xrCore/_vector3d.h b/src/xrCore/_vector3d.h index d6e7fc7aa46..dc99fba346d 100644 --- a/src/xrCore/_vector3d.h +++ b/src/xrCore/_vector3d.h @@ -356,7 +356,7 @@ bool _valid(const _vector3& v) ////////////////////////////////////////////////////////////////////////// #pragma warning(push) #pragma warning(disable : 4244) -double rsqrt(double v); +double rsqrt(double v) noexcept; bool exact_normalize(float* a); bool exact_normalize(Fvector3& a); #pragma warning(pop) From dd2684bbbdba71395c3f0e7b9f46fc20d3f57997 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Sun, 1 Feb 2026 16:20:09 +0100 Subject: [PATCH 170/261] fix: Mark rsqrt function as noexcept (#2041) --- src/utils/xrMiscMath/vector.cpp | 2 +- src/xrCore/_vector3d.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/xrMiscMath/vector.cpp b/src/utils/xrMiscMath/vector.cpp index 686a52ea0aa..01f6bbfc292 100644 --- a/src/utils/xrMiscMath/vector.cpp +++ b/src/utils/xrMiscMath/vector.cpp @@ -140,7 +140,7 @@ float angle_inertion_var(float src, float tgt, float min_speed, float max_speed, return src; } -double rsqrt(double v) { return 1.0 / _sqrt(v); } +double rsqrt(double v) noexcept { return 1.0 / _sqrt(v); } ////////////////////////////////////////////////////////////////// diff --git a/src/xrCore/_vector3d.h b/src/xrCore/_vector3d.h index d6e7fc7aa46..dc99fba346d 100644 --- a/src/xrCore/_vector3d.h +++ b/src/xrCore/_vector3d.h @@ -356,7 +356,7 @@ bool _valid(const _vector3& v) ////////////////////////////////////////////////////////////////////////// #pragma warning(push) #pragma warning(disable : 4244) -double rsqrt(double v); +double rsqrt(double v) noexcept; bool exact_normalize(float* a); bool exact_normalize(Fvector3& a); #pragma warning(pop) From 24eb3d0acf4b8c75ddf246bdf820e6f0753c3308 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:58:23 +0300 Subject: [PATCH 171/261] build(deps): bump Externals/imgui from `be2cfc2` to `913a3c6` (#2042) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index be2cfc26c9a..913a3c60561 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit be2cfc26c9ae77de99e6eed8d5e97a5a99b0cf3f +Subproject commit 913a3c60561bb07e8fd410ec7d4a8f6f485defd6 From 17dc97cb096d9d4814e36331e91b8a966308a821 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:58:23 +0300 Subject: [PATCH 172/261] build(deps): bump Externals/imgui from `be2cfc2` to `913a3c6` (#2042) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index be2cfc26c9a..913a3c60561 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit be2cfc26c9ae77de99e6eed8d5e97a5a99b0cf3f +Subproject commit 913a3c60561bb07e8fd410ec7d4a8f6f485defd6 From dac65d6d823d8f0bbccd9aee19c4626b0123a8ae Mon Sep 17 00:00:00 2001 From: AMS21 Date: Fri, 6 Feb 2026 15:42:37 +0100 Subject: [PATCH 173/261] Add Flatpak manifest and workflow (#1403) (#2040) --- .github/workflows/cibuild.yml | 27 +++++++++++++++++++++++++++ misc/flatpak/manifest.yml | 26 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 misc/flatpak/manifest.yml diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index de48121a37b..d70d39218db 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -241,3 +241,30 @@ jobs: shutdown_vm: false sync_files: false run: cmake --build build --config ${{ matrix.Configuration }} --parallel 4 + + flatpak: + name: Flatpak ${{ matrix.platform.arch }} + runs-on: ${{ matrix.platform.os }} + + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08 + options: --privileged + + strategy: + fail-fast: false + matrix: + platform: + - arch: x86_64 + os: ubuntu-latest + - arch: aarch64 + os: ubuntu-24.04-arm + + steps: + - uses: actions/checkout@main + + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: io.github.OpenXRay.xray-16-${{ matrix.platform.arch }}-${{ github.run_number }}.flatpak + manifest-path: misc/flatpak/manifest.yml + arch: ${{ matrix.platform.arch }} + verbose: true diff --git a/misc/flatpak/manifest.yml b/misc/flatpak/manifest.yml new file mode 100644 index 00000000000..7e2dd65d354 --- /dev/null +++ b/misc/flatpak/manifest.yml @@ -0,0 +1,26 @@ +id: io.github.OpenXRay.xray-16 +runtime: org.freedesktop.Platform +runtime-version: '25.08' +sdk: org.freedesktop.Sdk +command: xr_3da +finish-args: + - --share=network + - --share=ipc + - --socket=fallback-x11 + - --socket=wayland + - --socket=pulseaudio + - --device=dri + +modules: + - name: OpenXRay + buildsystem: cmake + sources: + - type: git + url: https://github.com/OpenXRay/xray-16.git + branch: dev + config-opts: + - -D CMAKE_BUILD_TYPE=ReleaseMasterGold + - -D CMAKE_UNITY_BUILD=ON + +#rename-icon: openxray_cop.png +rename-desktop-file: openxray_cop.desktop From b0291d4426c5618bd4d9faa509032c75ecd6d038 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Fri, 6 Feb 2026 15:42:37 +0100 Subject: [PATCH 174/261] Add Flatpak manifest and workflow (#1403) (#2040) --- .github/workflows/cibuild.yml | 27 +++++++++++++++++++++++++++ misc/flatpak/manifest.yml | 26 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 misc/flatpak/manifest.yml diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index de48121a37b..d70d39218db 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -241,3 +241,30 @@ jobs: shutdown_vm: false sync_files: false run: cmake --build build --config ${{ matrix.Configuration }} --parallel 4 + + flatpak: + name: Flatpak ${{ matrix.platform.arch }} + runs-on: ${{ matrix.platform.os }} + + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08 + options: --privileged + + strategy: + fail-fast: false + matrix: + platform: + - arch: x86_64 + os: ubuntu-latest + - arch: aarch64 + os: ubuntu-24.04-arm + + steps: + - uses: actions/checkout@main + + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: io.github.OpenXRay.xray-16-${{ matrix.platform.arch }}-${{ github.run_number }}.flatpak + manifest-path: misc/flatpak/manifest.yml + arch: ${{ matrix.platform.arch }} + verbose: true diff --git a/misc/flatpak/manifest.yml b/misc/flatpak/manifest.yml new file mode 100644 index 00000000000..7e2dd65d354 --- /dev/null +++ b/misc/flatpak/manifest.yml @@ -0,0 +1,26 @@ +id: io.github.OpenXRay.xray-16 +runtime: org.freedesktop.Platform +runtime-version: '25.08' +sdk: org.freedesktop.Sdk +command: xr_3da +finish-args: + - --share=network + - --share=ipc + - --socket=fallback-x11 + - --socket=wayland + - --socket=pulseaudio + - --device=dri + +modules: + - name: OpenXRay + buildsystem: cmake + sources: + - type: git + url: https://github.com/OpenXRay/xray-16.git + branch: dev + config-opts: + - -D CMAKE_BUILD_TYPE=ReleaseMasterGold + - -D CMAKE_UNITY_BUILD=ON + +#rename-icon: openxray_cop.png +rename-desktop-file: openxray_cop.desktop From a85b0da2a76eb8298dae373501ff8361913bedb6 Mon Sep 17 00:00:00 2001 From: 1therealcloud <1therealcloud@gmail.com> Date: Fri, 6 Feb 2026 22:04:33 +0200 Subject: [PATCH 175/261] Removed eax.lib (#2043) --- sdk/libraries/eax.lib | Bin 2432 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 sdk/libraries/eax.lib diff --git a/sdk/libraries/eax.lib b/sdk/libraries/eax.lib deleted file mode 100644 index 81a2cf747b23749fb3a1450ff6dc0aeef26bffd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2432 zcmcIm&2G~`5FR^!ZHiC?y&ynxQ4i%3TsvtJfhvcjf>1ZDlCtZ*lrLfX^cV}jIcE0&pue@qoos-S<3+ZMnl?rN6 zR@K5yvLv&$vQcES!!!Ua0*IFYi3@<_7+_(gj|=`db?lWHmc=T8E8wjXPF(lIT-f470Ybfrnau&F}>3><92q~ zzT%9HF8_^bSB92l_U!}HI`0koTvw108OPE!j!OvSr3i4502n6$MhgHk_GLsjtPwc@ zaY%vyBAdm+@p0`4L%;zbXh&pUO+kMa$UYwgFA^F9Xh^oI>mJy%qO~9^UbN~$4F3GE zUXBHfO+@fFfUqPW+|ybHndq7L6q%UALqU?Ik28A1l%4;dmF=1zhR3T-u4xk|AJKk) z(;)LY+v~RN_Nv0^n6~f*<-RgmvQY#1ipgzoIl}RmPmbssNLL6M+^6DzEybL+1`jhS zT)TK-y}$ScCJyku9w?o=bYIzD=6~?H*ESC!OH>Bjo^%DT6eo0<_NH^b9XK zk>W6!a9!McbSn|Ktr~!H&l65@tRfvTPRWrB62OzmUsH(wgg&g%=>U1QqCtdr_>G)V zbBTN<$1C_@S{Fno^083kot!3wMMl@$eEAf$-ujB9 zqgD&0upX!uKT*Rp%sw*=_~66L3k8b5Z(M+lzHG{I7Ri0*3!mJ4u)PbOk5Fr9_(jGt zpJPx;^q)Yvs0&nmpP>fI{Ltsxc`$e`NA+G_4cEAW+Ner|YNRKKD~xQTmtz0e_!zZO Loic*2ny2e8Cnd3p From 342161c326d937569e4ae63e9601be1c99b33f0f Mon Sep 17 00:00:00 2001 From: 1therealcloud <1therealcloud@gmail.com> Date: Fri, 6 Feb 2026 22:04:33 +0200 Subject: [PATCH 176/261] Removed eax.lib (#2043) --- sdk/libraries/eax.lib | Bin 2432 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 sdk/libraries/eax.lib diff --git a/sdk/libraries/eax.lib b/sdk/libraries/eax.lib deleted file mode 100644 index 81a2cf747b23749fb3a1450ff6dc0aeef26bffd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2432 zcmcIm&2G~`5FR^!ZHiC?y&ynxQ4i%3TsvtJfhvcjf>1ZDlCtZ*lrLfX^cV}jIcE0&pue@qoos-S<3+ZMnl?rN6 zR@K5yvLv&$vQcES!!!Ua0*IFYi3@<_7+_(gj|=`db?lWHmc=T8E8wjXPF(lIT-f470Ybfrnau&F}>3><92q~ zzT%9HF8_^bSB92l_U!}HI`0koTvw108OPE!j!OvSr3i4502n6$MhgHk_GLsjtPwc@ zaY%vyBAdm+@p0`4L%;zbXh&pUO+kMa$UYwgFA^F9Xh^oI>mJy%qO~9^UbN~$4F3GE zUXBHfO+@fFfUqPW+|ybHndq7L6q%UALqU?Ik28A1l%4;dmF=1zhR3T-u4xk|AJKk) z(;)LY+v~RN_Nv0^n6~f*<-RgmvQY#1ipgzoIl}RmPmbssNLL6M+^6DzEybL+1`jhS zT)TK-y}$ScCJyku9w?o=bYIzD=6~?H*ESC!OH>Bjo^%DT6eo0<_NH^b9XK zk>W6!a9!McbSn|Ktr~!H&l65@tRfvTPRWrB62OzmUsH(wgg&g%=>U1QqCtdr_>G)V zbBTN<$1C_@S{Fno^083kot!3wMMl@$eEAf$-ujB9 zqgD&0upX!uKT*Rp%sw*=_~66L3k8b5Z(M+lzHG{I7Ri0*3!mJ4u)PbOk5Fr9_(jGt zpJPx;^q)Yvs0&nmpP>fI{Ltsxc`$e`NA+G_4cEAW+Ner|YNRKKD~xQTmtz0e_!zZO Loic*2ny2e8Cnd3p From 7d357460ceb95628fc72e65c80728c375a854eb0 Mon Sep 17 00:00:00 2001 From: Lev Khoroshansky Date: Mon, 9 Feb 2026 00:26:57 +0300 Subject: [PATCH 177/261] ci: macOS artifacts (ZIP) + Intel runner + ccache (#2046) Issue #1403 checklist: - Output binaries for macOS builds: run CPack on macOS and upload the resulting openxray*.zip as a workflow artifact. What this change does: - Add a minimal macOS matrix: macos-latest (arm64) + macos-15-intel (x86_64). - Force -DCPACK_GENERATOR=ZIP on macOS for deterministic packaging. - Speed/stability: install ccache and cache ~/.ccache; disable Homebrew auto-updates via HOMEBREW_NO_AUTO_UPDATE. --- .github/workflows/cibuild.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index d70d39218db..a22d2344950 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -81,6 +81,7 @@ jobs: env: CFLAGS: "-w" CXXFLAGS: "-w" + HOMEBREW_NO_AUTO_UPDATE: 1 strategy: fail-fast: false @@ -99,7 +100,9 @@ jobs: - { name: Alpine, os: ubuntu-latest, arch: x86, cc: gcc, shell: 'alpine.sh {0}', flags: "-DXRAY_LINKER=mold", } - { name: Fedora, os: ubuntu-latest, arch: x86_64, cc: gcc, container: 'fedora:latest', } #- { name: Haiku, os: ubuntu-latest, arch: x86_64, cc: x86_64-unknown-haiku-gcc, cxx: x86_64-unknown-haiku-g++, container: 'haiku/cross-compiler:x86_64-r1beta4', } - - { name: macOS, os: macos-latest, arch: arm64, cc: clang, threads: 3 } + # macOS images have tighter concurrency limits; keep the matrix minimal. + - { name: macOS, os: macos-latest, arch: arm64, cc: clang, threads: 3, flags: "-DCPACK_GENERATOR=ZIP" } + - { name: macOS Intel, os: macos-15-intel, arch: x86_64, cc: clang, flags: "-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCPACK_GENERATOR=ZIP" } configuration: [Debug, Mixed, Release, ReleaseMasterGold] @@ -133,8 +136,7 @@ jobs: - name: Install macOS packages if: ${{ startsWith(matrix.platform.name, 'macOS') }} run: | - brew update - brew install sdl2 lzo libogg libvorbis theora + brew install ccache cmake sdl2 lzo libogg libvorbis theora openal-soft jpeg-turbo - name: Install Fedora packages if: ${{ matrix.platform.name == 'Fedora' }} @@ -150,6 +152,16 @@ jobs: with: submodules: recursive + - name: Restore ccache + if: ${{ startsWith(matrix.platform.name, 'macOS') }} + uses: actions/cache@main + with: + path: ~/.ccache + key: ccache-${{ runner.os }}-${{ matrix.platform.arch }}-${{ matrix.configuration }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'src/**/CMakeLists.txt', 'Externals/**/CMakeLists.txt') }} + restore-keys: | + ccache-${{ runner.os }}-${{ matrix.platform.arch }}-${{ matrix.configuration }}- + ccache-${{ runner.os }}-${{ matrix.platform.arch }}- + - name: Run CMake run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} -DCMAKE_UNITY_BUILD=ON ${{ matrix.platform.flags }} --log-level VERBOSE @@ -157,6 +169,10 @@ jobs: id: cmake-build run: cmake --build build --config ${{ matrix.configuration }} --parallel ${{ matrix.platform.threads || 4 }} + - name: Show ccache stats + if: ${{ startsWith(matrix.platform.name, 'macOS') }} + run: ccache -s || true + - name: Make package if: ${{ steps.cmake-build.outcome == 'success' }} id: make-package From d981666e562b6f080d623669b5c0a0834131c440 Mon Sep 17 00:00:00 2001 From: Lev Khoroshansky Date: Mon, 9 Feb 2026 00:26:57 +0300 Subject: [PATCH 178/261] ci: macOS artifacts (ZIP) + Intel runner + ccache (#2046) Issue #1403 checklist: - Output binaries for macOS builds: run CPack on macOS and upload the resulting openxray*.zip as a workflow artifact. What this change does: - Add a minimal macOS matrix: macos-latest (arm64) + macos-15-intel (x86_64). - Force -DCPACK_GENERATOR=ZIP on macOS for deterministic packaging. - Speed/stability: install ccache and cache ~/.ccache; disable Homebrew auto-updates via HOMEBREW_NO_AUTO_UPDATE. --- .github/workflows/cibuild.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index d70d39218db..a22d2344950 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -81,6 +81,7 @@ jobs: env: CFLAGS: "-w" CXXFLAGS: "-w" + HOMEBREW_NO_AUTO_UPDATE: 1 strategy: fail-fast: false @@ -99,7 +100,9 @@ jobs: - { name: Alpine, os: ubuntu-latest, arch: x86, cc: gcc, shell: 'alpine.sh {0}', flags: "-DXRAY_LINKER=mold", } - { name: Fedora, os: ubuntu-latest, arch: x86_64, cc: gcc, container: 'fedora:latest', } #- { name: Haiku, os: ubuntu-latest, arch: x86_64, cc: x86_64-unknown-haiku-gcc, cxx: x86_64-unknown-haiku-g++, container: 'haiku/cross-compiler:x86_64-r1beta4', } - - { name: macOS, os: macos-latest, arch: arm64, cc: clang, threads: 3 } + # macOS images have tighter concurrency limits; keep the matrix minimal. + - { name: macOS, os: macos-latest, arch: arm64, cc: clang, threads: 3, flags: "-DCPACK_GENERATOR=ZIP" } + - { name: macOS Intel, os: macos-15-intel, arch: x86_64, cc: clang, flags: "-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCPACK_GENERATOR=ZIP" } configuration: [Debug, Mixed, Release, ReleaseMasterGold] @@ -133,8 +136,7 @@ jobs: - name: Install macOS packages if: ${{ startsWith(matrix.platform.name, 'macOS') }} run: | - brew update - brew install sdl2 lzo libogg libvorbis theora + brew install ccache cmake sdl2 lzo libogg libvorbis theora openal-soft jpeg-turbo - name: Install Fedora packages if: ${{ matrix.platform.name == 'Fedora' }} @@ -150,6 +152,16 @@ jobs: with: submodules: recursive + - name: Restore ccache + if: ${{ startsWith(matrix.platform.name, 'macOS') }} + uses: actions/cache@main + with: + path: ~/.ccache + key: ccache-${{ runner.os }}-${{ matrix.platform.arch }}-${{ matrix.configuration }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'src/**/CMakeLists.txt', 'Externals/**/CMakeLists.txt') }} + restore-keys: | + ccache-${{ runner.os }}-${{ matrix.platform.arch }}-${{ matrix.configuration }}- + ccache-${{ runner.os }}-${{ matrix.platform.arch }}- + - name: Run CMake run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} -DCMAKE_UNITY_BUILD=ON ${{ matrix.platform.flags }} --log-level VERBOSE @@ -157,6 +169,10 @@ jobs: id: cmake-build run: cmake --build build --config ${{ matrix.configuration }} --parallel ${{ matrix.platform.threads || 4 }} + - name: Show ccache stats + if: ${{ startsWith(matrix.platform.name, 'macOS') }} + run: ccache -s || true + - name: Make package if: ${{ steps.cmake-build.outcome == 'success' }} id: make-package From 4da154c22002a1b0fe7a984c17eeb1375d5ec983 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Tue, 10 Feb 2026 11:29:56 +0100 Subject: [PATCH 179/261] Add AppImage workflow to GitHub Actions (#1403) (#2047) --- .github/workflows/cibuild.yml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index a22d2344950..0c1c392fe40 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -100,6 +100,8 @@ jobs: - { name: Alpine, os: ubuntu-latest, arch: x86, cc: gcc, shell: 'alpine.sh {0}', flags: "-DXRAY_LINKER=mold", } - { name: Fedora, os: ubuntu-latest, arch: x86_64, cc: gcc, container: 'fedora:latest', } #- { name: Haiku, os: ubuntu-latest, arch: x86_64, cc: x86_64-unknown-haiku-gcc, cxx: x86_64-unknown-haiku-g++, container: 'haiku/cross-compiler:x86_64-r1beta4', } + - { name: AppImage, os: ubuntu-latest, arch: amd64, cc: gcc, flags: "-DBUILD_SHARED_LIBS=OFF", linuxdeploy: linuxdeploy-x86_64.AppImage } + - { name: AppImage, os: ubuntu-24.04-arm, arch: arm64, cc: gcc, flags: "-DBUILD_SHARED_LIBS=OFF", linuxdeploy: linuxdeploy-aarch64.AppImage } # macOS images have tighter concurrency limits; keep the matrix minimal. - { name: macOS, os: macos-latest, arch: arm64, cc: clang, threads: 3, flags: "-DCPACK_GENERATOR=ZIP" } - { name: macOS Intel, os: macos-15-intel, arch: x86_64, cc: clang, flags: "-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCPACK_GENERATOR=ZIP" } @@ -116,7 +118,7 @@ jobs: packages: build-base cmake git mold sdl2-dev lzo-dev libjpeg-turbo-dev openal-soft-dev libogg-dev libtheora-dev libvorbis-dev - name: Install Ubuntu packages - if: ${{ matrix.platform.name == 'Ubuntu' }} + if: ${{ matrix.platform.name == 'Ubuntu' || matrix.platform.name == 'AppImage' }} run: | sudo dpkg --add-architecture ${{ matrix.platform.arch }} sudo apt-get update -qq @@ -142,6 +144,12 @@ jobs: if: ${{ matrix.platform.name == 'Fedora' }} run: dnf install -y git gcc gcc-c++ rpmdevtools cmake SDL2-devel lzo-devel libjpeg-turbo-devel openal-soft-devel libogg-devel libtheora-devel libvorbis-devel + - name: Install AppImage linuxdeploy + if: ${{ matrix.platform.name == 'AppImage' }} + uses: pcolby/install-linuxdeploy@v1 + with: + plugins: appimage + - name: Set environment variables if: ${{ matrix.platform.cc != '' && matrix.platform.cxx != '' }} run: | @@ -173,8 +181,10 @@ jobs: if: ${{ startsWith(matrix.platform.name, 'macOS') }} run: ccache -s || true + # TODO: Merge this step with 'Make AppImage' once we switch to CMake 4.2 which directly supports AppImage generation with CPack + # https://cmake.org/cmake/help/latest/cpack_gen/appimage.html - name: Make package - if: ${{ steps.cmake-build.outcome == 'success' }} + if: ${{ steps.cmake-build.outcome == 'success' && matrix.platform.name != 'AppImage' }} id: make-package #continue-on-error: true working-directory: build @@ -182,8 +192,18 @@ jobs: cpack -B artifacts -C ${{ matrix.configuration }} -DCPACK_THREADS=${{ matrix.platform.threads || 4 }} file artifacts/openxray*.* + - name: Make AppImage + if: ${{ steps.cmake-build.outcome == 'success' && matrix.platform.name == 'AppImage' }} + id: make-appimage + run: | + DESTDIR=AppDir cmake --install build --prefix /usr + ${{ matrix.platform.linuxdeploy }} --appdir AppDir --output appimage --executable AppDir/usr/bin/xr_3da --desktop-file AppDir/usr/share/applications/openxray_cop.desktop --desktop-file AppDir/usr/share/applications/openxray_cs.desktop --desktop-file AppDir/usr/share/applications/openxray_soc.desktop --icon-file AppDir/usr/share/icons/hicolor/64x64/apps/openxray_cop.png --icon-file AppDir/usr/share/icons/hicolor/64x64/apps/openxray_cs.png --icon-file AppDir/usr/share/icons/hicolor/64x64/apps/openxray_soc.png + mkdir -p build/artifacts + mv *.AppImage build/artifacts/openxray-${{ matrix.configuration }}-${{ matrix.platform.arch }}.AppImage + file build/artifacts/openxray*.* + - name: Upload OpenXRay artifact - if: ${{ steps.make-package.outcome == 'success' }} + if: ${{ steps.make-package.outcome == 'success' || steps.make-appimage.outcome == 'success' }} uses: actions/upload-artifact@main with: name: ${{ matrix.platform.name }} ${{ matrix.configuration }} ${{ matrix.platform.arch }} (${{ matrix.platform.cc }} github-${{ github.run_number }}) From 26694c022599782c5def7e8d29a9f4c2183af214 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Tue, 10 Feb 2026 11:29:56 +0100 Subject: [PATCH 180/261] Add AppImage workflow to GitHub Actions (#1403) (#2047) --- .github/workflows/cibuild.yml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index a22d2344950..0c1c392fe40 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -100,6 +100,8 @@ jobs: - { name: Alpine, os: ubuntu-latest, arch: x86, cc: gcc, shell: 'alpine.sh {0}', flags: "-DXRAY_LINKER=mold", } - { name: Fedora, os: ubuntu-latest, arch: x86_64, cc: gcc, container: 'fedora:latest', } #- { name: Haiku, os: ubuntu-latest, arch: x86_64, cc: x86_64-unknown-haiku-gcc, cxx: x86_64-unknown-haiku-g++, container: 'haiku/cross-compiler:x86_64-r1beta4', } + - { name: AppImage, os: ubuntu-latest, arch: amd64, cc: gcc, flags: "-DBUILD_SHARED_LIBS=OFF", linuxdeploy: linuxdeploy-x86_64.AppImage } + - { name: AppImage, os: ubuntu-24.04-arm, arch: arm64, cc: gcc, flags: "-DBUILD_SHARED_LIBS=OFF", linuxdeploy: linuxdeploy-aarch64.AppImage } # macOS images have tighter concurrency limits; keep the matrix minimal. - { name: macOS, os: macos-latest, arch: arm64, cc: clang, threads: 3, flags: "-DCPACK_GENERATOR=ZIP" } - { name: macOS Intel, os: macos-15-intel, arch: x86_64, cc: clang, flags: "-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCPACK_GENERATOR=ZIP" } @@ -116,7 +118,7 @@ jobs: packages: build-base cmake git mold sdl2-dev lzo-dev libjpeg-turbo-dev openal-soft-dev libogg-dev libtheora-dev libvorbis-dev - name: Install Ubuntu packages - if: ${{ matrix.platform.name == 'Ubuntu' }} + if: ${{ matrix.platform.name == 'Ubuntu' || matrix.platform.name == 'AppImage' }} run: | sudo dpkg --add-architecture ${{ matrix.platform.arch }} sudo apt-get update -qq @@ -142,6 +144,12 @@ jobs: if: ${{ matrix.platform.name == 'Fedora' }} run: dnf install -y git gcc gcc-c++ rpmdevtools cmake SDL2-devel lzo-devel libjpeg-turbo-devel openal-soft-devel libogg-devel libtheora-devel libvorbis-devel + - name: Install AppImage linuxdeploy + if: ${{ matrix.platform.name == 'AppImage' }} + uses: pcolby/install-linuxdeploy@v1 + with: + plugins: appimage + - name: Set environment variables if: ${{ matrix.platform.cc != '' && matrix.platform.cxx != '' }} run: | @@ -173,8 +181,10 @@ jobs: if: ${{ startsWith(matrix.platform.name, 'macOS') }} run: ccache -s || true + # TODO: Merge this step with 'Make AppImage' once we switch to CMake 4.2 which directly supports AppImage generation with CPack + # https://cmake.org/cmake/help/latest/cpack_gen/appimage.html - name: Make package - if: ${{ steps.cmake-build.outcome == 'success' }} + if: ${{ steps.cmake-build.outcome == 'success' && matrix.platform.name != 'AppImage' }} id: make-package #continue-on-error: true working-directory: build @@ -182,8 +192,18 @@ jobs: cpack -B artifacts -C ${{ matrix.configuration }} -DCPACK_THREADS=${{ matrix.platform.threads || 4 }} file artifacts/openxray*.* + - name: Make AppImage + if: ${{ steps.cmake-build.outcome == 'success' && matrix.platform.name == 'AppImage' }} + id: make-appimage + run: | + DESTDIR=AppDir cmake --install build --prefix /usr + ${{ matrix.platform.linuxdeploy }} --appdir AppDir --output appimage --executable AppDir/usr/bin/xr_3da --desktop-file AppDir/usr/share/applications/openxray_cop.desktop --desktop-file AppDir/usr/share/applications/openxray_cs.desktop --desktop-file AppDir/usr/share/applications/openxray_soc.desktop --icon-file AppDir/usr/share/icons/hicolor/64x64/apps/openxray_cop.png --icon-file AppDir/usr/share/icons/hicolor/64x64/apps/openxray_cs.png --icon-file AppDir/usr/share/icons/hicolor/64x64/apps/openxray_soc.png + mkdir -p build/artifacts + mv *.AppImage build/artifacts/openxray-${{ matrix.configuration }}-${{ matrix.platform.arch }}.AppImage + file build/artifacts/openxray*.* + - name: Upload OpenXRay artifact - if: ${{ steps.make-package.outcome == 'success' }} + if: ${{ steps.make-package.outcome == 'success' || steps.make-appimage.outcome == 'success' }} uses: actions/upload-artifact@main with: name: ${{ matrix.platform.name }} ${{ matrix.configuration }} ${{ matrix.platform.arch }} (${{ matrix.platform.cc }} github-${{ github.run_number }}) From 24991d373a031ce5f12007f18deecda7a4b215fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:55:46 +0300 Subject: [PATCH 181/261] build(deps): bump Externals/gli from `779b99a` to `7efdcf8` (#2048) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/gli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/gli b/Externals/gli index 779b99ac665..7efdcf87dd5 160000 --- a/Externals/gli +++ b/Externals/gli @@ -1 +1 @@ -Subproject commit 779b99ac6656e4d30c3b24e96e0136a59649a869 +Subproject commit 7efdcf87dd51cfe828b0da1068f2072542b5631a From 132323d9e205ec161ae70109ec60c424a2a16b97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:55:46 +0300 Subject: [PATCH 182/261] build(deps): bump Externals/gli from `779b99a` to `7efdcf8` (#2048) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/gli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/gli b/Externals/gli index 779b99ac665..7efdcf87dd5 160000 --- a/Externals/gli +++ b/Externals/gli @@ -1 +1 @@ -Subproject commit 779b99ac6656e4d30c3b24e96e0136a59649a869 +Subproject commit 7efdcf87dd51cfe828b0da1068f2072542b5631a From dfb8c873864dcc479c0bfa6371c34c2629c45901 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Wed, 11 Feb 2026 17:09:05 +0100 Subject: [PATCH 183/261] fix: `m_opt_backup_value` not being initialized in `CUICheckButton` (#2049) Co-authored-by: Xottab-DUTY --- src/xrUICore/Buttons/UICheckButton.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrUICore/Buttons/UICheckButton.h b/src/xrUICore/Buttons/UICheckButton.h index b1703b63c55..38b00fdd13b 100644 --- a/src/xrUICore/Buttons/UICheckButton.h +++ b/src/xrUICore/Buttons/UICheckButton.h @@ -34,7 +34,7 @@ class XRUICORE_API CUICheckButton final : public CUI3tButton, public CUIOptionsI pcstr GetDebugType() override { return "CUICheckButton"; } private: - bool m_opt_backup_value; + bool m_opt_backup_value{}; void InitTexture2(LPCSTR texture_name); CUIWindow* m_pDependControl; }; From b6dd88ad6a772687b2a3d1e7d8b7973bf3d4e3f3 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Wed, 11 Feb 2026 17:09:05 +0100 Subject: [PATCH 184/261] fix: `m_opt_backup_value` not being initialized in `CUICheckButton` (#2049) Co-authored-by: Xottab-DUTY --- src/xrUICore/Buttons/UICheckButton.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrUICore/Buttons/UICheckButton.h b/src/xrUICore/Buttons/UICheckButton.h index b1703b63c55..38b00fdd13b 100644 --- a/src/xrUICore/Buttons/UICheckButton.h +++ b/src/xrUICore/Buttons/UICheckButton.h @@ -34,7 +34,7 @@ class XRUICORE_API CUICheckButton final : public CUI3tButton, public CUIOptionsI pcstr GetDebugType() override { return "CUICheckButton"; } private: - bool m_opt_backup_value; + bool m_opt_backup_value{}; void InitTexture2(LPCSTR texture_name); CUIWindow* m_pDependControl; }; From 4515e0592c9a0a98e2e300463d3e0e7ed631bc54 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 03:06:16 +0300 Subject: [PATCH 185/261] build(deps): bump Externals/imgui from `913a3c6` to `3fb22b8` (#2052) Bumps [Externals/imgui](https://github.com/ocornut/imgui) from `913a3c6` to `3fb22b8`. - [Release notes](https://github.com/ocornut/imgui/releases) - [Commits](https://github.com/ocornut/imgui/compare/913a3c60561bb07e8fd410ec7d4a8f6f485defd6...3fb22b836ba968c62d280d9de079ec5ed7fcd1b5) --- updated-dependencies: - dependency-name: Externals/imgui dependency-version: 3fb22b836ba968c62d280d9de079ec5ed7fcd1b5 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 913a3c60561..3fb22b836ba 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 913a3c60561bb07e8fd410ec7d4a8f6f485defd6 +Subproject commit 3fb22b836ba968c62d280d9de079ec5ed7fcd1b5 From 4575db9a9b88d79e93858bcc85e505bccfa0389b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 03:06:16 +0300 Subject: [PATCH 186/261] build(deps): bump Externals/imgui from `913a3c6` to `3fb22b8` (#2052) Bumps [Externals/imgui](https://github.com/ocornut/imgui) from `913a3c6` to `3fb22b8`. - [Release notes](https://github.com/ocornut/imgui/releases) - [Commits](https://github.com/ocornut/imgui/compare/913a3c60561bb07e8fd410ec7d4a8f6f485defd6...3fb22b836ba968c62d280d9de079ec5ed7fcd1b5) --- updated-dependencies: - dependency-name: Externals/imgui dependency-version: 3fb22b836ba968c62d280d9de079ec5ed7fcd1b5 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/imgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/imgui b/Externals/imgui index 913a3c60561..3fb22b836ba 160000 --- a/Externals/imgui +++ b/Externals/imgui @@ -1 +1 @@ -Subproject commit 913a3c60561bb07e8fd410ec7d4a8f6f485defd6 +Subproject commit 3fb22b836ba968c62d280d9de079ec5ed7fcd1b5 From cf132b906fcd1e4311aeb21c26028086e1277bf1 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 17 Feb 2026 02:09:03 +0300 Subject: [PATCH 187/261] Introduce CascadindCollapsingHeader for ImGui to be used in the weather editor It allows to collapse multiple headers when collapsing just one by using the same ID for the underlying state storage without triggering ID conflicts. --- src/xrEngine/Environment_editor.cpp | 25 ++++++++++++------------- src/xrEngine/editor_helper.cpp | 28 ++++++++++++++++++++++++++++ src/xrEngine/editor_helper.h | 18 ++++++++++++++++++ 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/xrEngine/Environment_editor.cpp b/src/xrEngine/Environment_editor.cpp index 486464a66b3..d7cf6659b89 100644 --- a/src/xrEngine/Environment_editor.cpp +++ b/src/xrEngine/Environment_editor.cpp @@ -89,8 +89,9 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) #ifndef MASTER_GOLD using namespace xray::imgui; - ImGui::PushID(m_identifier.c_str()); - if (ImGui::CollapsingHeader("sun##category", ImGuiTreeNodeFlags_DefaultOpen)) + ScopeID scope(this); + + if (CascadingCollapsingHeader("sun##category", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::BeginCombo("sun##lensflareid", lens_flare ? lens_flare->section.c_str() : "")) { @@ -151,7 +152,7 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) ImGui::DragFloat("shafts intensity", &m_fSunShaftsIntensity, 0.001f, 0.0f, 1.0f); } - if (ImGui::CollapsingHeader("hemisphere##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("hemisphere##category", ImGuiTreeNodeFlags_DefaultOpen)) { if (InputText("sky texture", sky_texture_name)) { @@ -171,7 +172,7 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) if (ImGui::DragFloat("sky rotation", &rotation, 0.5f, -360.0f, 360.f)) sky_rotation = deg2rad(rotation); } - if (ImGui::CollapsingHeader("clouds##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("clouds##category", ImGuiTreeNodeFlags_DefaultOpen)) { if (InputText("clouds texture", clouds_texture_name)) on_device_create(); @@ -182,7 +183,7 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) if (ImGui::DragFloat("clouds rotation", &rotation, 0.5f, -360.0f, 360.f)) clouds_rotation = deg2rad(rotation); } - if (ImGui::CollapsingHeader("ambient##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("ambient##category", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::BeginCombo("ambient##env_ambient", env_ambient ? env_ambient->name().c_str() : "")) { @@ -209,7 +210,7 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) "CS/COP: ambient_color\n" " SOC: ambient"); } - if (ImGui::CollapsingHeader("fog##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("fog##category", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::DragFloat("far plane", &far_plane); ImGui::ColorEdit3("fog color", reinterpret_cast(&fog_color)); @@ -218,12 +219,12 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) ImGui::DragFloat("fog density", &fog_density, 0.001f, 0.0f, 1.0f); ImGui::DragFloat("water intensity", &m_fWaterIntensity, 0.001f, 0.0f, 1.0f); } - if (ImGui::CollapsingHeader("rain##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("rain##category", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::ColorEdit3("rain color", reinterpret_cast(&rain_color)); ImGui::DragFloat("rain density", &rain_density, 0.001f, 0.0f, 1.0f); } - if (ImGui::CollapsingHeader("thunderbolts##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("thunderbolts##category", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::BeginCombo("thunderbolts", thunderbolt ? thunderbolt->section.c_str() : "")) { @@ -247,7 +248,7 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) ImGui::DragFloat("duration", &bolt_duration); ImGui::DragFloat("period", &bolt_period); } - if (ImGui::CollapsingHeader("wind##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("wind##category", ImGuiTreeNodeFlags_DefaultOpen)) { float direction = rad2deg(wind_direction); if (ImGui::DragFloat("wind direction", &direction, 0.5f, -360.0f, 360.f)) @@ -255,7 +256,6 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) ImGui::DragFloat("wind velocity", &wind_velocity, 1.0f, 0.0f, 1000.0f); } - ImGui::PopID(); #endif } @@ -266,8 +266,8 @@ void CEnvDescriptorMixer::ed_show_params(const CEnvironment& env) CEnvDescriptor::ed_show_params(env); - ImGui::PushID(m_identifier.c_str()); - if (ImGui::CollapsingHeader("mixer##category", ImGuiTreeNodeFlags_DefaultOpen)) + ScopeID scope(this); + if (CascadingCollapsingHeader("mixer##category", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::ColorEdit4("environment color", reinterpret_cast(&env_color)); ItemHelp("CS/COP: takes hemisphere color as the base value.\n" @@ -278,7 +278,6 @@ void CEnvDescriptorMixer::ed_show_params(const CEnvironment& env) ImGui::DragFloat("fog near", &fog_near); ImGui::DragFloat("fog far", &fog_far); } - ImGui::PopID(); #endif } diff --git a/src/xrEngine/editor_helper.cpp b/src/xrEngine/editor_helper.cpp index 837eba70c52..43bc9cdf82f 100644 --- a/src/xrEngine/editor_helper.cpp +++ b/src/xrEngine/editor_helper.cpp @@ -1,3 +1,31 @@ #include "stdafx.h" #include "editor_helper.h" + +#include "imgui_internal.h" + +namespace xray::imgui +{ +bool CascadingCollapsingHeader(cpcstr label, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiID seed = 0; + if (window->IDStack.size() > 1) + seed = *(window->IDStack.end() - 2); + else + seed = window->GetID("CascadingCollapsingHeader"); + + ImGuiID id = ImHashStr(label, 0, seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *window->Ctx; + if (g.DebugHookIdInfoId == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_String, label, nullptr); +#endif + ImGui::SetNextItemStorageID(id); + + return ImGui::CollapsingHeader(label, flags); +} +} // namespace xray::imgui diff --git a/src/xrEngine/editor_helper.h b/src/xrEngine/editor_helper.h index 0165d0a5555..b8232df6ce6 100644 --- a/src/xrEngine/editor_helper.h +++ b/src/xrEngine/editor_helper.h @@ -2,6 +2,24 @@ namespace xray::imgui { +struct ScopeID final : Noncopyable +{ + template + explicit ScopeID(Args... args) + { + ImGui::PushID(std::forward(args)...); + } + + ~ScopeID() + { + ImGui::PopID(); + } +}; + +// Uses previous ItemID in the ID stack for the storage ID to allow +// same ID for multiple items without causing ID conflicts +bool CascadingCollapsingHeader(cpcstr label, ImGuiTreeNodeFlags flags = 0); + inline void ItemHelp(const char* desc, bool use_separate_marker = true, bool on_same_line = true) { if (use_separate_marker) From 9ef7ab2769af798cca7ac55ca86256fbd6e08be1 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 17 Feb 2026 02:09:03 +0300 Subject: [PATCH 188/261] Introduce CascadindCollapsingHeader for ImGui to be used in the weather editor It allows to collapse multiple headers when collapsing just one by using the same ID for the underlying state storage without triggering ID conflicts. --- src/xrEngine/Environment_editor.cpp | 25 ++++++++++++------------- src/xrEngine/editor_helper.cpp | 28 ++++++++++++++++++++++++++++ src/xrEngine/editor_helper.h | 18 ++++++++++++++++++ 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/xrEngine/Environment_editor.cpp b/src/xrEngine/Environment_editor.cpp index 486464a66b3..d7cf6659b89 100644 --- a/src/xrEngine/Environment_editor.cpp +++ b/src/xrEngine/Environment_editor.cpp @@ -89,8 +89,9 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) #ifndef MASTER_GOLD using namespace xray::imgui; - ImGui::PushID(m_identifier.c_str()); - if (ImGui::CollapsingHeader("sun##category", ImGuiTreeNodeFlags_DefaultOpen)) + ScopeID scope(this); + + if (CascadingCollapsingHeader("sun##category", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::BeginCombo("sun##lensflareid", lens_flare ? lens_flare->section.c_str() : "")) { @@ -151,7 +152,7 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) ImGui::DragFloat("shafts intensity", &m_fSunShaftsIntensity, 0.001f, 0.0f, 1.0f); } - if (ImGui::CollapsingHeader("hemisphere##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("hemisphere##category", ImGuiTreeNodeFlags_DefaultOpen)) { if (InputText("sky texture", sky_texture_name)) { @@ -171,7 +172,7 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) if (ImGui::DragFloat("sky rotation", &rotation, 0.5f, -360.0f, 360.f)) sky_rotation = deg2rad(rotation); } - if (ImGui::CollapsingHeader("clouds##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("clouds##category", ImGuiTreeNodeFlags_DefaultOpen)) { if (InputText("clouds texture", clouds_texture_name)) on_device_create(); @@ -182,7 +183,7 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) if (ImGui::DragFloat("clouds rotation", &rotation, 0.5f, -360.0f, 360.f)) clouds_rotation = deg2rad(rotation); } - if (ImGui::CollapsingHeader("ambient##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("ambient##category", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::BeginCombo("ambient##env_ambient", env_ambient ? env_ambient->name().c_str() : "")) { @@ -209,7 +210,7 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) "CS/COP: ambient_color\n" " SOC: ambient"); } - if (ImGui::CollapsingHeader("fog##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("fog##category", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::DragFloat("far plane", &far_plane); ImGui::ColorEdit3("fog color", reinterpret_cast(&fog_color)); @@ -218,12 +219,12 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) ImGui::DragFloat("fog density", &fog_density, 0.001f, 0.0f, 1.0f); ImGui::DragFloat("water intensity", &m_fWaterIntensity, 0.001f, 0.0f, 1.0f); } - if (ImGui::CollapsingHeader("rain##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("rain##category", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::ColorEdit3("rain color", reinterpret_cast(&rain_color)); ImGui::DragFloat("rain density", &rain_density, 0.001f, 0.0f, 1.0f); } - if (ImGui::CollapsingHeader("thunderbolts##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("thunderbolts##category", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::BeginCombo("thunderbolts", thunderbolt ? thunderbolt->section.c_str() : "")) { @@ -247,7 +248,7 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) ImGui::DragFloat("duration", &bolt_duration); ImGui::DragFloat("period", &bolt_period); } - if (ImGui::CollapsingHeader("wind##category", ImGuiTreeNodeFlags_DefaultOpen)) + if (CascadingCollapsingHeader("wind##category", ImGuiTreeNodeFlags_DefaultOpen)) { float direction = rad2deg(wind_direction); if (ImGui::DragFloat("wind direction", &direction, 0.5f, -360.0f, 360.f)) @@ -255,7 +256,6 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) ImGui::DragFloat("wind velocity", &wind_velocity, 1.0f, 0.0f, 1000.0f); } - ImGui::PopID(); #endif } @@ -266,8 +266,8 @@ void CEnvDescriptorMixer::ed_show_params(const CEnvironment& env) CEnvDescriptor::ed_show_params(env); - ImGui::PushID(m_identifier.c_str()); - if (ImGui::CollapsingHeader("mixer##category", ImGuiTreeNodeFlags_DefaultOpen)) + ScopeID scope(this); + if (CascadingCollapsingHeader("mixer##category", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::ColorEdit4("environment color", reinterpret_cast(&env_color)); ItemHelp("CS/COP: takes hemisphere color as the base value.\n" @@ -278,7 +278,6 @@ void CEnvDescriptorMixer::ed_show_params(const CEnvironment& env) ImGui::DragFloat("fog near", &fog_near); ImGui::DragFloat("fog far", &fog_far); } - ImGui::PopID(); #endif } diff --git a/src/xrEngine/editor_helper.cpp b/src/xrEngine/editor_helper.cpp index 837eba70c52..43bc9cdf82f 100644 --- a/src/xrEngine/editor_helper.cpp +++ b/src/xrEngine/editor_helper.cpp @@ -1,3 +1,31 @@ #include "stdafx.h" #include "editor_helper.h" + +#include "imgui_internal.h" + +namespace xray::imgui +{ +bool CascadingCollapsingHeader(cpcstr label, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiID seed = 0; + if (window->IDStack.size() > 1) + seed = *(window->IDStack.end() - 2); + else + seed = window->GetID("CascadingCollapsingHeader"); + + ImGuiID id = ImHashStr(label, 0, seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *window->Ctx; + if (g.DebugHookIdInfoId == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_String, label, nullptr); +#endif + ImGui::SetNextItemStorageID(id); + + return ImGui::CollapsingHeader(label, flags); +} +} // namespace xray::imgui diff --git a/src/xrEngine/editor_helper.h b/src/xrEngine/editor_helper.h index 0165d0a5555..b8232df6ce6 100644 --- a/src/xrEngine/editor_helper.h +++ b/src/xrEngine/editor_helper.h @@ -2,6 +2,24 @@ namespace xray::imgui { +struct ScopeID final : Noncopyable +{ + template + explicit ScopeID(Args... args) + { + ImGui::PushID(std::forward(args)...); + } + + ~ScopeID() + { + ImGui::PopID(); + } +}; + +// Uses previous ItemID in the ID stack for the storage ID to allow +// same ID for multiple items without causing ID conflicts +bool CascadingCollapsingHeader(cpcstr label, ImGuiTreeNodeFlags flags = 0); + inline void ItemHelp(const char* desc, bool use_separate_marker = true, bool on_same_line = true) { if (use_separate_marker) From a4e8458c76008b44f006d0b7694d2b513e16e843 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 17 Feb 2026 02:56:02 +0300 Subject: [PATCH 189/261] Introduce Selector imgui control to be reused in few places --- src/xrEngine/Environment_editor.cpp | 39 +++---------------- src/xrEngine/editor_helper.h | 24 ++++++++++++ .../object_factory_spawner.cpp | 11 ++---- src/xrUICore/ProgressBar/UIProgressBar.cpp | 9 ++--- 4 files changed, 36 insertions(+), 47 deletions(-) diff --git a/src/xrEngine/Environment_editor.cpp b/src/xrEngine/Environment_editor.cpp index d7cf6659b89..ef604157c26 100644 --- a/src/xrEngine/Environment_editor.cpp +++ b/src/xrEngine/Environment_editor.cpp @@ -17,19 +17,11 @@ bool window_ambients = false; bool window_thunderbolts = false; bool window_level_weathers = false; -struct combo_raii -{ - ~combo_raii() - { - ImGui::EndCombo(); - } -}; - bool TimeFrameCombo(pcstr label, CEnvDescriptor*& descriptor, const CEnvironment::EnvVec* currentWeather) { if (ImGui::BeginCombo(label, descriptor ? descriptor->m_identifier.c_str() : "##")) { - combo_raii raii; + xray::imgui::combo_raii raii; if (ImGui::Selectable("##", !descriptor)) { @@ -53,35 +45,16 @@ bool TimeFrameCombo(pcstr label, CEnvDescriptor*& descriptor, const CEnvironment bool ConfigStyleSelector(pcstr label, bool& soc_style) { - using namespace xray::imgui; - - bool result = false; - enum ConfigStyle - { - ConfigStyle_SOC, - ConfigStyle_CSCOP, - ConfigStyle_COUNT - }; - constexpr pcstr styles[ConfigStyle_COUNT] = + constexpr pcstr styles[] = { + "CS/COP", "SOC", - "CS/COP" }; - int selected = soc_style ? ConfigStyle_SOC : ConfigStyle_CSCOP; - if (ImGui::SliderInt(label, &selected, 0, ConfigStyle_COUNT - 1, styles[selected])) - { - switch (static_cast(selected)) - { - case ConfigStyle_SOC: soc_style = true; break; - case ConfigStyle_CSCOP: soc_style = false; break; - } - result = true; - } - ItemHelp("This affects how environment color will be calculated in the mixer. " + return xray::imgui::Selector(label, soc_style, styles, std::size(styles), + "This affects how environment color will be calculated in the mixer. " "(look at the environment color in the mixer category)"); - return result; -} } +} // namespace #endif void CEnvDescriptor::ed_show_params(const CEnvironment& env) diff --git a/src/xrEngine/editor_helper.h b/src/xrEngine/editor_helper.h index b8232df6ce6..d16b2ad1980 100644 --- a/src/xrEngine/editor_helper.h +++ b/src/xrEngine/editor_helper.h @@ -16,6 +16,14 @@ struct ScopeID final : Noncopyable } }; +struct combo_raii +{ + ~combo_raii() + { + ImGui::EndCombo(); + } +}; + // Uses previous ItemID in the ID stack for the storage ID to allow // same ID for multiple items without causing ID conflicts bool CascadingCollapsingHeader(cpcstr label, ImGuiTreeNodeFlags flags = 0); @@ -47,6 +55,22 @@ inline bool MenuItemWithShortcut(pcstr label, EGameActions shortcut, const char* return result; } +template +inline bool Selector(cpcstr label, T& current_item, cpcstr items[], const int items_count, cpcstr desc = nullptr) +{ + VERIFY2(items, "There's no point in using Selector without text items."); + int selected = static_cast(current_item); + clamp(selected, 0, items_count - 1); + const bool result = ImGui::SliderInt(label, &selected, 0, items_count - 1, items ? items[selected] : "%d", ImGuiSliderFlags_NoInput); + if (result) + current_item = static_cast(selected); + if (items_count == 2 && ImGui::IsItemDeactivated() && !ImGui::IsItemDeactivatedAfterEdit()) + current_item = static_cast(!selected); + if (desc) + ItemHelp(desc); + return result; +} + inline bool InputText(pcstr label, shared_str& texture_name) { string_path temp; diff --git a/src/xrServerEntities/object_factory_spawner.cpp b/src/xrServerEntities/object_factory_spawner.cpp index 834c2f08c23..42206dbb33e 100644 --- a/src/xrServerEntities/object_factory_spawner.cpp +++ b/src/xrServerEntities/object_factory_spawner.cpp @@ -240,17 +240,12 @@ void CObjectFactory::on_tool_frame() constexpr pcstr styles[] = { "Game", - "LTX" + "LTX", }; ImGui::SetNextItemWidth(ImGui::CalcTextSize(" Game ").x); // shrink to minimal - ImGui::SliderInt("", &display_mode, 0, std::size(styles) - 1, styles[display_mode], ImGuiSliderFlags_NoInput); - if (ImGui::IsItemDeactivated() && !ImGui::IsItemDeactivatedAfterEdit()) - { - display_mode = !display_mode; - } - - imgui::ItemHelp("Left-click on the item in this list to spawn it.\n" + imgui::Selector("", display_mode, styles, std::size(styles), + "Left-click on the item in this list to spawn it.\n" "You can also select between displaying game names or ltx sections by using a switch."); ImGui::EndMenuBar(); } diff --git a/src/xrUICore/ProgressBar/UIProgressBar.cpp b/src/xrUICore/ProgressBar/UIProgressBar.cpp index 3e55849abed..ed64907b286 100644 --- a/src/xrUICore/ProgressBar/UIProgressBar.cpp +++ b/src/xrUICore/ProgressBar/UIProgressBar.cpp @@ -1,6 +1,8 @@ #include "pch.hpp" #include "UIProgressBar.h" +#include "xrEngine/editor_helper.h" + CUIProgressBar::CUIProgressBar() : CUIWindow("CUIProgressBar"), m_UIProgressItem("Progress"), m_UIBackgroundItem("Background") { @@ -165,12 +167,7 @@ void CUIProgressBar::FillDebugInfo() "Horizontal (from center to sides)", "Vertical (from center to sides)", }; - int mode = m_orient_mode; - if (ImGui::SliderInt("Mode", &mode, 0, om_count - 1, styles[m_orient_mode])) - { - m_orient_mode = (EOrientMode)mode; - update |= true; - } + update |= xray::imgui::Selector("Mode", m_orient_mode, styles, om_count); update |= ImGui::Button("Update"); From f3f343a1221ddcb3317c81c6d3e253bfc9a5d630 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 17 Feb 2026 02:56:02 +0300 Subject: [PATCH 190/261] Introduce Selector imgui control to be reused in few places --- src/xrEngine/Environment_editor.cpp | 39 +++---------------- src/xrEngine/editor_helper.h | 24 ++++++++++++ .../object_factory_spawner.cpp | 11 ++---- src/xrUICore/ProgressBar/UIProgressBar.cpp | 9 ++--- 4 files changed, 36 insertions(+), 47 deletions(-) diff --git a/src/xrEngine/Environment_editor.cpp b/src/xrEngine/Environment_editor.cpp index d7cf6659b89..ef604157c26 100644 --- a/src/xrEngine/Environment_editor.cpp +++ b/src/xrEngine/Environment_editor.cpp @@ -17,19 +17,11 @@ bool window_ambients = false; bool window_thunderbolts = false; bool window_level_weathers = false; -struct combo_raii -{ - ~combo_raii() - { - ImGui::EndCombo(); - } -}; - bool TimeFrameCombo(pcstr label, CEnvDescriptor*& descriptor, const CEnvironment::EnvVec* currentWeather) { if (ImGui::BeginCombo(label, descriptor ? descriptor->m_identifier.c_str() : "##")) { - combo_raii raii; + xray::imgui::combo_raii raii; if (ImGui::Selectable("##", !descriptor)) { @@ -53,35 +45,16 @@ bool TimeFrameCombo(pcstr label, CEnvDescriptor*& descriptor, const CEnvironment bool ConfigStyleSelector(pcstr label, bool& soc_style) { - using namespace xray::imgui; - - bool result = false; - enum ConfigStyle - { - ConfigStyle_SOC, - ConfigStyle_CSCOP, - ConfigStyle_COUNT - }; - constexpr pcstr styles[ConfigStyle_COUNT] = + constexpr pcstr styles[] = { + "CS/COP", "SOC", - "CS/COP" }; - int selected = soc_style ? ConfigStyle_SOC : ConfigStyle_CSCOP; - if (ImGui::SliderInt(label, &selected, 0, ConfigStyle_COUNT - 1, styles[selected])) - { - switch (static_cast(selected)) - { - case ConfigStyle_SOC: soc_style = true; break; - case ConfigStyle_CSCOP: soc_style = false; break; - } - result = true; - } - ItemHelp("This affects how environment color will be calculated in the mixer. " + return xray::imgui::Selector(label, soc_style, styles, std::size(styles), + "This affects how environment color will be calculated in the mixer. " "(look at the environment color in the mixer category)"); - return result; -} } +} // namespace #endif void CEnvDescriptor::ed_show_params(const CEnvironment& env) diff --git a/src/xrEngine/editor_helper.h b/src/xrEngine/editor_helper.h index b8232df6ce6..d16b2ad1980 100644 --- a/src/xrEngine/editor_helper.h +++ b/src/xrEngine/editor_helper.h @@ -16,6 +16,14 @@ struct ScopeID final : Noncopyable } }; +struct combo_raii +{ + ~combo_raii() + { + ImGui::EndCombo(); + } +}; + // Uses previous ItemID in the ID stack for the storage ID to allow // same ID for multiple items without causing ID conflicts bool CascadingCollapsingHeader(cpcstr label, ImGuiTreeNodeFlags flags = 0); @@ -47,6 +55,22 @@ inline bool MenuItemWithShortcut(pcstr label, EGameActions shortcut, const char* return result; } +template +inline bool Selector(cpcstr label, T& current_item, cpcstr items[], const int items_count, cpcstr desc = nullptr) +{ + VERIFY2(items, "There's no point in using Selector without text items."); + int selected = static_cast(current_item); + clamp(selected, 0, items_count - 1); + const bool result = ImGui::SliderInt(label, &selected, 0, items_count - 1, items ? items[selected] : "%d", ImGuiSliderFlags_NoInput); + if (result) + current_item = static_cast(selected); + if (items_count == 2 && ImGui::IsItemDeactivated() && !ImGui::IsItemDeactivatedAfterEdit()) + current_item = static_cast(!selected); + if (desc) + ItemHelp(desc); + return result; +} + inline bool InputText(pcstr label, shared_str& texture_name) { string_path temp; diff --git a/src/xrServerEntities/object_factory_spawner.cpp b/src/xrServerEntities/object_factory_spawner.cpp index 834c2f08c23..42206dbb33e 100644 --- a/src/xrServerEntities/object_factory_spawner.cpp +++ b/src/xrServerEntities/object_factory_spawner.cpp @@ -240,17 +240,12 @@ void CObjectFactory::on_tool_frame() constexpr pcstr styles[] = { "Game", - "LTX" + "LTX", }; ImGui::SetNextItemWidth(ImGui::CalcTextSize(" Game ").x); // shrink to minimal - ImGui::SliderInt("", &display_mode, 0, std::size(styles) - 1, styles[display_mode], ImGuiSliderFlags_NoInput); - if (ImGui::IsItemDeactivated() && !ImGui::IsItemDeactivatedAfterEdit()) - { - display_mode = !display_mode; - } - - imgui::ItemHelp("Left-click on the item in this list to spawn it.\n" + imgui::Selector("", display_mode, styles, std::size(styles), + "Left-click on the item in this list to spawn it.\n" "You can also select between displaying game names or ltx sections by using a switch."); ImGui::EndMenuBar(); } diff --git a/src/xrUICore/ProgressBar/UIProgressBar.cpp b/src/xrUICore/ProgressBar/UIProgressBar.cpp index 3e55849abed..ed64907b286 100644 --- a/src/xrUICore/ProgressBar/UIProgressBar.cpp +++ b/src/xrUICore/ProgressBar/UIProgressBar.cpp @@ -1,6 +1,8 @@ #include "pch.hpp" #include "UIProgressBar.h" +#include "xrEngine/editor_helper.h" + CUIProgressBar::CUIProgressBar() : CUIWindow("CUIProgressBar"), m_UIProgressItem("Progress"), m_UIBackgroundItem("Background") { @@ -165,12 +167,7 @@ void CUIProgressBar::FillDebugInfo() "Horizontal (from center to sides)", "Vertical (from center to sides)", }; - int mode = m_orient_mode; - if (ImGui::SliderInt("Mode", &mode, 0, om_count - 1, styles[m_orient_mode])) - { - m_orient_mode = (EOrientMode)mode; - update |= true; - } + update |= xray::imgui::Selector("Mode", m_orient_mode, styles, om_count); update |= ImGui::Button("Update"); From 6743d5eeab908fd5ca76dbd31c80bfc34851f3fb Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 17 Feb 2026 12:30:45 +0300 Subject: [PATCH 191/261] Correctly load sun flares and thunderbolts when CS/COP and SOC weathers are mixed in gamedata Was broken in 80e0af8211f09ff497fec6ca736c71ca398ec3d5 and now works even better --- src/xrEngine/thunderbolt.cpp | 23 +++++++++++++++++++---- src/xrEngine/xr_efflensflare.cpp | 12 +++++++++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/xrEngine/thunderbolt.cpp b/src/xrEngine/thunderbolt.cpp index 31167677791..f56d3183b12 100644 --- a/src/xrEngine/thunderbolt.cpp +++ b/src/xrEngine/thunderbolt.cpp @@ -150,11 +150,26 @@ SThunderboltCollection* CEffect_Thunderbolt::AppendDef(shared_str sect) if (item->section == sect) return item; - auto* result = xr_new(sect, - m_thunderbolt_collections_config ? m_thunderbolt_collections_config : pSettings, - m_thunderbolts_config ? m_thunderbolts_config : pSettings - ); + const CInifile* collections_config; + const CInifile* thunderbolts_config; + + if (m_thunderbolt_collections_config && m_thunderbolt_collections_config->section_exist(sect)) + { + collections_config = m_thunderbolt_collections_config; + thunderbolts_config = m_thunderbolts_config; + } + else if (pSettings->section_exist(sect)) + { + collections_config = pSettings; + thunderbolts_config = pSettings; + } + else + { + collections_config = m_thunderbolt_collections_config ? m_thunderbolt_collections_config : pSettings; + thunderbolts_config = m_thunderbolts_config ? m_thunderbolts_config : pSettings; + } + auto* result = xr_new(sect, collections_config, thunderbolts_config); return collections.emplace_back(result); } diff --git a/src/xrEngine/xr_efflensflare.cpp b/src/xrEngine/xr_efflensflare.cpp index 2d6ff84688b..83373c4f5f8 100644 --- a/src/xrEngine/xr_efflensflare.cpp +++ b/src/xrEngine/xr_efflensflare.cpp @@ -561,9 +561,15 @@ CLensFlareDescriptor* CLensFlare::AppendDef(shared_str sect) if (flare->section == sect) return flare; - const auto descriptor = xr_new(sect, - m_suns_config ? m_suns_config : pSettings - ); + const CInifile* suns_config; + if (m_suns_config && m_suns_config->section_exist(sect)) + suns_config = m_suns_config; + else if (pSettings->section_exist(sect)) + suns_config = pSettings; + else + suns_config = m_suns_config ? m_suns_config : pSettings; + + const auto descriptor = xr_new(sect, suns_config); return m_Palette.emplace_back(descriptor); } From a72392a9715eebbe8cd86cff318e64883226faa1 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 17 Feb 2026 12:30:45 +0300 Subject: [PATCH 192/261] Correctly load sun flares and thunderbolts when CS/COP and SOC weathers are mixed in gamedata Was broken in 80e0af8211f09ff497fec6ca736c71ca398ec3d5 and now works even better --- src/xrEngine/thunderbolt.cpp | 23 +++++++++++++++++++---- src/xrEngine/xr_efflensflare.cpp | 12 +++++++++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/xrEngine/thunderbolt.cpp b/src/xrEngine/thunderbolt.cpp index 31167677791..f56d3183b12 100644 --- a/src/xrEngine/thunderbolt.cpp +++ b/src/xrEngine/thunderbolt.cpp @@ -150,11 +150,26 @@ SThunderboltCollection* CEffect_Thunderbolt::AppendDef(shared_str sect) if (item->section == sect) return item; - auto* result = xr_new(sect, - m_thunderbolt_collections_config ? m_thunderbolt_collections_config : pSettings, - m_thunderbolts_config ? m_thunderbolts_config : pSettings - ); + const CInifile* collections_config; + const CInifile* thunderbolts_config; + + if (m_thunderbolt_collections_config && m_thunderbolt_collections_config->section_exist(sect)) + { + collections_config = m_thunderbolt_collections_config; + thunderbolts_config = m_thunderbolts_config; + } + else if (pSettings->section_exist(sect)) + { + collections_config = pSettings; + thunderbolts_config = pSettings; + } + else + { + collections_config = m_thunderbolt_collections_config ? m_thunderbolt_collections_config : pSettings; + thunderbolts_config = m_thunderbolts_config ? m_thunderbolts_config : pSettings; + } + auto* result = xr_new(sect, collections_config, thunderbolts_config); return collections.emplace_back(result); } diff --git a/src/xrEngine/xr_efflensflare.cpp b/src/xrEngine/xr_efflensflare.cpp index 2d6ff84688b..83373c4f5f8 100644 --- a/src/xrEngine/xr_efflensflare.cpp +++ b/src/xrEngine/xr_efflensflare.cpp @@ -561,9 +561,15 @@ CLensFlareDescriptor* CLensFlare::AppendDef(shared_str sect) if (flare->section == sect) return flare; - const auto descriptor = xr_new(sect, - m_suns_config ? m_suns_config : pSettings - ); + const CInifile* suns_config; + if (m_suns_config && m_suns_config->section_exist(sect)) + suns_config = m_suns_config; + else if (pSettings->section_exist(sect)) + suns_config = pSettings; + else + suns_config = m_suns_config ? m_suns_config : pSettings; + + const auto descriptor = xr_new(sect, suns_config); return m_Palette.emplace_back(descriptor); } From 5f0e021696e9ab520a92d670546b5913db1db56a Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 17 Feb 2026 12:44:37 +0300 Subject: [PATCH 193/261] xrEngine/xr_efflensflare.h|cpp: use const shared_str&, use in-class initializer and emplace_back The same in xrEngine/thunderbolt.h|cpp --- src/xrEngine/thunderbolt.cpp | 4 ++-- src/xrEngine/thunderbolt.h | 8 +++----- src/xrEngine/xr_efflensflare.cpp | 17 +++++++++-------- src/xrEngine/xr_efflensflare.h | 9 ++++----- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/xrEngine/thunderbolt.cpp b/src/xrEngine/thunderbolt.cpp index f56d3183b12..97d81bbbffd 100644 --- a/src/xrEngine/thunderbolt.cpp +++ b/src/xrEngine/thunderbolt.cpp @@ -58,7 +58,7 @@ SThunderboltDesc::~SThunderboltDesc() //---------------------------------------------------------------------------------------------- // collection //---------------------------------------------------------------------------------------------- -SThunderboltCollection::SThunderboltCollection(shared_str sect, CInifile const* pIni, CInifile const* thunderbolts) +SThunderboltCollection::SThunderboltCollection(const shared_str& sect, CInifile const* pIni, CInifile const* thunderbolts) { section = sect; const int tb_count = pIni->line_count(sect); @@ -141,7 +141,7 @@ CEffect_Thunderbolt::~CEffect_Thunderbolt() m_thunderbolts_config = nullptr; } -SThunderboltCollection* CEffect_Thunderbolt::AppendDef(shared_str sect) +SThunderboltCollection* CEffect_Thunderbolt::AppendDef(const shared_str& sect) { if (!sect || (0 == sect[0])) return nullptr; diff --git a/src/xrEngine/thunderbolt.h b/src/xrEngine/thunderbolt.h index ca880af1bf2..8e8dcefeb9e 100644 --- a/src/xrEngine/thunderbolt.h +++ b/src/xrEngine/thunderbolt.h @@ -67,11 +67,10 @@ struct ENGINE_API SThunderboltDesc struct ENGINE_API SThunderboltCollection { - using DescVec = xr_vector; - DescVec palette; + xr_vector palette; shared_str section; - SThunderboltCollection(shared_str sect, CInifile const* pIni, CInifile const* thunderbolts); + SThunderboltCollection(const shared_str& sect, CInifile const* pIni, CInifile const* thunderbolts); ~SThunderboltCollection(); SThunderboltDesc* GetRandomDesc() @@ -120,7 +119,6 @@ class ENGINE_API CEffect_Thunderbolt CInifile* m_thunderbolt_collections_config{}; CInifile* m_thunderbolts_config{}; - // params static constexpr float MAX_DIST_FACTOR = 0.95f; Fvector2 p_var_alt; @@ -143,7 +141,7 @@ class ENGINE_API CEffect_Thunderbolt void OnFrame(CEnvDescriptorMixer& currentEnv); void Render(); - SThunderboltCollection* AppendDef(shared_str sect); + SThunderboltCollection* AppendDef(const shared_str& sect); [[nodiscard]] auto& GetCollections() { return collections; } diff --git a/src/xrEngine/xr_efflensflare.cpp b/src/xrEngine/xr_efflensflare.cpp index 83373c4f5f8..c8561526a01 100644 --- a/src/xrEngine/xr_efflensflare.cpp +++ b/src/xrEngine/xr_efflensflare.cpp @@ -51,13 +51,14 @@ void CLensFlareDescriptor::SetGradient(float fMaxRadius, float fOpacity, pcstr t void CLensFlareDescriptor::AddFlare(float fRadius, float fOpacity, float fPosition, pcstr tex_name, pcstr sh_name) { - SFlare F; - F.fRadius = fRadius; - F.fOpacity = fOpacity; - F.fPosition = fPosition; - F.shader = sh_name; - F.texture = tex_name; - m_Flares.push_back(F); + m_Flares.emplace_back(SFlare + { + .fOpacity = fOpacity, + .fRadius = fRadius, + .fPosition = fPosition, + .texture = tex_name, + .shader = sh_name, + }); } struct FlareDescriptorFields @@ -79,7 +80,7 @@ FlareDescriptorFields SunFields = "sun", "sun_shader", "sun_texture", "sun_radius", "sun_ignore_color" }; -CLensFlareDescriptor::CLensFlareDescriptor(shared_str sect, CInifile const* pIni) +CLensFlareDescriptor::CLensFlareDescriptor(const shared_str& sect, CInifile const* pIni) { section = sect; diff --git a/src/xrEngine/xr_efflensflare.h b/src/xrEngine/xr_efflensflare.h index 886623831c1..d8cb0cb338f 100644 --- a/src/xrEngine/xr_efflensflare.h +++ b/src/xrEngine/xr_efflensflare.h @@ -26,13 +26,12 @@ class ENGINE_API CLensFlareDescriptor public: struct SFlare { - float fOpacity; - float fRadius; - float fPosition; + float fOpacity{}; + float fRadius{}; + float fPosition{}; shared_str texture; shared_str shader; FactoryPtr m_pRender; - SFlare() { fOpacity = fRadius = fPosition = 0; } }; struct SSource : public SFlare @@ -68,7 +67,7 @@ class ENGINE_API CLensFlareDescriptor shared_str section; public: - CLensFlareDescriptor(shared_str section, CInifile const* pIni); + CLensFlareDescriptor(const shared_str& section, CInifile const* pIni); void OnDeviceCreate(); void OnDeviceDestroy(); }; From f51c65f7a6b8d5a10b2d51a04c56a8c56cbece5f Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 17 Feb 2026 12:44:37 +0300 Subject: [PATCH 194/261] xrEngine/xr_efflensflare.h|cpp: use const shared_str&, use in-class initializer and emplace_back The same in xrEngine/thunderbolt.h|cpp --- src/xrEngine/thunderbolt.cpp | 4 ++-- src/xrEngine/thunderbolt.h | 8 +++----- src/xrEngine/xr_efflensflare.cpp | 17 +++++++++-------- src/xrEngine/xr_efflensflare.h | 9 ++++----- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/xrEngine/thunderbolt.cpp b/src/xrEngine/thunderbolt.cpp index f56d3183b12..97d81bbbffd 100644 --- a/src/xrEngine/thunderbolt.cpp +++ b/src/xrEngine/thunderbolt.cpp @@ -58,7 +58,7 @@ SThunderboltDesc::~SThunderboltDesc() //---------------------------------------------------------------------------------------------- // collection //---------------------------------------------------------------------------------------------- -SThunderboltCollection::SThunderboltCollection(shared_str sect, CInifile const* pIni, CInifile const* thunderbolts) +SThunderboltCollection::SThunderboltCollection(const shared_str& sect, CInifile const* pIni, CInifile const* thunderbolts) { section = sect; const int tb_count = pIni->line_count(sect); @@ -141,7 +141,7 @@ CEffect_Thunderbolt::~CEffect_Thunderbolt() m_thunderbolts_config = nullptr; } -SThunderboltCollection* CEffect_Thunderbolt::AppendDef(shared_str sect) +SThunderboltCollection* CEffect_Thunderbolt::AppendDef(const shared_str& sect) { if (!sect || (0 == sect[0])) return nullptr; diff --git a/src/xrEngine/thunderbolt.h b/src/xrEngine/thunderbolt.h index ca880af1bf2..8e8dcefeb9e 100644 --- a/src/xrEngine/thunderbolt.h +++ b/src/xrEngine/thunderbolt.h @@ -67,11 +67,10 @@ struct ENGINE_API SThunderboltDesc struct ENGINE_API SThunderboltCollection { - using DescVec = xr_vector; - DescVec palette; + xr_vector palette; shared_str section; - SThunderboltCollection(shared_str sect, CInifile const* pIni, CInifile const* thunderbolts); + SThunderboltCollection(const shared_str& sect, CInifile const* pIni, CInifile const* thunderbolts); ~SThunderboltCollection(); SThunderboltDesc* GetRandomDesc() @@ -120,7 +119,6 @@ class ENGINE_API CEffect_Thunderbolt CInifile* m_thunderbolt_collections_config{}; CInifile* m_thunderbolts_config{}; - // params static constexpr float MAX_DIST_FACTOR = 0.95f; Fvector2 p_var_alt; @@ -143,7 +141,7 @@ class ENGINE_API CEffect_Thunderbolt void OnFrame(CEnvDescriptorMixer& currentEnv); void Render(); - SThunderboltCollection* AppendDef(shared_str sect); + SThunderboltCollection* AppendDef(const shared_str& sect); [[nodiscard]] auto& GetCollections() { return collections; } diff --git a/src/xrEngine/xr_efflensflare.cpp b/src/xrEngine/xr_efflensflare.cpp index 83373c4f5f8..c8561526a01 100644 --- a/src/xrEngine/xr_efflensflare.cpp +++ b/src/xrEngine/xr_efflensflare.cpp @@ -51,13 +51,14 @@ void CLensFlareDescriptor::SetGradient(float fMaxRadius, float fOpacity, pcstr t void CLensFlareDescriptor::AddFlare(float fRadius, float fOpacity, float fPosition, pcstr tex_name, pcstr sh_name) { - SFlare F; - F.fRadius = fRadius; - F.fOpacity = fOpacity; - F.fPosition = fPosition; - F.shader = sh_name; - F.texture = tex_name; - m_Flares.push_back(F); + m_Flares.emplace_back(SFlare + { + .fOpacity = fOpacity, + .fRadius = fRadius, + .fPosition = fPosition, + .texture = tex_name, + .shader = sh_name, + }); } struct FlareDescriptorFields @@ -79,7 +80,7 @@ FlareDescriptorFields SunFields = "sun", "sun_shader", "sun_texture", "sun_radius", "sun_ignore_color" }; -CLensFlareDescriptor::CLensFlareDescriptor(shared_str sect, CInifile const* pIni) +CLensFlareDescriptor::CLensFlareDescriptor(const shared_str& sect, CInifile const* pIni) { section = sect; diff --git a/src/xrEngine/xr_efflensflare.h b/src/xrEngine/xr_efflensflare.h index 886623831c1..d8cb0cb338f 100644 --- a/src/xrEngine/xr_efflensflare.h +++ b/src/xrEngine/xr_efflensflare.h @@ -26,13 +26,12 @@ class ENGINE_API CLensFlareDescriptor public: struct SFlare { - float fOpacity; - float fRadius; - float fPosition; + float fOpacity{}; + float fRadius{}; + float fPosition{}; shared_str texture; shared_str shader; FactoryPtr m_pRender; - SFlare() { fOpacity = fRadius = fPosition = 0; } }; struct SSource : public SFlare @@ -68,7 +67,7 @@ class ENGINE_API CLensFlareDescriptor shared_str section; public: - CLensFlareDescriptor(shared_str section, CInifile const* pIni); + CLensFlareDescriptor(const shared_str& section, CInifile const* pIni); void OnDeviceCreate(); void OnDeviceDestroy(); }; From be1fba86cb9a547b6649cf6408d816fb22695f2e Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 17 Feb 2026 12:45:20 +0300 Subject: [PATCH 195/261] xrCore/xr_ini.h|cpp: allow to write includes into the file --- src/xrCore/xr_ini.cpp | 25 +++++++++++++++++++++++++ src/xrCore/xr_ini.h | 3 +++ 2 files changed, 28 insertions(+) diff --git a/src/xrCore/xr_ini.cpp b/src/xrCore/xr_ini.cpp index 9ac7808b6db..c4249f1b756 100644 --- a/src/xrCore/xr_ini.cpp +++ b/src/xrCore/xr_ini.cpp @@ -643,6 +643,15 @@ void CInifile::Load(IReader* F, pcstr path, allow_include_func_t allow_include_f void CInifile::save_as(IWriter& writer, bool bcheck) const { string4096 temp, val; + for (const auto& include : m_includes) + { + strconcat(temp, "#include \"", include.c_str(), "\""); + writer.w_string(temp); + } + + if (!m_includes.empty()) + writer.w_string(" "); + for (auto r_it = DATA.begin(); r_it != DATA.end(); ++r_it) { xr_sprintf(temp, sizeof temp, "[%s]", (*r_it)->Name.c_str()); @@ -959,6 +968,11 @@ bool CInifile::r_line(const shared_str& S, int L, pcstr* N, pcstr* V) const { re //-------------------------------------------------------------------------------------------------------- // Write functions //-------------------------------------------------------------------------------------- +void CInifile::w_include(cpcstr include) +{ + m_includes.emplace_back(include); +} + void CInifile::w_string(pcstr S, pcstr L, pcstr V, pcstr comment) { R_ASSERT(!m_flags.test(eReadOnly)); @@ -1130,6 +1144,7 @@ void CInifile::w_fvector4(pcstr S, pcstr L, const Fvector4& V, pcstr comment) } void CInifile::w_bool(pcstr S, pcstr L, bool V, pcstr comment) { w_string(S, L, V ? "on" : "off", comment); } + void CInifile::remove_line(pcstr S, pcstr L) { R_ASSERT(!m_flags.test(eReadOnly)); @@ -1143,6 +1158,16 @@ void CInifile::remove_line(pcstr S, pcstr L) } } +void CInifile::remove_include(cpcstr include) +{ + const auto it = std::find_if(m_includes.begin(), m_includes.end(), [&](const xr_string& incl) + { + return xr_strcmp(incl.c_str(), include) == 0; + }); + if (it != m_includes.end()) + m_includes.erase(it); +} + template<> XRCORE_API pcstr CInifile::read(pcstr section, pcstr line) const { diff --git a/src/xrCore/xr_ini.h b/src/xrCore/xr_ini.h index 653a2d3946e..1113a899446 100644 --- a/src/xrCore/xr_ini.h +++ b/src/xrCore/xr_ini.h @@ -68,6 +68,7 @@ class XRCORE_API CInifile Flags8 m_flags; string_path m_file_name; Root DATA; + xr_vector m_includes; void Load(IReader* F, pcstr path, allow_include_func_t allow_include_func = nullptr); @@ -237,6 +238,7 @@ class XRCORE_API CInifile bool r_line(pcstr S, int L, pcstr* N, pcstr* V) const; bool r_line(const shared_str& S, int L, pcstr* N, pcstr* V) const; + void w_include(pcstr include); void w_string(pcstr S, pcstr L, pcstr V, pcstr comment = nullptr); void w_u8(pcstr S, pcstr L, u8 V, pcstr comment = nullptr); void w_u16(pcstr S, pcstr L, u16 V, pcstr comment = nullptr); @@ -258,6 +260,7 @@ class XRCORE_API CInifile void w_bool(pcstr S, pcstr L, bool V, pcstr comment = nullptr); void remove_line(pcstr S, pcstr L); + void remove_include(pcstr include); }; #define READ_IF_EXISTS(ltx, method, section, name, default_value) \ From c0f8185034f6e582bed3bd48e038d24a040757c5 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 17 Feb 2026 12:45:20 +0300 Subject: [PATCH 196/261] xrCore/xr_ini.h|cpp: allow to write includes into the file --- src/xrCore/xr_ini.cpp | 25 +++++++++++++++++++++++++ src/xrCore/xr_ini.h | 3 +++ 2 files changed, 28 insertions(+) diff --git a/src/xrCore/xr_ini.cpp b/src/xrCore/xr_ini.cpp index 9ac7808b6db..c4249f1b756 100644 --- a/src/xrCore/xr_ini.cpp +++ b/src/xrCore/xr_ini.cpp @@ -643,6 +643,15 @@ void CInifile::Load(IReader* F, pcstr path, allow_include_func_t allow_include_f void CInifile::save_as(IWriter& writer, bool bcheck) const { string4096 temp, val; + for (const auto& include : m_includes) + { + strconcat(temp, "#include \"", include.c_str(), "\""); + writer.w_string(temp); + } + + if (!m_includes.empty()) + writer.w_string(" "); + for (auto r_it = DATA.begin(); r_it != DATA.end(); ++r_it) { xr_sprintf(temp, sizeof temp, "[%s]", (*r_it)->Name.c_str()); @@ -959,6 +968,11 @@ bool CInifile::r_line(const shared_str& S, int L, pcstr* N, pcstr* V) const { re //-------------------------------------------------------------------------------------------------------- // Write functions //-------------------------------------------------------------------------------------- +void CInifile::w_include(cpcstr include) +{ + m_includes.emplace_back(include); +} + void CInifile::w_string(pcstr S, pcstr L, pcstr V, pcstr comment) { R_ASSERT(!m_flags.test(eReadOnly)); @@ -1130,6 +1144,7 @@ void CInifile::w_fvector4(pcstr S, pcstr L, const Fvector4& V, pcstr comment) } void CInifile::w_bool(pcstr S, pcstr L, bool V, pcstr comment) { w_string(S, L, V ? "on" : "off", comment); } + void CInifile::remove_line(pcstr S, pcstr L) { R_ASSERT(!m_flags.test(eReadOnly)); @@ -1143,6 +1158,16 @@ void CInifile::remove_line(pcstr S, pcstr L) } } +void CInifile::remove_include(cpcstr include) +{ + const auto it = std::find_if(m_includes.begin(), m_includes.end(), [&](const xr_string& incl) + { + return xr_strcmp(incl.c_str(), include) == 0; + }); + if (it != m_includes.end()) + m_includes.erase(it); +} + template<> XRCORE_API pcstr CInifile::read(pcstr section, pcstr line) const { diff --git a/src/xrCore/xr_ini.h b/src/xrCore/xr_ini.h index 653a2d3946e..1113a899446 100644 --- a/src/xrCore/xr_ini.h +++ b/src/xrCore/xr_ini.h @@ -68,6 +68,7 @@ class XRCORE_API CInifile Flags8 m_flags; string_path m_file_name; Root DATA; + xr_vector m_includes; void Load(IReader* F, pcstr path, allow_include_func_t allow_include_func = nullptr); @@ -237,6 +238,7 @@ class XRCORE_API CInifile bool r_line(pcstr S, int L, pcstr* N, pcstr* V) const; bool r_line(const shared_str& S, int L, pcstr* N, pcstr* V) const; + void w_include(pcstr include); void w_string(pcstr S, pcstr L, pcstr V, pcstr comment = nullptr); void w_u8(pcstr S, pcstr L, u8 V, pcstr comment = nullptr); void w_u16(pcstr S, pcstr L, u16 V, pcstr comment = nullptr); @@ -258,6 +260,7 @@ class XRCORE_API CInifile void w_bool(pcstr S, pcstr L, bool V, pcstr comment = nullptr); void remove_line(pcstr S, pcstr L); + void remove_include(pcstr include); }; #define READ_IF_EXISTS(ltx, method, section, name, default_value) \ From 1cdf0880be98f1ae08253a0d7a3e12ab42e28e08 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 17 Feb 2026 13:46:15 +0300 Subject: [PATCH 197/261] Allow to edit sun flares and partially thunderbolts in the weather editor --- src/xrEngine/Environment_editor.cpp | 60 +++++-------- src/xrEngine/Environment_misc.cpp | 22 ++--- src/xrEngine/thunderbolt.cpp | 112 +++++++++++++++++++++++++ src/xrEngine/thunderbolt.h | 9 ++ src/xrEngine/xr_efflensflare.cpp | 126 ++++++++++++++++++++++++++++ src/xrEngine/xr_efflensflare.h | 8 ++ 6 files changed, 287 insertions(+), 50 deletions(-) diff --git a/src/xrEngine/Environment_editor.cpp b/src/xrEngine/Environment_editor.cpp index ef604157c26..01039edd343 100644 --- a/src/xrEngine/Environment_editor.cpp +++ b/src/xrEngine/Environment_editor.cpp @@ -12,9 +12,7 @@ namespace { bool window_weather_cycle = false; -bool window_suns = false; bool window_ambients = false; -bool window_thunderbolts = false; bool window_level_weathers = false; bool TimeFrameCombo(pcstr label, CEnvDescriptor*& descriptor, const CEnvironment::EnvVec* currentWeather) @@ -68,10 +66,6 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) { if (ImGui::BeginCombo("sun##lensflareid", lens_flare ? lens_flare->section.c_str() : "")) { - if (ImGui::Selectable("", false)) - { - window_suns = true; - } if (ImGui::Selectable("##", !lens_flare)) lens_flare = nullptr; for (CLensFlareDescriptor* desc : env.eff_LensFlare->GetDescriptors()) @@ -85,6 +79,15 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) "CS/COP: sun\n" " SOC: flares"); + ImGui::SameLine(); + ImGui::Button("Edit##flares"); + if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonLeft)) + { + if (lens_flare) + lens_flare->ed_show_params(); + ImGui::EndPopup(); + } + ImGui::ColorEdit3("sun color", reinterpret_cast(&sun_color)); ImGui::Checkbox("Dynamic sun direction", &use_dynamic_sun_dir); @@ -199,12 +202,8 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) } if (CascadingCollapsingHeader("thunderbolts##category", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::BeginCombo("thunderbolts", thunderbolt ? thunderbolt->section.c_str() : "")) + if (ImGui::BeginCombo("bolts", thunderbolt ? thunderbolt->section.c_str() : "")) { - if (ImGui::Selectable("", false)) - { - window_thunderbolts = true; - } if (ImGui::Selectable("##", !thunderbolt)) thunderbolt = nullptr; for (const auto& bolts : env.eff_Thunderbolt->GetCollections()) @@ -218,6 +217,16 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) "CS/COP: thunderbolts_collection\n" " SOC: thunderbolt"); + ImGui::SameLine(); + ImGui::Button("Edit##thunderbolts"); + if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonLeft)) + { + env.eff_Thunderbolt->ED_ShowParams(); + if (thunderbolt) + thunderbolt->ed_show_params(); + ImGui::EndPopup(); + } + ImGui::DragFloat("duration", &bolt_duration); ImGui::DragFloat("period", &bolt_period); } @@ -254,33 +263,6 @@ void CEnvDescriptorMixer::ed_show_params(const CEnvironment& env) #endif } -void CEffect_Thunderbolt::ED_ShowParams() -{ -#ifndef MASTER_GOLD - using namespace xray::imgui; - - float altitude[2] = { rad2deg(p_var_alt.x) , rad2deg(p_var_alt.y) }; - if (ImGui::DragFloat2("altitude", altitude, 0.5f, -360.0f, 360.f)) - p_var_alt = { rad2deg(altitude[0]) , rad2deg(altitude[1]) }; - - float deltalongitude = rad2deg(p_var_long); - if (ImGui::DragFloat("wind direction", &deltalongitude, 0.5f, -360.0f, 360.f)) - p_var_long = deg2rad(deltalongitude); - - ImGui::DragFloat("minimum distance factor", &p_min_dist, 0.001f, 0.0f, MAX_DIST_FACTOR); - ItemHelp("Distance from far plane"); - - float tilt = rad2deg(p_tilt); - if (ImGui::DragFloat("tilt", &tilt, 0.01f, 15.0f, 30.f)) - p_tilt = deg2rad(tilt); - - ImGui::DragFloat("second probability", &p_second_prop, 0.001f, 0.0f, 1.0f); - ImGui::DragFloat("sky color", &p_sky_color, 0.001f, 0.0f, 1.0f); - ImGui::DragFloat("sun color", &p_sun_color, 0.001f, 0.0f, 1.0f); - ImGui::DragFloat("fog color", &p_fog_color, 0.001f, 0.0f, 1.0f); -#endif -} - void CEnvironment::on_tool_frame() { #ifndef MASTER_GOLD @@ -310,9 +292,7 @@ void CEnvironment::on_tool_frame() if (ImGui::BeginMenu("Windows")) { if (ImGui::MenuItem("Weather cycles", nullptr, &window_weather_cycle)) {} - if (ImGui::MenuItem("Suns", nullptr, &window_suns)) {} if (ImGui::MenuItem("Ambients", nullptr, &window_ambients)) {} - if (ImGui::MenuItem("Thunderbolts", nullptr, &window_thunderbolts)) {} if (ImGui::MenuItem("Level weathers", nullptr, &window_level_weathers)) {} ImGui::EndMenu(); diff --git a/src/xrEngine/Environment_misc.cpp b/src/xrEngine/Environment_misc.cpp index ac2afa11628..bf103377436 100644 --- a/src/xrEngine/Environment_misc.cpp +++ b/src/xrEngine/Environment_misc.cpp @@ -1055,10 +1055,18 @@ void CEnvironment::save() const FS.update_path(environment_config_path, "$game_config$", "weathers\\environment.ltx"); CInifile* environment_config = xr_new(environment_config_path, false, false, false); + environment_config->w_include("env_ambient.ltx"); + environment_config->w_include("flares.ltx"); + environment_config->w_include("thunderbolt.ltx"); save_weathers(environment_config); save_weather_effects(environment_config); + const bool soc_style = !environment_config->sections().empty(); + eff_LensFlare->save(soc_style); + // XXX: save thunderbolts, sound channels, effects, etc. + + environment_config->save_at_end(soc_style); CInifile::Destroy(environment_config); } @@ -1073,14 +1081,14 @@ void CEnvironment::save_weathers(CInifile* environment_config /*= nullptr*/) con string_path weathers_path_soc; FS.update_path(weathers_path_soc, "$game_config$", "weathers\\weather_"); - bool should_save_environment_config = false; for (const auto& [name, descriptors] : WeatherCycles) { const bool soc_style = descriptors.soc_style; string_path weather_sect; if (soc_style && environment_config) { - should_save_environment_config = true; + strconcat(weather_sect, "weather_", name.c_str(), ".ltx"); + environment_config->w_include(weather_sect); strconcat(weather_sect, "sect_weather_", name.c_str()); environment_config->w_string("weathers", name.c_str(), weather_sect); } @@ -1108,9 +1116,6 @@ void CEnvironment::save_weathers(CInifile* environment_config /*= nullptr*/) con CInifile::Destroy(config); } - - if (should_save_environment_config && environment_config) - environment_config->save_at_end(true); } void CEnvironment::save_weather_effects(CInifile* environment_config /*= nullptr*/) const @@ -1124,14 +1129,14 @@ void CEnvironment::save_weather_effects(CInifile* environment_config /*= nullptr string_path effects_path_soc; FS.update_path(effects_path_soc, "$game_config$", "weathers\\weather_"); - bool should_save_environment_config = false; for (const auto& [name, descriptors] : WeatherFXs) { const bool soc_style = descriptors.soc_style; string_path weather_sect; if (soc_style && environment_config) { - should_save_environment_config = true; + strconcat(weather_sect, "weather_", name.c_str(), ".ltx"); + environment_config->w_include(weather_sect); strconcat(weather_sect, "sect_weather_", name.c_str()); environment_config->w_string("weather_effects", name.c_str(), weather_sect); } @@ -1159,7 +1164,4 @@ void CEnvironment::save_weather_effects(CInifile* environment_config /*= nullptr CInifile::Destroy(config); } - - if (should_save_environment_config && environment_config) - environment_config->save_at_end(true); } diff --git a/src/xrEngine/thunderbolt.cpp b/src/xrEngine/thunderbolt.cpp index 97d81bbbffd..3e7d3401945 100644 --- a/src/xrEngine/thunderbolt.cpp +++ b/src/xrEngine/thunderbolt.cpp @@ -4,6 +4,7 @@ #include "Render.h" #endif #include "thunderbolt.h" + #include "IGame_Persistent.h" #include "LightAnimLibrary.h" @@ -15,6 +16,8 @@ #include "xr_object.h" #endif +#include "editor_helper.h" + SThunderboltDesc::SThunderboltDesc(const CInifile& pIni, shared_str const& sect) { m_GradientTop = create_gradient("gradient_top", pIni, sect); @@ -100,6 +103,18 @@ CEffect_Thunderbolt::CEffect_Thunderbolt() m_thunderbolt_collections_config = load_config("environment\\thunderbolt_collections.ltx"); m_thunderbolts_config = load_config("environment\\thunderbolts.ltx"); +#ifndef MASTER_GOLD + // Load all sections so weather editor can see all of them + if (m_thunderbolt_collections_config && m_thunderbolts_config) + { + for (const auto& section : m_thunderbolt_collections_config->sections()) + { + auto* result = xr_new(section->Name, m_thunderbolt_collections_config, m_thunderbolts_config); + collections.emplace_back(result); + } + } +#endif + pcstr section = "environment"; CInifile const* config = load_config("environment\\environment.ltx"); @@ -315,3 +330,100 @@ void CEffect_Thunderbolt::Render() if (state == stWorking) m_pRender->Render(*this); } + +void CEffect_Thunderbolt::ED_ShowParams() +{ +#ifndef MASTER_GOLD + using namespace xray::imgui; + + ImGui::SeparatorText("Common"); + float altitude[2] = { rad2deg(p_var_alt.x) , rad2deg(p_var_alt.y) }; + if (ImGui::DragFloat2("altitude", altitude, 0.5f, -360.0f, 360.f)) + p_var_alt = { rad2deg(altitude[0]) , rad2deg(altitude[1]) }; + + float deltalongitude = rad2deg(p_var_long); + if (ImGui::DragFloat("wind direction", &deltalongitude, 0.5f, -360.0f, 360.f)) + p_var_long = deg2rad(deltalongitude); + + ImGui::DragFloat("minimum distance factor", &p_min_dist, 0.001f, 0.0f, MAX_DIST_FACTOR); + ItemHelp("Distance from far plane"); + + float tilt = rad2deg(p_tilt); + if (ImGui::DragFloat("tilt", &tilt, 0.01f, 15.0f, 30.f)) + p_tilt = deg2rad(tilt); + + ImGui::DragFloat("second probability", &p_second_prop, 0.001f, 0.0f, 1.0f); + ImGui::DragFloat("sky color", &p_sky_color, 0.001f, 0.0f, 1.0f); + ImGui::DragFloat("sun color", &p_sun_color, 0.001f, 0.0f, 1.0f); + ImGui::DragFloat("fog color", &p_fog_color, 0.001f, 0.0f, 1.0f); +#endif +} + +void SThunderboltDesc::SFlare::ed_show_params() +{ +#ifndef MASTER_GOLD + using namespace xray::imgui; + ScopeID scope{ this }; + ImGui::DragFloat("Opacity", &fOpacity); + ImGui::DragFloat2("Radius", reinterpret_cast(&fRadius)); + if (InputText("Shader", shader)) + { + m_pFlare->DestroyShader(); + m_pFlare->CreateShader(shader.c_str(), texture.c_str()); + } + if (InputText("Texture", texture)) + { + m_pFlare->DestroyShader(); + m_pFlare->CreateShader(shader.c_str(), texture.c_str()); + } +#endif +} + +void SThunderboltDesc::ed_show_params() +{ +#ifndef MASTER_GOLD + // XXX: color anim, model, sound + ImGui::SeparatorText("Top gradient"); + m_GradientTop->ed_show_params(); + ImGui::SeparatorText("Center gradient"); + m_GradientCenter->ed_show_params(); +#endif +} + +void SThunderboltCollection::ed_show_params() +{ +#ifndef MASTER_GOLD + if (ImGui::BeginTabBar("Thunderbolts")) + { + for (auto& bolt : palette) + { + if (ImGui::BeginTabItem(bolt->name.c_str())) + { + bolt->ed_show_params(); + ImGui::EndTabItem(); + } + } + // XXX: add new thunderbolt + //if (ImGui::TabItemButton("+", ImGuiTabItemFlags_Trailing | ImGuiTabItemFlags_NoTooltip)) + // ; + ImGui::EndTabBar(); + } +#endif +} + +void SThunderboltDesc::SFlare::save(CInifile* config) const +{ +} + +void SThunderboltDesc::save(CInifile* config) const +{ +} + +void SThunderboltCollection::save(CInifile* config) const +{ +} + +void CEffect_Thunderbolt::save() +{ + // XXX: save +} diff --git a/src/xrEngine/thunderbolt.h b/src/xrEngine/thunderbolt.h index 8e8dcefeb9e..a312f025f3e 100644 --- a/src/xrEngine/thunderbolt.h +++ b/src/xrEngine/thunderbolt.h @@ -53,6 +53,9 @@ struct ENGINE_API SThunderboltDesc { m_pFlare->DestroyShader(); } + + void ed_show_params(); // ImGui editor + void save(CInifile* config) const; }; SFlare* m_GradientTop; SFlare* m_GradientCenter; @@ -63,6 +66,8 @@ struct ENGINE_API SThunderboltDesc SThunderboltDesc(const CInifile& pIni, shared_str const& sect); ~SThunderboltDesc(); static SFlare* create_gradient(pcstr gradient_name, const CInifile& config, shared_str const& sect); + void ed_show_params(); // ImGui editor + void save(CInifile* config) const; }; struct ENGINE_API SThunderboltCollection @@ -78,6 +83,9 @@ struct ENGINE_API SThunderboltCollection VERIFY(palette.size() > 0); return palette[Random.randI(palette.size())]; } + + void ed_show_params(); // ImGui editor + void save(CInifile* config) const; }; #define THUNDERBOLT_CACHE_SIZE 8 @@ -147,4 +155,5 @@ class ENGINE_API CEffect_Thunderbolt auto& GetCollections() { return collections; } void ED_ShowParams(); // ImGui editor + void save(); }; diff --git a/src/xrEngine/xr_efflensflare.cpp b/src/xrEngine/xr_efflensflare.cpp index c8561526a01..b0be1b60341 100644 --- a/src/xrEngine/xr_efflensflare.cpp +++ b/src/xrEngine/xr_efflensflare.cpp @@ -21,6 +21,8 @@ #include "IGame_Level.h" #endif +#include "editor_helper.h" + #define FAR_DIST g_pGamePersistent->Environment().CurrentEnv.far_plane //#define MAX_Flares 24 @@ -61,6 +63,42 @@ void CLensFlareDescriptor::AddFlare(float fRadius, float fOpacity, float fPositi }); } +bool CLensFlareDescriptor::SFlare::ed_show_params() +{ +#ifndef MASTER_GOLD + using namespace xray::imgui; + ScopeID scope{ this }; + bool result{}; + result |= ImGui::DragFloat("Opacity", &fOpacity); + result |= ImGui::DragFloat("Radius", &fRadius); + result |= ImGui::DragFloat("Position", &fPosition); + if (InputText("Shader", shader)) + { + m_pRender->DestroyShader(); + m_pRender->CreateShader(shader.c_str(), texture.c_str()); + result = true; + } + if (InputText("Texture", texture)) + { + m_pRender->DestroyShader(); + m_pRender->CreateShader(shader.c_str(), texture.c_str()); + result = true; + } + return result; +#endif +} + +bool CLensFlareDescriptor::SSource::ed_show_params() +{ +#ifndef MASTER_GOLD + using namespace xray::imgui; + bool result = SFlare::ed_show_params(); + ScopeID scope{ this }; + result |= ImGui::Checkbox("Ignore color", &ignore_color); + return result; +#endif +} + struct FlareDescriptorFields { pcstr line; @@ -164,6 +202,62 @@ void CLensFlareDescriptor::OnDeviceDestroy() flare.m_pRender->DestroyShader(); } +void CLensFlareDescriptor::ed_show_params() +{ +#ifndef MASTER_GOLD + using namespace xray::imgui; + + float blend_rise_time = 1.f / m_StateBlendUpSpeed; + if (ImGui::DragFloat("Blend rise time", &blend_rise_time)) + m_StateBlendUpSpeed = 1.f / (_max(blend_rise_time, 0.f) + EPS_S); + + float blend_down_time = 1.f / m_StateBlendDnSpeed; + if (ImGui::DragFloat("Blend down time", &blend_down_time)) + m_StateBlendUpSpeed = 1.f / (_max(blend_down_time, 0.f) + EPS_S); + + bool v = m_Flags.is(flSource); + if (ImGui::Checkbox("Source", &v)) + m_Flags.set(flSource, v); + ItemHelp("Name in configs: \n" + "CS/COP: sun\n" + " SOC: source"); + + ImGui::SameLine(); + v = m_Flags.is(flFlare); + if (ImGui::Checkbox("Flares", &v)) + m_Flags.set(flFlare, v); + + ImGui::SameLine(); + v = m_Flags.is(flGradient); + if (ImGui::Checkbox("Gradient", &v)) + m_Flags.set(flGradient, v); + + ImGui::SeparatorText("Source"); + m_Source.ed_show_params(); + + ImGui::SeparatorText("Flares"); + if (ImGui::BeginTabBar("Flares")) + { + size_t id = 1; + for (auto& flare : m_Flares) + { + if (ImGui::BeginTabItem(std::to_string(id).c_str())) + { + flare.ed_show_params(); + ImGui::EndTabItem(); + } + ++id; + } + if (ImGui::TabItemButton("+", ImGuiTabItemFlags_Trailing | ImGuiTabItemFlags_NoTooltip)) + AddFlare(0.f, 0.f, 0.f, nullptr, "effects\\flare"); + ImGui::EndTabBar(); + } + + ImGui::SeparatorText("Gradient"); + m_Gradient.ed_show_params(); +#endif +} + //------------------------------------------------------------------------------ CLensFlare::CLensFlare() { @@ -192,6 +286,18 @@ CLensFlare::CLensFlare() if (FS.exist(filePath, "$game_config$", "environment\\suns.ltx")) m_suns_config = xr_new(filePath, true, true, false); +#ifndef MASTER_GOLD + // Load all sections so weather editor can see all of them + if (m_suns_config) + { + for (const auto& section : m_suns_config->sections()) + { + const auto descriptor = xr_new(section->Name, m_suns_config); + m_Palette.emplace_back(descriptor); + } + } +#endif + OnDeviceCreate(); } @@ -593,3 +699,23 @@ void CLensFlare::OnDeviceDestroy() // VS m_pRender->OnDeviceDestroy(); } + +void CLensFlareDescriptor::save(CInifile* config) const +{ +} + +void CLensFlare::save(bool soc_style) const +{ + string_path path; + + if (soc_style) + { + FS.update_path(path, "$game_config$", "weathers\\flares.ltx"); + } + else if (!FS.update_path(path, "$game_weathers$", "suns.ltx", false)) + { + FS.update_path(path, "$game_config$", "environment\\suns.ltx"); + } + + // XXX: save +} diff --git a/src/xrEngine/xr_efflensflare.h b/src/xrEngine/xr_efflensflare.h index d8cb0cb338f..cd91d545af8 100644 --- a/src/xrEngine/xr_efflensflare.h +++ b/src/xrEngine/xr_efflensflare.h @@ -32,11 +32,14 @@ class ENGINE_API CLensFlareDescriptor shared_str texture; shared_str shader; FactoryPtr m_pRender; + + bool ed_show_params(); // ImGui editor }; struct SSource : public SFlare { bool ignore_color; + bool ed_show_params(); // ImGui editor }; using FlareVec = xr_vector; @@ -70,6 +73,9 @@ class ENGINE_API CLensFlareDescriptor CLensFlareDescriptor(const shared_str& section, CInifile const* pIni); void OnDeviceCreate(); void OnDeviceDestroy(); + + void ed_show_params(); // ImGui editor + void save(CInifile* config) const; }; class ENGINE_API CLensFlare @@ -136,4 +142,6 @@ class ENGINE_API CLensFlare [[nodiscard]] auto& GetDescriptors() { return m_Palette; } + + void save(bool soc_style) const; }; From 01750e71c606fe0f2cec792faab209d49bf9692a Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 17 Feb 2026 13:46:15 +0300 Subject: [PATCH 198/261] Allow to edit sun flares and partially thunderbolts in the weather editor --- src/xrEngine/Environment_editor.cpp | 60 +++++-------- src/xrEngine/Environment_misc.cpp | 22 ++--- src/xrEngine/thunderbolt.cpp | 112 +++++++++++++++++++++++++ src/xrEngine/thunderbolt.h | 9 ++ src/xrEngine/xr_efflensflare.cpp | 126 ++++++++++++++++++++++++++++ src/xrEngine/xr_efflensflare.h | 8 ++ 6 files changed, 287 insertions(+), 50 deletions(-) diff --git a/src/xrEngine/Environment_editor.cpp b/src/xrEngine/Environment_editor.cpp index ef604157c26..01039edd343 100644 --- a/src/xrEngine/Environment_editor.cpp +++ b/src/xrEngine/Environment_editor.cpp @@ -12,9 +12,7 @@ namespace { bool window_weather_cycle = false; -bool window_suns = false; bool window_ambients = false; -bool window_thunderbolts = false; bool window_level_weathers = false; bool TimeFrameCombo(pcstr label, CEnvDescriptor*& descriptor, const CEnvironment::EnvVec* currentWeather) @@ -68,10 +66,6 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) { if (ImGui::BeginCombo("sun##lensflareid", lens_flare ? lens_flare->section.c_str() : "")) { - if (ImGui::Selectable("", false)) - { - window_suns = true; - } if (ImGui::Selectable("##", !lens_flare)) lens_flare = nullptr; for (CLensFlareDescriptor* desc : env.eff_LensFlare->GetDescriptors()) @@ -85,6 +79,15 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) "CS/COP: sun\n" " SOC: flares"); + ImGui::SameLine(); + ImGui::Button("Edit##flares"); + if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonLeft)) + { + if (lens_flare) + lens_flare->ed_show_params(); + ImGui::EndPopup(); + } + ImGui::ColorEdit3("sun color", reinterpret_cast(&sun_color)); ImGui::Checkbox("Dynamic sun direction", &use_dynamic_sun_dir); @@ -199,12 +202,8 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) } if (CascadingCollapsingHeader("thunderbolts##category", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::BeginCombo("thunderbolts", thunderbolt ? thunderbolt->section.c_str() : "")) + if (ImGui::BeginCombo("bolts", thunderbolt ? thunderbolt->section.c_str() : "")) { - if (ImGui::Selectable("", false)) - { - window_thunderbolts = true; - } if (ImGui::Selectable("##", !thunderbolt)) thunderbolt = nullptr; for (const auto& bolts : env.eff_Thunderbolt->GetCollections()) @@ -218,6 +217,16 @@ void CEnvDescriptor::ed_show_params(const CEnvironment& env) "CS/COP: thunderbolts_collection\n" " SOC: thunderbolt"); + ImGui::SameLine(); + ImGui::Button("Edit##thunderbolts"); + if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonLeft)) + { + env.eff_Thunderbolt->ED_ShowParams(); + if (thunderbolt) + thunderbolt->ed_show_params(); + ImGui::EndPopup(); + } + ImGui::DragFloat("duration", &bolt_duration); ImGui::DragFloat("period", &bolt_period); } @@ -254,33 +263,6 @@ void CEnvDescriptorMixer::ed_show_params(const CEnvironment& env) #endif } -void CEffect_Thunderbolt::ED_ShowParams() -{ -#ifndef MASTER_GOLD - using namespace xray::imgui; - - float altitude[2] = { rad2deg(p_var_alt.x) , rad2deg(p_var_alt.y) }; - if (ImGui::DragFloat2("altitude", altitude, 0.5f, -360.0f, 360.f)) - p_var_alt = { rad2deg(altitude[0]) , rad2deg(altitude[1]) }; - - float deltalongitude = rad2deg(p_var_long); - if (ImGui::DragFloat("wind direction", &deltalongitude, 0.5f, -360.0f, 360.f)) - p_var_long = deg2rad(deltalongitude); - - ImGui::DragFloat("minimum distance factor", &p_min_dist, 0.001f, 0.0f, MAX_DIST_FACTOR); - ItemHelp("Distance from far plane"); - - float tilt = rad2deg(p_tilt); - if (ImGui::DragFloat("tilt", &tilt, 0.01f, 15.0f, 30.f)) - p_tilt = deg2rad(tilt); - - ImGui::DragFloat("second probability", &p_second_prop, 0.001f, 0.0f, 1.0f); - ImGui::DragFloat("sky color", &p_sky_color, 0.001f, 0.0f, 1.0f); - ImGui::DragFloat("sun color", &p_sun_color, 0.001f, 0.0f, 1.0f); - ImGui::DragFloat("fog color", &p_fog_color, 0.001f, 0.0f, 1.0f); -#endif -} - void CEnvironment::on_tool_frame() { #ifndef MASTER_GOLD @@ -310,9 +292,7 @@ void CEnvironment::on_tool_frame() if (ImGui::BeginMenu("Windows")) { if (ImGui::MenuItem("Weather cycles", nullptr, &window_weather_cycle)) {} - if (ImGui::MenuItem("Suns", nullptr, &window_suns)) {} if (ImGui::MenuItem("Ambients", nullptr, &window_ambients)) {} - if (ImGui::MenuItem("Thunderbolts", nullptr, &window_thunderbolts)) {} if (ImGui::MenuItem("Level weathers", nullptr, &window_level_weathers)) {} ImGui::EndMenu(); diff --git a/src/xrEngine/Environment_misc.cpp b/src/xrEngine/Environment_misc.cpp index ac2afa11628..bf103377436 100644 --- a/src/xrEngine/Environment_misc.cpp +++ b/src/xrEngine/Environment_misc.cpp @@ -1055,10 +1055,18 @@ void CEnvironment::save() const FS.update_path(environment_config_path, "$game_config$", "weathers\\environment.ltx"); CInifile* environment_config = xr_new(environment_config_path, false, false, false); + environment_config->w_include("env_ambient.ltx"); + environment_config->w_include("flares.ltx"); + environment_config->w_include("thunderbolt.ltx"); save_weathers(environment_config); save_weather_effects(environment_config); + const bool soc_style = !environment_config->sections().empty(); + eff_LensFlare->save(soc_style); + // XXX: save thunderbolts, sound channels, effects, etc. + + environment_config->save_at_end(soc_style); CInifile::Destroy(environment_config); } @@ -1073,14 +1081,14 @@ void CEnvironment::save_weathers(CInifile* environment_config /*= nullptr*/) con string_path weathers_path_soc; FS.update_path(weathers_path_soc, "$game_config$", "weathers\\weather_"); - bool should_save_environment_config = false; for (const auto& [name, descriptors] : WeatherCycles) { const bool soc_style = descriptors.soc_style; string_path weather_sect; if (soc_style && environment_config) { - should_save_environment_config = true; + strconcat(weather_sect, "weather_", name.c_str(), ".ltx"); + environment_config->w_include(weather_sect); strconcat(weather_sect, "sect_weather_", name.c_str()); environment_config->w_string("weathers", name.c_str(), weather_sect); } @@ -1108,9 +1116,6 @@ void CEnvironment::save_weathers(CInifile* environment_config /*= nullptr*/) con CInifile::Destroy(config); } - - if (should_save_environment_config && environment_config) - environment_config->save_at_end(true); } void CEnvironment::save_weather_effects(CInifile* environment_config /*= nullptr*/) const @@ -1124,14 +1129,14 @@ void CEnvironment::save_weather_effects(CInifile* environment_config /*= nullptr string_path effects_path_soc; FS.update_path(effects_path_soc, "$game_config$", "weathers\\weather_"); - bool should_save_environment_config = false; for (const auto& [name, descriptors] : WeatherFXs) { const bool soc_style = descriptors.soc_style; string_path weather_sect; if (soc_style && environment_config) { - should_save_environment_config = true; + strconcat(weather_sect, "weather_", name.c_str(), ".ltx"); + environment_config->w_include(weather_sect); strconcat(weather_sect, "sect_weather_", name.c_str()); environment_config->w_string("weather_effects", name.c_str(), weather_sect); } @@ -1159,7 +1164,4 @@ void CEnvironment::save_weather_effects(CInifile* environment_config /*= nullptr CInifile::Destroy(config); } - - if (should_save_environment_config && environment_config) - environment_config->save_at_end(true); } diff --git a/src/xrEngine/thunderbolt.cpp b/src/xrEngine/thunderbolt.cpp index 97d81bbbffd..3e7d3401945 100644 --- a/src/xrEngine/thunderbolt.cpp +++ b/src/xrEngine/thunderbolt.cpp @@ -4,6 +4,7 @@ #include "Render.h" #endif #include "thunderbolt.h" + #include "IGame_Persistent.h" #include "LightAnimLibrary.h" @@ -15,6 +16,8 @@ #include "xr_object.h" #endif +#include "editor_helper.h" + SThunderboltDesc::SThunderboltDesc(const CInifile& pIni, shared_str const& sect) { m_GradientTop = create_gradient("gradient_top", pIni, sect); @@ -100,6 +103,18 @@ CEffect_Thunderbolt::CEffect_Thunderbolt() m_thunderbolt_collections_config = load_config("environment\\thunderbolt_collections.ltx"); m_thunderbolts_config = load_config("environment\\thunderbolts.ltx"); +#ifndef MASTER_GOLD + // Load all sections so weather editor can see all of them + if (m_thunderbolt_collections_config && m_thunderbolts_config) + { + for (const auto& section : m_thunderbolt_collections_config->sections()) + { + auto* result = xr_new(section->Name, m_thunderbolt_collections_config, m_thunderbolts_config); + collections.emplace_back(result); + } + } +#endif + pcstr section = "environment"; CInifile const* config = load_config("environment\\environment.ltx"); @@ -315,3 +330,100 @@ void CEffect_Thunderbolt::Render() if (state == stWorking) m_pRender->Render(*this); } + +void CEffect_Thunderbolt::ED_ShowParams() +{ +#ifndef MASTER_GOLD + using namespace xray::imgui; + + ImGui::SeparatorText("Common"); + float altitude[2] = { rad2deg(p_var_alt.x) , rad2deg(p_var_alt.y) }; + if (ImGui::DragFloat2("altitude", altitude, 0.5f, -360.0f, 360.f)) + p_var_alt = { rad2deg(altitude[0]) , rad2deg(altitude[1]) }; + + float deltalongitude = rad2deg(p_var_long); + if (ImGui::DragFloat("wind direction", &deltalongitude, 0.5f, -360.0f, 360.f)) + p_var_long = deg2rad(deltalongitude); + + ImGui::DragFloat("minimum distance factor", &p_min_dist, 0.001f, 0.0f, MAX_DIST_FACTOR); + ItemHelp("Distance from far plane"); + + float tilt = rad2deg(p_tilt); + if (ImGui::DragFloat("tilt", &tilt, 0.01f, 15.0f, 30.f)) + p_tilt = deg2rad(tilt); + + ImGui::DragFloat("second probability", &p_second_prop, 0.001f, 0.0f, 1.0f); + ImGui::DragFloat("sky color", &p_sky_color, 0.001f, 0.0f, 1.0f); + ImGui::DragFloat("sun color", &p_sun_color, 0.001f, 0.0f, 1.0f); + ImGui::DragFloat("fog color", &p_fog_color, 0.001f, 0.0f, 1.0f); +#endif +} + +void SThunderboltDesc::SFlare::ed_show_params() +{ +#ifndef MASTER_GOLD + using namespace xray::imgui; + ScopeID scope{ this }; + ImGui::DragFloat("Opacity", &fOpacity); + ImGui::DragFloat2("Radius", reinterpret_cast(&fRadius)); + if (InputText("Shader", shader)) + { + m_pFlare->DestroyShader(); + m_pFlare->CreateShader(shader.c_str(), texture.c_str()); + } + if (InputText("Texture", texture)) + { + m_pFlare->DestroyShader(); + m_pFlare->CreateShader(shader.c_str(), texture.c_str()); + } +#endif +} + +void SThunderboltDesc::ed_show_params() +{ +#ifndef MASTER_GOLD + // XXX: color anim, model, sound + ImGui::SeparatorText("Top gradient"); + m_GradientTop->ed_show_params(); + ImGui::SeparatorText("Center gradient"); + m_GradientCenter->ed_show_params(); +#endif +} + +void SThunderboltCollection::ed_show_params() +{ +#ifndef MASTER_GOLD + if (ImGui::BeginTabBar("Thunderbolts")) + { + for (auto& bolt : palette) + { + if (ImGui::BeginTabItem(bolt->name.c_str())) + { + bolt->ed_show_params(); + ImGui::EndTabItem(); + } + } + // XXX: add new thunderbolt + //if (ImGui::TabItemButton("+", ImGuiTabItemFlags_Trailing | ImGuiTabItemFlags_NoTooltip)) + // ; + ImGui::EndTabBar(); + } +#endif +} + +void SThunderboltDesc::SFlare::save(CInifile* config) const +{ +} + +void SThunderboltDesc::save(CInifile* config) const +{ +} + +void SThunderboltCollection::save(CInifile* config) const +{ +} + +void CEffect_Thunderbolt::save() +{ + // XXX: save +} diff --git a/src/xrEngine/thunderbolt.h b/src/xrEngine/thunderbolt.h index 8e8dcefeb9e..a312f025f3e 100644 --- a/src/xrEngine/thunderbolt.h +++ b/src/xrEngine/thunderbolt.h @@ -53,6 +53,9 @@ struct ENGINE_API SThunderboltDesc { m_pFlare->DestroyShader(); } + + void ed_show_params(); // ImGui editor + void save(CInifile* config) const; }; SFlare* m_GradientTop; SFlare* m_GradientCenter; @@ -63,6 +66,8 @@ struct ENGINE_API SThunderboltDesc SThunderboltDesc(const CInifile& pIni, shared_str const& sect); ~SThunderboltDesc(); static SFlare* create_gradient(pcstr gradient_name, const CInifile& config, shared_str const& sect); + void ed_show_params(); // ImGui editor + void save(CInifile* config) const; }; struct ENGINE_API SThunderboltCollection @@ -78,6 +83,9 @@ struct ENGINE_API SThunderboltCollection VERIFY(palette.size() > 0); return palette[Random.randI(palette.size())]; } + + void ed_show_params(); // ImGui editor + void save(CInifile* config) const; }; #define THUNDERBOLT_CACHE_SIZE 8 @@ -147,4 +155,5 @@ class ENGINE_API CEffect_Thunderbolt auto& GetCollections() { return collections; } void ED_ShowParams(); // ImGui editor + void save(); }; diff --git a/src/xrEngine/xr_efflensflare.cpp b/src/xrEngine/xr_efflensflare.cpp index c8561526a01..b0be1b60341 100644 --- a/src/xrEngine/xr_efflensflare.cpp +++ b/src/xrEngine/xr_efflensflare.cpp @@ -21,6 +21,8 @@ #include "IGame_Level.h" #endif +#include "editor_helper.h" + #define FAR_DIST g_pGamePersistent->Environment().CurrentEnv.far_plane //#define MAX_Flares 24 @@ -61,6 +63,42 @@ void CLensFlareDescriptor::AddFlare(float fRadius, float fOpacity, float fPositi }); } +bool CLensFlareDescriptor::SFlare::ed_show_params() +{ +#ifndef MASTER_GOLD + using namespace xray::imgui; + ScopeID scope{ this }; + bool result{}; + result |= ImGui::DragFloat("Opacity", &fOpacity); + result |= ImGui::DragFloat("Radius", &fRadius); + result |= ImGui::DragFloat("Position", &fPosition); + if (InputText("Shader", shader)) + { + m_pRender->DestroyShader(); + m_pRender->CreateShader(shader.c_str(), texture.c_str()); + result = true; + } + if (InputText("Texture", texture)) + { + m_pRender->DestroyShader(); + m_pRender->CreateShader(shader.c_str(), texture.c_str()); + result = true; + } + return result; +#endif +} + +bool CLensFlareDescriptor::SSource::ed_show_params() +{ +#ifndef MASTER_GOLD + using namespace xray::imgui; + bool result = SFlare::ed_show_params(); + ScopeID scope{ this }; + result |= ImGui::Checkbox("Ignore color", &ignore_color); + return result; +#endif +} + struct FlareDescriptorFields { pcstr line; @@ -164,6 +202,62 @@ void CLensFlareDescriptor::OnDeviceDestroy() flare.m_pRender->DestroyShader(); } +void CLensFlareDescriptor::ed_show_params() +{ +#ifndef MASTER_GOLD + using namespace xray::imgui; + + float blend_rise_time = 1.f / m_StateBlendUpSpeed; + if (ImGui::DragFloat("Blend rise time", &blend_rise_time)) + m_StateBlendUpSpeed = 1.f / (_max(blend_rise_time, 0.f) + EPS_S); + + float blend_down_time = 1.f / m_StateBlendDnSpeed; + if (ImGui::DragFloat("Blend down time", &blend_down_time)) + m_StateBlendUpSpeed = 1.f / (_max(blend_down_time, 0.f) + EPS_S); + + bool v = m_Flags.is(flSource); + if (ImGui::Checkbox("Source", &v)) + m_Flags.set(flSource, v); + ItemHelp("Name in configs: \n" + "CS/COP: sun\n" + " SOC: source"); + + ImGui::SameLine(); + v = m_Flags.is(flFlare); + if (ImGui::Checkbox("Flares", &v)) + m_Flags.set(flFlare, v); + + ImGui::SameLine(); + v = m_Flags.is(flGradient); + if (ImGui::Checkbox("Gradient", &v)) + m_Flags.set(flGradient, v); + + ImGui::SeparatorText("Source"); + m_Source.ed_show_params(); + + ImGui::SeparatorText("Flares"); + if (ImGui::BeginTabBar("Flares")) + { + size_t id = 1; + for (auto& flare : m_Flares) + { + if (ImGui::BeginTabItem(std::to_string(id).c_str())) + { + flare.ed_show_params(); + ImGui::EndTabItem(); + } + ++id; + } + if (ImGui::TabItemButton("+", ImGuiTabItemFlags_Trailing | ImGuiTabItemFlags_NoTooltip)) + AddFlare(0.f, 0.f, 0.f, nullptr, "effects\\flare"); + ImGui::EndTabBar(); + } + + ImGui::SeparatorText("Gradient"); + m_Gradient.ed_show_params(); +#endif +} + //------------------------------------------------------------------------------ CLensFlare::CLensFlare() { @@ -192,6 +286,18 @@ CLensFlare::CLensFlare() if (FS.exist(filePath, "$game_config$", "environment\\suns.ltx")) m_suns_config = xr_new(filePath, true, true, false); +#ifndef MASTER_GOLD + // Load all sections so weather editor can see all of them + if (m_suns_config) + { + for (const auto& section : m_suns_config->sections()) + { + const auto descriptor = xr_new(section->Name, m_suns_config); + m_Palette.emplace_back(descriptor); + } + } +#endif + OnDeviceCreate(); } @@ -593,3 +699,23 @@ void CLensFlare::OnDeviceDestroy() // VS m_pRender->OnDeviceDestroy(); } + +void CLensFlareDescriptor::save(CInifile* config) const +{ +} + +void CLensFlare::save(bool soc_style) const +{ + string_path path; + + if (soc_style) + { + FS.update_path(path, "$game_config$", "weathers\\flares.ltx"); + } + else if (!FS.update_path(path, "$game_weathers$", "suns.ltx", false)) + { + FS.update_path(path, "$game_config$", "environment\\suns.ltx"); + } + + // XXX: save +} diff --git a/src/xrEngine/xr_efflensflare.h b/src/xrEngine/xr_efflensflare.h index d8cb0cb338f..cd91d545af8 100644 --- a/src/xrEngine/xr_efflensflare.h +++ b/src/xrEngine/xr_efflensflare.h @@ -32,11 +32,14 @@ class ENGINE_API CLensFlareDescriptor shared_str texture; shared_str shader; FactoryPtr m_pRender; + + bool ed_show_params(); // ImGui editor }; struct SSource : public SFlare { bool ignore_color; + bool ed_show_params(); // ImGui editor }; using FlareVec = xr_vector; @@ -70,6 +73,9 @@ class ENGINE_API CLensFlareDescriptor CLensFlareDescriptor(const shared_str& section, CInifile const* pIni); void OnDeviceCreate(); void OnDeviceDestroy(); + + void ed_show_params(); // ImGui editor + void save(CInifile* config) const; }; class ENGINE_API CLensFlare @@ -136,4 +142,6 @@ class ENGINE_API CLensFlare [[nodiscard]] auto& GetDescriptors() { return m_Palette; } + + void save(bool soc_style) const; }; From 0f2bbb75a0b1877ef0166fe5f760649cdd57afba Mon Sep 17 00:00:00 2001 From: Sultan Luchezarniy Date: Thu, 19 Feb 2026 19:14:05 +0300 Subject: [PATCH 199/261] GitHub Actions: switch utility workflows to ubuntu-slim image (#2053) * Switch pull request labeler to ubuntu-slim image * Switch mirror workflow to ubuntu-slim image This also switches the workflow to another mirroring action (https://github.com/marketplace/actions/mirror-to) because previous doesn't work with ubuntu-slim currently. --- .github/workflows/labeler.yml | 2 +- .github/workflows/mirror.yml | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 145743e6d00..99ad75d0834 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -11,7 +11,7 @@ permissions: jobs: labeler: - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - name: on new pull request if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.opened == true || github.event.pull_request.reopened == true }} diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 1311adf8f3f..8095ee84a8b 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -5,27 +5,33 @@ on: [push, delete, workflow_dispatch] jobs: mirror: if: github.repository == 'OpenXRay/xray-16' - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Bitbucket.org - uses: pixta-dev/repository-mirroring-action@v1.1.1 + uses: Xottab-DUTY/git-mirror@v1.0.0 with: - target_repo_url: git@bitbucket.org:OpenXRay/xray-16.git - ssh_private_key: ${{ secrets.RABOTYAGA_BITBUCKET_PRIVATE_SSH_KEY }} + target: git@bitbucket.org:OpenXRay/xray-16.git + password: ${{ secrets.RABOTYAGA_BITBUCKET_PRIVATE_SSH_KEY }} + push_args: "--tags --prune --force" + refspec: "refs/remotes/origin/*:refs/heads/*" - name: gitflic.ru - uses: pixta-dev/repository-mirroring-action@v1.1.1 + uses: Xottab-DUTY/git-mirror@v1.0.0 with: - target_repo_url: git@gitflic.ru:openxray/xray-16.git - ssh_private_key: ${{ secrets.GITFLIC_PRIVATE_SSH_KEY }} + target: git@gitflic.ru:openxray/xray-16.git + password: ${{ secrets.GITFLIC_PRIVATE_SSH_KEY }} + push_args: "--tags --prune --force" + refspec: "refs/remotes/origin/*:refs/heads/*" - name: abf.io continue-on-error: true - uses: pixta-dev/repository-mirroring-action@v1.1.1 + uses: Xottab-DUTY/git-mirror@v1.0.0 with: - target_repo_url: git@abf.io:openxray/xray-16.git - ssh_private_key: ${{ secrets.ABF_IO_PRIVATE_SSH_KEY }} + target: git@abf.io:openxray/xray-16.git + password: ${{ secrets.ABF_IO_PRIVATE_SSH_KEY }} + push_args: "--tags --prune --force" + refspec: "refs/remotes/origin/*:refs/heads/*" From 8ff7be4ba652ab99dd83bcb0269ea5c7d3eba28e Mon Sep 17 00:00:00 2001 From: Sultan Luchezarniy Date: Thu, 19 Feb 2026 19:14:05 +0300 Subject: [PATCH 200/261] GitHub Actions: switch utility workflows to ubuntu-slim image (#2053) * Switch pull request labeler to ubuntu-slim image * Switch mirror workflow to ubuntu-slim image This also switches the workflow to another mirroring action (https://github.com/marketplace/actions/mirror-to) because previous doesn't work with ubuntu-slim currently. --- .github/workflows/labeler.yml | 2 +- .github/workflows/mirror.yml | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 145743e6d00..99ad75d0834 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -11,7 +11,7 @@ permissions: jobs: labeler: - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - name: on new pull request if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.opened == true || github.event.pull_request.reopened == true }} diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 1311adf8f3f..8095ee84a8b 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -5,27 +5,33 @@ on: [push, delete, workflow_dispatch] jobs: mirror: if: github.repository == 'OpenXRay/xray-16' - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Bitbucket.org - uses: pixta-dev/repository-mirroring-action@v1.1.1 + uses: Xottab-DUTY/git-mirror@v1.0.0 with: - target_repo_url: git@bitbucket.org:OpenXRay/xray-16.git - ssh_private_key: ${{ secrets.RABOTYAGA_BITBUCKET_PRIVATE_SSH_KEY }} + target: git@bitbucket.org:OpenXRay/xray-16.git + password: ${{ secrets.RABOTYAGA_BITBUCKET_PRIVATE_SSH_KEY }} + push_args: "--tags --prune --force" + refspec: "refs/remotes/origin/*:refs/heads/*" - name: gitflic.ru - uses: pixta-dev/repository-mirroring-action@v1.1.1 + uses: Xottab-DUTY/git-mirror@v1.0.0 with: - target_repo_url: git@gitflic.ru:openxray/xray-16.git - ssh_private_key: ${{ secrets.GITFLIC_PRIVATE_SSH_KEY }} + target: git@gitflic.ru:openxray/xray-16.git + password: ${{ secrets.GITFLIC_PRIVATE_SSH_KEY }} + push_args: "--tags --prune --force" + refspec: "refs/remotes/origin/*:refs/heads/*" - name: abf.io continue-on-error: true - uses: pixta-dev/repository-mirroring-action@v1.1.1 + uses: Xottab-DUTY/git-mirror@v1.0.0 with: - target_repo_url: git@abf.io:openxray/xray-16.git - ssh_private_key: ${{ secrets.ABF_IO_PRIVATE_SSH_KEY }} + target: git@abf.io:openxray/xray-16.git + password: ${{ secrets.ABF_IO_PRIVATE_SSH_KEY }} + push_args: "--tags --prune --force" + refspec: "refs/remotes/origin/*:refs/heads/*" From fcbdfcbe39a59cb99c4e4e60d6fc548e1628dc9d Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 25 Feb 2026 03:05:23 +0300 Subject: [PATCH 201/261] Hardened weapon addons bones interactions (fixes #2055) --- src/Layers/xrRender/SkeletonCustom.cpp | 3 +- src/Layers/xrRender/SkeletonCustom.h | 11 +--- src/xrGame/Weapon.cpp | 86 ++++++++------------------ 3 files changed, 31 insertions(+), 69 deletions(-) diff --git a/src/Layers/xrRender/SkeletonCustom.cpp b/src/Layers/xrRender/SkeletonCustom.cpp index 5f27211cf0c..c6c4bf19e50 100644 --- a/src/Layers/xrRender/SkeletonCustom.cpp +++ b/src/Layers/xrRender/SkeletonCustom.cpp @@ -493,7 +493,8 @@ void CKinematics::Release() void CKinematics::LL_SetBoneVisible(u16 bone_id, BOOL val, BOOL bRecursive) { - VERIFY(bone_id < LL_BoneCount()); + R_ASSERT1_CURE(bone_id < LL_BoneCount(), { return; }); + u64 mask = u64(1) << bone_id; visimask.set(mask, val); if (!visimask.is(mask)) diff --git a/src/Layers/xrRender/SkeletonCustom.h b/src/Layers/xrRender/SkeletonCustom.h index 8c8631c1999..d239f66f656 100644 --- a/src/Layers/xrRender/SkeletonCustom.h +++ b/src/Layers/xrRender/SkeletonCustom.h @@ -221,12 +221,7 @@ class CKinematics : public FHierrarhyVisual, public IKinematics } CBoneData* LL_GetBoneData(u16 bone_id) { - VERIFY(bone_id < LL_BoneCount()); - VERIFY(bones); - u32 sz = sizeof(vecBones); - u32 sz1 = sizeof(((*bones)[bone_id])->children); - Msg("sz: %d", sz); - Msg("sz1: %d", sz1); + R_ASSERT1_CURE(bones && bone_id < LL_BoneCount(), { return nullptr; }); CBoneData* bd = ((*bones)[bone_id]); return bd; } @@ -254,13 +249,13 @@ class CKinematics : public FHierrarhyVisual, public IKinematics u16 LL_GetBoneRoot() override { return iRoot; } void LL_SetBoneRoot(u16 bone_id) override { - VERIFY(bone_id < LL_BoneCount()); + R_ASSERT1_CURE(bone_id < LL_BoneCount(), { return; }); iRoot = bone_id; } BOOL LL_GetBoneVisible(u16 bone_id) override { - VERIFY(bone_id < LL_BoneCount()); + R_ASSERT1_CURE(bone_id < LL_BoneCount(), { return false; }); return visimask.is(u64(1) << bone_id); } void LL_SetBoneVisible(u16 bone_id, BOOL val, BOOL bRecursive) override; diff --git a/src/xrGame/Weapon.cpp b/src/xrGame/Weapon.cpp index 23eb3356cb6..777e234e803 100644 --- a/src/xrGame/Weapon.cpp +++ b/src/xrGame/Weapon.cpp @@ -1357,76 +1357,42 @@ void CWeapon::UpdateAddonsVisibility() static shared_str wpn_grenade_launcher = WPN_GRENADE_LAUNCHER; static shared_str wpn_grenade_launcher_soc = WPN_GRENADE_LAUNCHER_SOC; - IKinematics* pWeaponVisual = smart_cast(Visual()); - R_ASSERT(pWeaponVisual); - - u16 bone_id; UpdateHUDAddonsVisibility(); + IKinematics* pWeaponVisual = smart_cast(Visual()); + R_ASSERT1_CURE(pWeaponVisual, { return; }); + pWeaponVisual->CalculateBones_Invalidate(); - bone_id = pWeaponVisual->LL_BoneID(wpn_scope); - if (ScopeAttachable()) - { - if (IsScopeAttached()) - { - if (!pWeaponVisual->LL_GetBoneVisible(bone_id)) - pWeaponVisual->LL_SetBoneVisible(bone_id, TRUE, TRUE); - } - else - { - if (pWeaponVisual->LL_GetBoneVisible(bone_id)) - pWeaponVisual->LL_SetBoneVisible(bone_id, FALSE, TRUE); - } - } - if (m_eScopeStatus == ALife::eAddonDisabled && bone_id != BI_NONE && pWeaponVisual->LL_GetBoneVisible(bone_id)) - { - pWeaponVisual->LL_SetBoneVisible(bone_id, FALSE, TRUE); - // Log("scope", pWeaponVisual->LL_GetBoneVisible (bone_id)); - } - bone_id = pWeaponVisual->LL_BoneID(wpn_silencer); - if (SilencerAttachable()) - { - if (IsSilencerAttached()) - { - if (!pWeaponVisual->LL_GetBoneVisible(bone_id)) - pWeaponVisual->LL_SetBoneVisible(bone_id, TRUE, TRUE); - } - else - { - if (pWeaponVisual->LL_GetBoneVisible(bone_id)) - pWeaponVisual->LL_SetBoneVisible(bone_id, FALSE, TRUE); - } - } - if (m_eSilencerStatus == ALife::eAddonDisabled && bone_id != BI_NONE && pWeaponVisual->LL_GetBoneVisible(bone_id)) + const auto checkBone = [this, pWeaponVisual](const shared_str& bone, + const ALife::EWeaponAddonStatus status, const CSE_ALifeItemWeapon::EWeaponAddonState flag) { - pWeaponVisual->LL_SetBoneVisible(bone_id, FALSE, TRUE); - // Log("silencer", pWeaponVisual->LL_GetBoneVisible (bone_id)); - } - - bone_id = pWeaponVisual->LL_BoneID(wpn_grenade_launcher); - if (bone_id == BI_NONE) - bone_id = pWeaponVisual->LL_BoneID(wpn_grenade_launcher_soc); + auto bone_id = pWeaponVisual->LL_BoneID(bone); + if (bone_id == BI_NONE) + return; - if (GrenadeLauncherAttachable()) - { - if (IsGrenadeLauncherAttached()) - { - if (!pWeaponVisual->LL_GetBoneVisible(bone_id)) - pWeaponVisual->LL_SetBoneVisible(bone_id, TRUE, TRUE); - } - else + switch (status) { + case ALife::eAddonAttachable: + if (0 != (m_flagsAddOnState & flag)) + { + if (!pWeaponVisual->LL_GetBoneVisible(bone_id)) + pWeaponVisual->LL_SetBoneVisible(bone_id, TRUE, TRUE); + break; + } + [[fallthrough]]; + + case ALife::eAddonDisabled: if (pWeaponVisual->LL_GetBoneVisible(bone_id)) pWeaponVisual->LL_SetBoneVisible(bone_id, FALSE, TRUE); + break; } - } - if (m_eGrenadeLauncherStatus == ALife::eAddonDisabled && bone_id != BI_NONE && - pWeaponVisual->LL_GetBoneVisible(bone_id)) - { - pWeaponVisual->LL_SetBoneVisible(bone_id, FALSE, TRUE); - // Log("gl", pWeaponVisual->LL_GetBoneVisible (bone_id)); - } + }; + + checkBone(wpn_scope, m_eScopeStatus, CSE_ALifeItemWeapon::eWeaponAddonScope); + checkBone(wpn_silencer, m_eSilencerStatus, CSE_ALifeItemWeapon::eWeaponAddonSilencer); + checkBone(wpn_grenade_launcher, m_eGrenadeLauncherStatus, CSE_ALifeItemWeapon::eWeaponAddonGrenadeLauncher); + checkBone(wpn_grenade_launcher_soc, m_eGrenadeLauncherStatus, CSE_ALifeItemWeapon::eWeaponAddonGrenadeLauncher); pWeaponVisual->CalculateBones_Invalidate(); pWeaponVisual->CalculateBones(TRUE); From 6761557854abbe2098f797f1e33a88f995d3a441 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 25 Feb 2026 03:05:23 +0300 Subject: [PATCH 202/261] Hardened weapon addons bones interactions (fixes #2055) --- src/Layers/xrRender/SkeletonCustom.cpp | 3 +- src/Layers/xrRender/SkeletonCustom.h | 11 +--- src/xrGame/Weapon.cpp | 86 ++++++++------------------ 3 files changed, 31 insertions(+), 69 deletions(-) diff --git a/src/Layers/xrRender/SkeletonCustom.cpp b/src/Layers/xrRender/SkeletonCustom.cpp index 5f27211cf0c..c6c4bf19e50 100644 --- a/src/Layers/xrRender/SkeletonCustom.cpp +++ b/src/Layers/xrRender/SkeletonCustom.cpp @@ -493,7 +493,8 @@ void CKinematics::Release() void CKinematics::LL_SetBoneVisible(u16 bone_id, BOOL val, BOOL bRecursive) { - VERIFY(bone_id < LL_BoneCount()); + R_ASSERT1_CURE(bone_id < LL_BoneCount(), { return; }); + u64 mask = u64(1) << bone_id; visimask.set(mask, val); if (!visimask.is(mask)) diff --git a/src/Layers/xrRender/SkeletonCustom.h b/src/Layers/xrRender/SkeletonCustom.h index 8c8631c1999..d239f66f656 100644 --- a/src/Layers/xrRender/SkeletonCustom.h +++ b/src/Layers/xrRender/SkeletonCustom.h @@ -221,12 +221,7 @@ class CKinematics : public FHierrarhyVisual, public IKinematics } CBoneData* LL_GetBoneData(u16 bone_id) { - VERIFY(bone_id < LL_BoneCount()); - VERIFY(bones); - u32 sz = sizeof(vecBones); - u32 sz1 = sizeof(((*bones)[bone_id])->children); - Msg("sz: %d", sz); - Msg("sz1: %d", sz1); + R_ASSERT1_CURE(bones && bone_id < LL_BoneCount(), { return nullptr; }); CBoneData* bd = ((*bones)[bone_id]); return bd; } @@ -254,13 +249,13 @@ class CKinematics : public FHierrarhyVisual, public IKinematics u16 LL_GetBoneRoot() override { return iRoot; } void LL_SetBoneRoot(u16 bone_id) override { - VERIFY(bone_id < LL_BoneCount()); + R_ASSERT1_CURE(bone_id < LL_BoneCount(), { return; }); iRoot = bone_id; } BOOL LL_GetBoneVisible(u16 bone_id) override { - VERIFY(bone_id < LL_BoneCount()); + R_ASSERT1_CURE(bone_id < LL_BoneCount(), { return false; }); return visimask.is(u64(1) << bone_id); } void LL_SetBoneVisible(u16 bone_id, BOOL val, BOOL bRecursive) override; diff --git a/src/xrGame/Weapon.cpp b/src/xrGame/Weapon.cpp index 23eb3356cb6..777e234e803 100644 --- a/src/xrGame/Weapon.cpp +++ b/src/xrGame/Weapon.cpp @@ -1357,76 +1357,42 @@ void CWeapon::UpdateAddonsVisibility() static shared_str wpn_grenade_launcher = WPN_GRENADE_LAUNCHER; static shared_str wpn_grenade_launcher_soc = WPN_GRENADE_LAUNCHER_SOC; - IKinematics* pWeaponVisual = smart_cast(Visual()); - R_ASSERT(pWeaponVisual); - - u16 bone_id; UpdateHUDAddonsVisibility(); + IKinematics* pWeaponVisual = smart_cast(Visual()); + R_ASSERT1_CURE(pWeaponVisual, { return; }); + pWeaponVisual->CalculateBones_Invalidate(); - bone_id = pWeaponVisual->LL_BoneID(wpn_scope); - if (ScopeAttachable()) - { - if (IsScopeAttached()) - { - if (!pWeaponVisual->LL_GetBoneVisible(bone_id)) - pWeaponVisual->LL_SetBoneVisible(bone_id, TRUE, TRUE); - } - else - { - if (pWeaponVisual->LL_GetBoneVisible(bone_id)) - pWeaponVisual->LL_SetBoneVisible(bone_id, FALSE, TRUE); - } - } - if (m_eScopeStatus == ALife::eAddonDisabled && bone_id != BI_NONE && pWeaponVisual->LL_GetBoneVisible(bone_id)) - { - pWeaponVisual->LL_SetBoneVisible(bone_id, FALSE, TRUE); - // Log("scope", pWeaponVisual->LL_GetBoneVisible (bone_id)); - } - bone_id = pWeaponVisual->LL_BoneID(wpn_silencer); - if (SilencerAttachable()) - { - if (IsSilencerAttached()) - { - if (!pWeaponVisual->LL_GetBoneVisible(bone_id)) - pWeaponVisual->LL_SetBoneVisible(bone_id, TRUE, TRUE); - } - else - { - if (pWeaponVisual->LL_GetBoneVisible(bone_id)) - pWeaponVisual->LL_SetBoneVisible(bone_id, FALSE, TRUE); - } - } - if (m_eSilencerStatus == ALife::eAddonDisabled && bone_id != BI_NONE && pWeaponVisual->LL_GetBoneVisible(bone_id)) + const auto checkBone = [this, pWeaponVisual](const shared_str& bone, + const ALife::EWeaponAddonStatus status, const CSE_ALifeItemWeapon::EWeaponAddonState flag) { - pWeaponVisual->LL_SetBoneVisible(bone_id, FALSE, TRUE); - // Log("silencer", pWeaponVisual->LL_GetBoneVisible (bone_id)); - } - - bone_id = pWeaponVisual->LL_BoneID(wpn_grenade_launcher); - if (bone_id == BI_NONE) - bone_id = pWeaponVisual->LL_BoneID(wpn_grenade_launcher_soc); + auto bone_id = pWeaponVisual->LL_BoneID(bone); + if (bone_id == BI_NONE) + return; - if (GrenadeLauncherAttachable()) - { - if (IsGrenadeLauncherAttached()) - { - if (!pWeaponVisual->LL_GetBoneVisible(bone_id)) - pWeaponVisual->LL_SetBoneVisible(bone_id, TRUE, TRUE); - } - else + switch (status) { + case ALife::eAddonAttachable: + if (0 != (m_flagsAddOnState & flag)) + { + if (!pWeaponVisual->LL_GetBoneVisible(bone_id)) + pWeaponVisual->LL_SetBoneVisible(bone_id, TRUE, TRUE); + break; + } + [[fallthrough]]; + + case ALife::eAddonDisabled: if (pWeaponVisual->LL_GetBoneVisible(bone_id)) pWeaponVisual->LL_SetBoneVisible(bone_id, FALSE, TRUE); + break; } - } - if (m_eGrenadeLauncherStatus == ALife::eAddonDisabled && bone_id != BI_NONE && - pWeaponVisual->LL_GetBoneVisible(bone_id)) - { - pWeaponVisual->LL_SetBoneVisible(bone_id, FALSE, TRUE); - // Log("gl", pWeaponVisual->LL_GetBoneVisible (bone_id)); - } + }; + + checkBone(wpn_scope, m_eScopeStatus, CSE_ALifeItemWeapon::eWeaponAddonScope); + checkBone(wpn_silencer, m_eSilencerStatus, CSE_ALifeItemWeapon::eWeaponAddonSilencer); + checkBone(wpn_grenade_launcher, m_eGrenadeLauncherStatus, CSE_ALifeItemWeapon::eWeaponAddonGrenadeLauncher); + checkBone(wpn_grenade_launcher_soc, m_eGrenadeLauncherStatus, CSE_ALifeItemWeapon::eWeaponAddonGrenadeLauncher); pWeaponVisual->CalculateBones_Invalidate(); pWeaponVisual->CalculateBones(TRUE); From 92763aee6174cd5f42286e686a08c4716f95d599 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 25 Feb 2026 03:06:39 +0300 Subject: [PATCH 203/261] Removed r__sunshafts_intensity debug console command We have weather editor for this --- src/Layers/xrRender/xrRender_console.cpp | 13 ------------- src/xrEngine/Environment.h | 1 - src/xrEngine/Environment_misc.cpp | 13 ++----------- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/src/Layers/xrRender/xrRender_console.cpp b/src/Layers/xrRender/xrRender_console.cpp index f813cf465af..500f28d8b49 100644 --- a/src/Layers/xrRender/xrRender_console.cpp +++ b/src/Layers/xrRender/xrRender_console.cpp @@ -706,15 +706,6 @@ class CCC_Dof : public CCC_Vector3 } }; -#ifdef DEBUG -class CCC_SunshaftsIntensity : public CCC_Float -{ -public: - CCC_SunshaftsIntensity(LPCSTR N, float* V, float _min, float _max) : CCC_Float(N, V, _min, _max) {} - virtual void Save(IWriter*) { ; } -}; -#endif - // Allow real-time fog config reload #if (RENDER == R_R3) || (RENDER == R_R4) # ifndef MASTER_GOLD @@ -923,10 +914,6 @@ void xrRender_initconsole() //float ps_r2_dof_near = 0.f; // 0.f //float ps_r2_dof_focus = 1.4f; // 1.4f -#ifdef DEBUG - CMD4(CCC_SunshaftsIntensity, "r__sunshafts_intensity", &SunshaftsIntensity, 0.f, 1.f); -#endif - CMD3(CCC_Mask, "r2_volumetric_lights", &ps_r2_ls_flags, R2FLAG_VOLUMETRIC_LIGHTS); //CMD3(CCC_Mask, "r2_sun_shafts", &ps_r2_ls_flags, R2FLAG_SUN_SHAFTS); CMD3(CCC_Token, "r2_sun_shafts", &ps_r_sun_shafts, qsun_shafts_token); diff --git a/src/xrEngine/Environment.h b/src/xrEngine/Environment.h index 762d0a31daa..99b229da1bd 100644 --- a/src/xrEngine/Environment.h +++ b/src/xrEngine/Environment.h @@ -387,4 +387,3 @@ class ENGINE_API CEnvironment : public xray::editor::ide_tool ENGINE_API extern Flags32 psEnvFlags; ENGINE_API extern float psVisDistance; -ENGINE_API extern float SunshaftsIntensity; diff --git a/src/xrEngine/Environment_misc.cpp b/src/xrEngine/Environment_misc.cpp index bf103377436..a021d530abe 100644 --- a/src/xrEngine/Environment_misc.cpp +++ b/src/xrEngine/Environment_misc.cpp @@ -10,8 +10,6 @@ #include "Common/object_broker.h" #include "Common/LevelGameDef.h" -ENGINE_API float SunshaftsIntensity = 0.f; - void CEnvModifier::load(IReader* fs, u32 version) { use_flags.one(); @@ -557,19 +555,12 @@ void CEnvDescriptorMixer::lerp(CEnvironment& parent, CEnvDescriptor& A, CEnvDesc rain_color.lerp(A.rain_color, B.rain_color, f); bolt_period = fi * A.bolt_period + f * B.bolt_period; bolt_duration = fi * A.bolt_duration + f * B.bolt_duration; + // wind wind_velocity = fi * A.wind_velocity + f * B.wind_velocity; wind_direction = fi * A.wind_direction + f * B.wind_direction; -#ifdef DEBUG - if (SunshaftsIntensity > 0.f) - m_fSunShaftsIntensity = SunshaftsIntensity; - else -#endif - { - m_fSunShaftsIntensity = fi * A.m_fSunShaftsIntensity + f * B.m_fSunShaftsIntensity; - } - + m_fSunShaftsIntensity = fi * A.m_fSunShaftsIntensity + f * B.m_fSunShaftsIntensity; m_fWaterIntensity = fi * A.m_fWaterIntensity + f * B.m_fWaterIntensity; // trees From ce2cd3704c2957eb23628154a5679160b15237c0 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 25 Feb 2026 03:06:39 +0300 Subject: [PATCH 204/261] Removed r__sunshafts_intensity debug console command We have weather editor for this --- src/Layers/xrRender/xrRender_console.cpp | 13 ------------- src/xrEngine/Environment.h | 1 - src/xrEngine/Environment_misc.cpp | 13 ++----------- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/src/Layers/xrRender/xrRender_console.cpp b/src/Layers/xrRender/xrRender_console.cpp index f813cf465af..500f28d8b49 100644 --- a/src/Layers/xrRender/xrRender_console.cpp +++ b/src/Layers/xrRender/xrRender_console.cpp @@ -706,15 +706,6 @@ class CCC_Dof : public CCC_Vector3 } }; -#ifdef DEBUG -class CCC_SunshaftsIntensity : public CCC_Float -{ -public: - CCC_SunshaftsIntensity(LPCSTR N, float* V, float _min, float _max) : CCC_Float(N, V, _min, _max) {} - virtual void Save(IWriter*) { ; } -}; -#endif - // Allow real-time fog config reload #if (RENDER == R_R3) || (RENDER == R_R4) # ifndef MASTER_GOLD @@ -923,10 +914,6 @@ void xrRender_initconsole() //float ps_r2_dof_near = 0.f; // 0.f //float ps_r2_dof_focus = 1.4f; // 1.4f -#ifdef DEBUG - CMD4(CCC_SunshaftsIntensity, "r__sunshafts_intensity", &SunshaftsIntensity, 0.f, 1.f); -#endif - CMD3(CCC_Mask, "r2_volumetric_lights", &ps_r2_ls_flags, R2FLAG_VOLUMETRIC_LIGHTS); //CMD3(CCC_Mask, "r2_sun_shafts", &ps_r2_ls_flags, R2FLAG_SUN_SHAFTS); CMD3(CCC_Token, "r2_sun_shafts", &ps_r_sun_shafts, qsun_shafts_token); diff --git a/src/xrEngine/Environment.h b/src/xrEngine/Environment.h index 762d0a31daa..99b229da1bd 100644 --- a/src/xrEngine/Environment.h +++ b/src/xrEngine/Environment.h @@ -387,4 +387,3 @@ class ENGINE_API CEnvironment : public xray::editor::ide_tool ENGINE_API extern Flags32 psEnvFlags; ENGINE_API extern float psVisDistance; -ENGINE_API extern float SunshaftsIntensity; diff --git a/src/xrEngine/Environment_misc.cpp b/src/xrEngine/Environment_misc.cpp index bf103377436..a021d530abe 100644 --- a/src/xrEngine/Environment_misc.cpp +++ b/src/xrEngine/Environment_misc.cpp @@ -10,8 +10,6 @@ #include "Common/object_broker.h" #include "Common/LevelGameDef.h" -ENGINE_API float SunshaftsIntensity = 0.f; - void CEnvModifier::load(IReader* fs, u32 version) { use_flags.one(); @@ -557,19 +555,12 @@ void CEnvDescriptorMixer::lerp(CEnvironment& parent, CEnvDescriptor& A, CEnvDesc rain_color.lerp(A.rain_color, B.rain_color, f); bolt_period = fi * A.bolt_period + f * B.bolt_period; bolt_duration = fi * A.bolt_duration + f * B.bolt_duration; + // wind wind_velocity = fi * A.wind_velocity + f * B.wind_velocity; wind_direction = fi * A.wind_direction + f * B.wind_direction; -#ifdef DEBUG - if (SunshaftsIntensity > 0.f) - m_fSunShaftsIntensity = SunshaftsIntensity; - else -#endif - { - m_fSunShaftsIntensity = fi * A.m_fSunShaftsIntensity + f * B.m_fSunShaftsIntensity; - } - + m_fSunShaftsIntensity = fi * A.m_fSunShaftsIntensity + f * B.m_fSunShaftsIntensity; m_fWaterIntensity = fi * A.m_fWaterIntensity + f * B.m_fWaterIntensity; // trees From 55914248882853f2c5fe07d3bc0c0c5170a1f4fa Mon Sep 17 00:00:00 2001 From: Lev Khoroshansky Date: Sun, 1 Mar 2026 01:05:22 +0300 Subject: [PATCH 205/261] GitHub Actions: Specify `ccache`'s cache path explicitly (#2061) --- .github/workflows/cibuild.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index 0c1c392fe40..9d671267886 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -160,6 +160,12 @@ jobs: with: submodules: recursive + - name: Configure ccache + if: ${{ startsWith(matrix.platform.name, 'macOS') }} + run: | + echo "CCACHE_DIR=$HOME/.ccache" >> $GITHUB_ENV + mkdir -p "$HOME/.ccache" + - name: Restore ccache if: ${{ startsWith(matrix.platform.name, 'macOS') }} uses: actions/cache@main From 30fdbc9f8783a794ec29c0abbb56c12c57ed64ef Mon Sep 17 00:00:00 2001 From: Lev Khoroshansky Date: Sun, 1 Mar 2026 01:05:22 +0300 Subject: [PATCH 206/261] GitHub Actions: Specify `ccache`'s cache path explicitly (#2061) --- .github/workflows/cibuild.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index 0c1c392fe40..9d671267886 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -160,6 +160,12 @@ jobs: with: submodules: recursive + - name: Configure ccache + if: ${{ startsWith(matrix.platform.name, 'macOS') }} + run: | + echo "CCACHE_DIR=$HOME/.ccache" >> $GITHUB_ENV + mkdir -p "$HOME/.ccache" + - name: Restore ccache if: ${{ startsWith(matrix.platform.name, 'macOS') }} uses: actions/cache@main From 960e9c2de958fd1f30abcc733c7472be4c82164b Mon Sep 17 00:00:00 2001 From: AMS21 Date: Fri, 6 Mar 2026 18:09:40 +0100 Subject: [PATCH 207/261] fix: populating npc spawn data asserting when npc has no class set (#2065) --- src/xrServerEntities/object_factory_spawner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrServerEntities/object_factory_spawner.cpp b/src/xrServerEntities/object_factory_spawner.cpp index 42206dbb33e..9765f957b21 100644 --- a/src/xrServerEntities/object_factory_spawner.cpp +++ b/src/xrServerEntities/object_factory_spawner.cpp @@ -61,7 +61,7 @@ void CObjectFactory::init_spawn_data() cpcstr npc_random = pSettings->read_if_exists(name, "npc_random", ""); _GetItem(npc ? npc : npc_random, 0, temp); - if (!temp.empty()) + if (!temp.empty() && pSettings->line_exist(temp.c_str(), "class")) { const auto npc_clsid = pSettings->r_clsid(temp.c_str(), "class"); const auto npc_kind = pSettings->read_if_exists(temp.c_str(), "kind", nullptr); From aa77d5ec457fad2ff47c0c592589146787e583b1 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Fri, 6 Mar 2026 18:09:40 +0100 Subject: [PATCH 208/261] fix: populating npc spawn data asserting when npc has no class set (#2065) --- src/xrServerEntities/object_factory_spawner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrServerEntities/object_factory_spawner.cpp b/src/xrServerEntities/object_factory_spawner.cpp index 42206dbb33e..9765f957b21 100644 --- a/src/xrServerEntities/object_factory_spawner.cpp +++ b/src/xrServerEntities/object_factory_spawner.cpp @@ -61,7 +61,7 @@ void CObjectFactory::init_spawn_data() cpcstr npc_random = pSettings->read_if_exists(name, "npc_random", ""); _GetItem(npc ? npc : npc_random, 0, temp); - if (!temp.empty()) + if (!temp.empty() && pSettings->line_exist(temp.c_str(), "class")) { const auto npc_clsid = pSettings->r_clsid(temp.c_str(), "class"); const auto npc_kind = pSettings->read_if_exists(temp.c_str(), "kind", nullptr); From f39b8e52a01925f2585315e953d8f8e6988db8a7 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Mon, 9 Mar 2026 17:10:43 +0100 Subject: [PATCH 209/261] fix: variable name for current Git commit SHA (#2068) --- src/xrCore/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrCore/CMakeLists.txt b/src/xrCore/CMakeLists.txt index 7e1fc50a178..9a1741bee04 100644 --- a/src/xrCore/CMakeLists.txt +++ b/src/xrCore/CMakeLists.txt @@ -482,7 +482,7 @@ target_compile_definitions(xrCore GITHUB_RUN_ID=\"$ENV{GITHUB_RUN_ID}\" GITHUB_RUN_NUMBER=\"$ENV{GITHUB_RUN_NUMBER}\" GITHUB_REPOSITORY=\"$ENV{GITHUB_REPOSITORY}\" - GIT_INFO_CURRENT_COMMIT=\"${XRAY_GIT_SHA1}\" + GIT_INFO_CURRENT_COMMIT=\"${XRAY_GIT_SHA}\" GIT_INFO_CURRENT_BRANCH=\"${XRAY_GIT_BRANCH}\" OPENSSL_NO_DEPRECATED ) From 677a1e8c97278c8fe3b7f0de958cabdb212b042a Mon Sep 17 00:00:00 2001 From: AMS21 Date: Mon, 9 Mar 2026 17:10:43 +0100 Subject: [PATCH 210/261] fix: variable name for current Git commit SHA (#2068) --- src/xrCore/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrCore/CMakeLists.txt b/src/xrCore/CMakeLists.txt index 7e1fc50a178..9a1741bee04 100644 --- a/src/xrCore/CMakeLists.txt +++ b/src/xrCore/CMakeLists.txt @@ -482,7 +482,7 @@ target_compile_definitions(xrCore GITHUB_RUN_ID=\"$ENV{GITHUB_RUN_ID}\" GITHUB_RUN_NUMBER=\"$ENV{GITHUB_RUN_NUMBER}\" GITHUB_REPOSITORY=\"$ENV{GITHUB_REPOSITORY}\" - GIT_INFO_CURRENT_COMMIT=\"${XRAY_GIT_SHA1}\" + GIT_INFO_CURRENT_COMMIT=\"${XRAY_GIT_SHA}\" GIT_INFO_CURRENT_BRANCH=\"${XRAY_GIT_BRANCH}\" OPENSSL_NO_DEPRECATED ) From 27dede446fd9951f39056d11c42f2d604ad0f134 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Sun, 15 Mar 2026 21:20:34 +0100 Subject: [PATCH 211/261] fix: PDA filters `QuestNpcs` and `PrimaryObjects` being swapped (#2073, closes #2072) --- src/xrGame/ui/UIMapFilters.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xrGame/ui/UIMapFilters.cpp b/src/xrGame/ui/UIMapFilters.cpp index 603ed4977be..fd2aa08d4e9 100644 --- a/src/xrGame/ui/UIMapFilters.cpp +++ b/src/xrGame/ui/UIMapFilters.cpp @@ -15,9 +15,9 @@ bool CUIMapFilters::Init(CUIXml& xml) constexpr std::tuple filters[] = { { Treasures, "filter_treasures" }, - { QuestNpcs, "filter_primary_objects" }, + { QuestNpcs, "filter_quest_npcs" }, { SecondaryTasks, "filter_secondary_tasks" }, - { PrimaryObjects, "filter_quest_npcs" }, + { PrimaryObjects, "filter_primary_objects" }, }; for (const auto& [filter_id, filter_section] : filters) From 3da6eec2d467d464a8584939b04a5a9eee6d3104 Mon Sep 17 00:00:00 2001 From: AMS21 Date: Sun, 15 Mar 2026 21:20:34 +0100 Subject: [PATCH 212/261] fix: PDA filters `QuestNpcs` and `PrimaryObjects` being swapped (#2073, closes #2072) --- src/xrGame/ui/UIMapFilters.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xrGame/ui/UIMapFilters.cpp b/src/xrGame/ui/UIMapFilters.cpp index 603ed4977be..fd2aa08d4e9 100644 --- a/src/xrGame/ui/UIMapFilters.cpp +++ b/src/xrGame/ui/UIMapFilters.cpp @@ -15,9 +15,9 @@ bool CUIMapFilters::Init(CUIXml& xml) constexpr std::tuple filters[] = { { Treasures, "filter_treasures" }, - { QuestNpcs, "filter_primary_objects" }, + { QuestNpcs, "filter_quest_npcs" }, { SecondaryTasks, "filter_secondary_tasks" }, - { PrimaryObjects, "filter_quest_npcs" }, + { PrimaryObjects, "filter_primary_objects" }, }; for (const auto& [filter_id, filter_section] : filters) From a58a849aafb81d08aefccc85fd31366facddedec Mon Sep 17 00:00:00 2001 From: Lev Khoroshansky Date: Mon, 30 Mar 2026 14:49:39 +0300 Subject: [PATCH 213/261] Make MP configs optional in single-player (#2045) --- src/xrGame/UIGameCustom.cpp | 43 +++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/xrGame/UIGameCustom.cpp b/src/xrGame/UIGameCustom.cpp index d6ae2255db6..b1c512a2d44 100644 --- a/src/xrGame/UIGameCustom.cpp +++ b/src/xrGame/UIGameCustom.cpp @@ -431,7 +431,18 @@ void CMapListHelper::Load() { string_path cfgFileName; FS.update_path(cfgFileName, "$game_config$", "mp" DELIMITER "map_list.ltx"); + if (!FS.exist(cfgFileName)) + { + Log("~ Can't open MP map list:", cfgFileName); + return; + } + CInifile maplistCfg(cfgFileName); + if (!maplistCfg.section_exist("weather")) + { + Log("~ MP map list has no [weather] section:", cfgFileName); + return; + } // read weathers set CInifile::Sect weatherCfg = maplistCfg.r_section("weather"); m_weathers.reserve(weatherCfg.Data.size()); @@ -468,9 +479,10 @@ void CMapListHelper::Load() FS.unload_archive(arch); } levelsPath->_set_root(prevRoot.c_str()); - // XXX nitrocaster: is that really fatal? - R_ASSERT2(m_storage.size() > 0, "unable to fill map list"); - R_ASSERT2(m_weathers.size() > 0, "unable to fill weathers list"); + if (m_storage.empty()) + Log("! Unable to fill MP map list (no maps discovered)."); + if (m_weathers.empty()) + Log("! Unable to fill MP weather list (no weather presets discovered)."); } const SGameTypeMaps& CMapListHelper::GetMapListFor(const shared_str& gameType) @@ -478,7 +490,16 @@ const SGameTypeMaps& CMapListHelper::GetMapListFor(const shared_str& gameType) if (m_storage.size() == 0) Load(); // XXX nitrocaster: always use enum for game type representation - return *GetMapListInt(gameType); + if (auto* maps = GetMapListInt(gameType)) + return *maps; + + static SGameTypeMaps empty{}; + if (!empty.m_game_type_name.size()) + { + empty.m_game_type_name = "unknown"; + empty.m_game_type_id = eGameIDNoGame; + } + return empty; } SGameTypeMaps* CMapListHelper::GetMapListInt(const shared_str& gameType) @@ -496,8 +517,16 @@ const SGameTypeMaps& CMapListHelper::GetMapListFor(const EGameIDs gameId) if (m_storage.size() == 0) { Load(); - // XXX nitrocaster: is that really fatal? - R_ASSERT2(m_storage.size() > 0, "unable to fill map list"); + } + if (m_storage.empty()) + { + static SGameTypeMaps empty{}; + if (!empty.m_game_type_name.size()) + { + empty.m_game_type_name = "unknown"; + empty.m_game_type_id = eGameIDNoGame; + } + return empty; } for (SGameTypeMaps& maps : m_storage) { @@ -509,7 +538,7 @@ const SGameTypeMaps& CMapListHelper::GetMapListFor(const EGameIDs gameId) const xr_vector& CMapListHelper::GetGameWeathers() { - if (m_weathers.size() == 0) + if (m_weathers.empty()) Load(); return m_weathers; } From 5e6462bad8c40233dcf504eb5bdac29aca78b996 Mon Sep 17 00:00:00 2001 From: Lev Khoroshansky Date: Mon, 30 Mar 2026 14:49:39 +0300 Subject: [PATCH 214/261] Make MP configs optional in single-player (#2045) --- src/xrGame/UIGameCustom.cpp | 43 +++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/xrGame/UIGameCustom.cpp b/src/xrGame/UIGameCustom.cpp index d6ae2255db6..b1c512a2d44 100644 --- a/src/xrGame/UIGameCustom.cpp +++ b/src/xrGame/UIGameCustom.cpp @@ -431,7 +431,18 @@ void CMapListHelper::Load() { string_path cfgFileName; FS.update_path(cfgFileName, "$game_config$", "mp" DELIMITER "map_list.ltx"); + if (!FS.exist(cfgFileName)) + { + Log("~ Can't open MP map list:", cfgFileName); + return; + } + CInifile maplistCfg(cfgFileName); + if (!maplistCfg.section_exist("weather")) + { + Log("~ MP map list has no [weather] section:", cfgFileName); + return; + } // read weathers set CInifile::Sect weatherCfg = maplistCfg.r_section("weather"); m_weathers.reserve(weatherCfg.Data.size()); @@ -468,9 +479,10 @@ void CMapListHelper::Load() FS.unload_archive(arch); } levelsPath->_set_root(prevRoot.c_str()); - // XXX nitrocaster: is that really fatal? - R_ASSERT2(m_storage.size() > 0, "unable to fill map list"); - R_ASSERT2(m_weathers.size() > 0, "unable to fill weathers list"); + if (m_storage.empty()) + Log("! Unable to fill MP map list (no maps discovered)."); + if (m_weathers.empty()) + Log("! Unable to fill MP weather list (no weather presets discovered)."); } const SGameTypeMaps& CMapListHelper::GetMapListFor(const shared_str& gameType) @@ -478,7 +490,16 @@ const SGameTypeMaps& CMapListHelper::GetMapListFor(const shared_str& gameType) if (m_storage.size() == 0) Load(); // XXX nitrocaster: always use enum for game type representation - return *GetMapListInt(gameType); + if (auto* maps = GetMapListInt(gameType)) + return *maps; + + static SGameTypeMaps empty{}; + if (!empty.m_game_type_name.size()) + { + empty.m_game_type_name = "unknown"; + empty.m_game_type_id = eGameIDNoGame; + } + return empty; } SGameTypeMaps* CMapListHelper::GetMapListInt(const shared_str& gameType) @@ -496,8 +517,16 @@ const SGameTypeMaps& CMapListHelper::GetMapListFor(const EGameIDs gameId) if (m_storage.size() == 0) { Load(); - // XXX nitrocaster: is that really fatal? - R_ASSERT2(m_storage.size() > 0, "unable to fill map list"); + } + if (m_storage.empty()) + { + static SGameTypeMaps empty{}; + if (!empty.m_game_type_name.size()) + { + empty.m_game_type_name = "unknown"; + empty.m_game_type_id = eGameIDNoGame; + } + return empty; } for (SGameTypeMaps& maps : m_storage) { @@ -509,7 +538,7 @@ const SGameTypeMaps& CMapListHelper::GetMapListFor(const EGameIDs gameId) const xr_vector& CMapListHelper::GetGameWeathers() { - if (m_weathers.size() == 0) + if (m_weathers.empty()) Load(); return m_weathers; } From 2d0599b3c9b7540b97f81379b0bc988263432a6d Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 30 Mar 2026 18:08:25 +0300 Subject: [PATCH 215/261] Don't crash on MP level start if $game_arch_mp$ is missing from fsgame.ltx (#1302) --- src/xrGame/Level_network_start_client.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/xrGame/Level_network_start_client.cpp b/src/xrGame/Level_network_start_client.cpp index c32bbd92e1b..95f80b3933d 100644 --- a/src/xrGame/Level_network_start_client.cpp +++ b/src/xrGame/Level_network_start_client.cpp @@ -66,8 +66,9 @@ bool CLevel::net_start_client2() void rescan_mp_archives() { ZoneScoped; - FS_Path* mp_archs_path = FS.get_path("$game_arch_mp$"); - FS.rescan_path(mp_archs_path->m_Path, mp_archs_path->m_Flags.is(FS_Path::flRecurse)); + FS_Path* mp_archs_path{}; + if (FS.get_path("$game_arch_mp$", &mp_archs_path)) + FS.rescan_path(mp_archs_path->m_Path, mp_archs_path->m_Flags.is(FS_Path::flRecurse)); } bool CLevel::net_start_client3() From 185e02cf41361d82fdeb479a428641b6d6802cfb Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 30 Mar 2026 18:08:25 +0300 Subject: [PATCH 216/261] Don't crash on MP level start if $game_arch_mp$ is missing from fsgame.ltx (#1302) --- src/xrGame/Level_network_start_client.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/xrGame/Level_network_start_client.cpp b/src/xrGame/Level_network_start_client.cpp index c32bbd92e1b..95f80b3933d 100644 --- a/src/xrGame/Level_network_start_client.cpp +++ b/src/xrGame/Level_network_start_client.cpp @@ -66,8 +66,9 @@ bool CLevel::net_start_client2() void rescan_mp_archives() { ZoneScoped; - FS_Path* mp_archs_path = FS.get_path("$game_arch_mp$"); - FS.rescan_path(mp_archs_path->m_Path, mp_archs_path->m_Flags.is(FS_Path::flRecurse)); + FS_Path* mp_archs_path{}; + if (FS.get_path("$game_arch_mp$", &mp_archs_path)) + FS.rescan_path(mp_archs_path->m_Path, mp_archs_path->m_Flags.is(FS_Path::flRecurse)); } bool CLevel::net_start_client3() From cb4ea8f8d09ef70adebf29759dd5a7aa3ebc58b4 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 30 Mar 2026 17:02:59 +0300 Subject: [PATCH 217/261] Don't crash if mp weathers list is missing (#1302) Also clean up CMapListHelper: use emplace_back, delete unused function --- src/xrGame/UIGameCustom.cpp | 65 +++++++++++++------------------------ src/xrGame/UIGameCustom.h | 1 - src/xrGame/ui/UIMapList.cpp | 13 ++++---- 3 files changed, 29 insertions(+), 50 deletions(-) diff --git a/src/xrGame/UIGameCustom.cpp b/src/xrGame/UIGameCustom.cpp index b1c512a2d44..fe8d5ee74b4 100644 --- a/src/xrGame/UIGameCustom.cpp +++ b/src/xrGame/UIGameCustom.cpp @@ -402,11 +402,7 @@ void CMapListHelper::LoadMapInfo(const char* cfgName, const xr_string& levelName if (!suitableLevels) { Msg("--unknown game type-%s", gameType.c_str()); - m_storage.resize(m_storage.size() + 1); - SGameTypeMaps& lastItem = m_storage.back(); - lastItem.m_game_type_name = gameType; - lastItem.m_game_type_id = ParseStringToGameType(gameType.c_str()); - suitableLevels = &m_storage.back(); + suitableLevels = &m_storage.emplace_back(gameType, ParseStringToGameType(gameType.c_str())); } MPLevelDesc levelDesc; levelDesc.map_name = shLevelName; @@ -430,29 +426,7 @@ void CMapListHelper::LoadMapInfo(const char* cfgName, const xr_string& levelName void CMapListHelper::Load() { string_path cfgFileName; - FS.update_path(cfgFileName, "$game_config$", "mp" DELIMITER "map_list.ltx"); - if (!FS.exist(cfgFileName)) - { - Log("~ Can't open MP map list:", cfgFileName); - return; - } - CInifile maplistCfg(cfgFileName); - if (!maplistCfg.section_exist("weather")) - { - Log("~ MP map list has no [weather] section:", cfgFileName); - return; - } - // read weathers set - CInifile::Sect weatherCfg = maplistCfg.r_section("weather"); - m_weathers.reserve(weatherCfg.Data.size()); - for (CInifile::Item& weatherDesc : weatherCfg.Data) - { - MPWeatherDesc gw; - gw.Name = weatherDesc.first; - gw.StartTime = weatherDesc.second; - m_weathers.push_back(gw); - } // scan for additional maps FS_FileSet levelCfgs; FS.file_list(levelCfgs, "$game_levels$", FS_ListFiles, "*level.ltx"); @@ -481,25 +455,30 @@ void CMapListHelper::Load() levelsPath->_set_root(prevRoot.c_str()); if (m_storage.empty()) Log("! Unable to fill MP map list (no maps discovered)."); - if (m_weathers.empty()) - Log("! Unable to fill MP weather list (no weather presets discovered)."); -} -const SGameTypeMaps& CMapListHelper::GetMapListFor(const shared_str& gameType) -{ - if (m_storage.size() == 0) - Load(); - // XXX nitrocaster: always use enum for game type representation - if (auto* maps = GetMapListInt(gameType)) - return *maps; + FS.update_path(cfgFileName, "$game_config$", "mp" DELIMITER "map_list.ltx"); + if (!FS.exist(cfgFileName)) + { + Log("~ Can't open MP map list:", cfgFileName); + return; + } - static SGameTypeMaps empty{}; - if (!empty.m_game_type_name.size()) + CInifile maplistCfg(cfgFileName); + if (!maplistCfg.section_exist("weather")) { - empty.m_game_type_name = "unknown"; - empty.m_game_type_id = eGameIDNoGame; + Log("~ MP map list has no [weather] section:", cfgFileName); + return; + } + // read weathers set + const CInifile::Sect& weatherCfg = maplistCfg.r_section("weather"); + m_weathers.reserve(weatherCfg.Data.size()); + for (const auto& weatherDesc : weatherCfg.Data) + { + m_weathers.emplace_back(weatherDesc.first, weatherDesc.second); } - return empty; + + if (m_weathers.empty()) + Log("! Unable to fill MP weather list (no weather presets discovered)."); } SGameTypeMaps* CMapListHelper::GetMapListInt(const shared_str& gameType) @@ -514,7 +493,7 @@ SGameTypeMaps* CMapListHelper::GetMapListInt(const shared_str& gameType) const SGameTypeMaps& CMapListHelper::GetMapListFor(const EGameIDs gameId) { - if (m_storage.size() == 0) + if (m_storage.empty()) { Load(); } diff --git a/src/xrGame/UIGameCustom.h b/src/xrGame/UIGameCustom.h index b54d4fe7ceb..0929fd1e29a 100644 --- a/src/xrGame/UIGameCustom.h +++ b/src/xrGame/UIGameCustom.h @@ -65,7 +65,6 @@ class CMapListHelper xr_vector m_weathers; public: - const SGameTypeMaps& GetMapListFor(const shared_str& gameType); const SGameTypeMaps& GetMapListFor(const EGameIDs gameId); const xr_vector& GetGameWeathers(); diff --git a/src/xrGame/ui/UIMapList.cpp b/src/xrGame/ui/UIMapList.cpp index 39b7997d94a..f6b424305df 100644 --- a/src/xrGame/ui/UIMapList.cpp +++ b/src/xrGame/ui/UIMapList.cpp @@ -204,11 +204,14 @@ const char* CUIMapList::GetCommandLine(LPCSTR player_name) m_command += m_srv_params; m_command += "/ver="; m_command += M.map_ver.c_str(); - m_command += "/estime="; - u32 id = m_pWeatherSelector->m_list_box.GetSelectedItem()->GetTAG(); + if (const auto selected = m_pWeatherSelector->m_list_box.GetSelectedItem()) + { + m_command += "/estime="; + const u32 id = selected->GetTAG(); + m_command += m_mapWeather[id].weather_time.c_str(); + } - m_command += m_mapWeather[id].weather_time.c_str(); m_command += ")"; m_command += " client(localhost/name="; @@ -289,9 +292,7 @@ void CUIMapList::AddWeather(const shared_str& WeatherType, const shared_str& Wea R_ASSERT2(m_pWeatherSelector, "m_pWeatherSelector == NULL"); m_pWeatherSelector->AddItem_(WeatherType.c_str(), 0)->SetTAG(_id); - m_mapWeather.resize(m_mapWeather.size() + 1); - m_mapWeather.back().weather_name = WeatherType; - m_mapWeather.back().weather_time = WeatherTime; + m_mapWeather.emplace_back(WeatherType, WeatherTime); } void CUIMapList::InitFromXml(CUIXml& xml_doc, const char* path) From 03158efd7d878d52ec0c5abb0ccd73c40b305e3d Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 30 Mar 2026 17:02:59 +0300 Subject: [PATCH 218/261] Don't crash if mp weathers list is missing (#1302) Also clean up CMapListHelper: use emplace_back, delete unused function --- src/xrGame/UIGameCustom.cpp | 65 +++++++++++++------------------------ src/xrGame/UIGameCustom.h | 1 - src/xrGame/ui/UIMapList.cpp | 13 ++++---- 3 files changed, 29 insertions(+), 50 deletions(-) diff --git a/src/xrGame/UIGameCustom.cpp b/src/xrGame/UIGameCustom.cpp index b1c512a2d44..fe8d5ee74b4 100644 --- a/src/xrGame/UIGameCustom.cpp +++ b/src/xrGame/UIGameCustom.cpp @@ -402,11 +402,7 @@ void CMapListHelper::LoadMapInfo(const char* cfgName, const xr_string& levelName if (!suitableLevels) { Msg("--unknown game type-%s", gameType.c_str()); - m_storage.resize(m_storage.size() + 1); - SGameTypeMaps& lastItem = m_storage.back(); - lastItem.m_game_type_name = gameType; - lastItem.m_game_type_id = ParseStringToGameType(gameType.c_str()); - suitableLevels = &m_storage.back(); + suitableLevels = &m_storage.emplace_back(gameType, ParseStringToGameType(gameType.c_str())); } MPLevelDesc levelDesc; levelDesc.map_name = shLevelName; @@ -430,29 +426,7 @@ void CMapListHelper::LoadMapInfo(const char* cfgName, const xr_string& levelName void CMapListHelper::Load() { string_path cfgFileName; - FS.update_path(cfgFileName, "$game_config$", "mp" DELIMITER "map_list.ltx"); - if (!FS.exist(cfgFileName)) - { - Log("~ Can't open MP map list:", cfgFileName); - return; - } - CInifile maplistCfg(cfgFileName); - if (!maplistCfg.section_exist("weather")) - { - Log("~ MP map list has no [weather] section:", cfgFileName); - return; - } - // read weathers set - CInifile::Sect weatherCfg = maplistCfg.r_section("weather"); - m_weathers.reserve(weatherCfg.Data.size()); - for (CInifile::Item& weatherDesc : weatherCfg.Data) - { - MPWeatherDesc gw; - gw.Name = weatherDesc.first; - gw.StartTime = weatherDesc.second; - m_weathers.push_back(gw); - } // scan for additional maps FS_FileSet levelCfgs; FS.file_list(levelCfgs, "$game_levels$", FS_ListFiles, "*level.ltx"); @@ -481,25 +455,30 @@ void CMapListHelper::Load() levelsPath->_set_root(prevRoot.c_str()); if (m_storage.empty()) Log("! Unable to fill MP map list (no maps discovered)."); - if (m_weathers.empty()) - Log("! Unable to fill MP weather list (no weather presets discovered)."); -} -const SGameTypeMaps& CMapListHelper::GetMapListFor(const shared_str& gameType) -{ - if (m_storage.size() == 0) - Load(); - // XXX nitrocaster: always use enum for game type representation - if (auto* maps = GetMapListInt(gameType)) - return *maps; + FS.update_path(cfgFileName, "$game_config$", "mp" DELIMITER "map_list.ltx"); + if (!FS.exist(cfgFileName)) + { + Log("~ Can't open MP map list:", cfgFileName); + return; + } - static SGameTypeMaps empty{}; - if (!empty.m_game_type_name.size()) + CInifile maplistCfg(cfgFileName); + if (!maplistCfg.section_exist("weather")) { - empty.m_game_type_name = "unknown"; - empty.m_game_type_id = eGameIDNoGame; + Log("~ MP map list has no [weather] section:", cfgFileName); + return; + } + // read weathers set + const CInifile::Sect& weatherCfg = maplistCfg.r_section("weather"); + m_weathers.reserve(weatherCfg.Data.size()); + for (const auto& weatherDesc : weatherCfg.Data) + { + m_weathers.emplace_back(weatherDesc.first, weatherDesc.second); } - return empty; + + if (m_weathers.empty()) + Log("! Unable to fill MP weather list (no weather presets discovered)."); } SGameTypeMaps* CMapListHelper::GetMapListInt(const shared_str& gameType) @@ -514,7 +493,7 @@ SGameTypeMaps* CMapListHelper::GetMapListInt(const shared_str& gameType) const SGameTypeMaps& CMapListHelper::GetMapListFor(const EGameIDs gameId) { - if (m_storage.size() == 0) + if (m_storage.empty()) { Load(); } diff --git a/src/xrGame/UIGameCustom.h b/src/xrGame/UIGameCustom.h index b54d4fe7ceb..0929fd1e29a 100644 --- a/src/xrGame/UIGameCustom.h +++ b/src/xrGame/UIGameCustom.h @@ -65,7 +65,6 @@ class CMapListHelper xr_vector m_weathers; public: - const SGameTypeMaps& GetMapListFor(const shared_str& gameType); const SGameTypeMaps& GetMapListFor(const EGameIDs gameId); const xr_vector& GetGameWeathers(); diff --git a/src/xrGame/ui/UIMapList.cpp b/src/xrGame/ui/UIMapList.cpp index 39b7997d94a..f6b424305df 100644 --- a/src/xrGame/ui/UIMapList.cpp +++ b/src/xrGame/ui/UIMapList.cpp @@ -204,11 +204,14 @@ const char* CUIMapList::GetCommandLine(LPCSTR player_name) m_command += m_srv_params; m_command += "/ver="; m_command += M.map_ver.c_str(); - m_command += "/estime="; - u32 id = m_pWeatherSelector->m_list_box.GetSelectedItem()->GetTAG(); + if (const auto selected = m_pWeatherSelector->m_list_box.GetSelectedItem()) + { + m_command += "/estime="; + const u32 id = selected->GetTAG(); + m_command += m_mapWeather[id].weather_time.c_str(); + } - m_command += m_mapWeather[id].weather_time.c_str(); m_command += ")"; m_command += " client(localhost/name="; @@ -289,9 +292,7 @@ void CUIMapList::AddWeather(const shared_str& WeatherType, const shared_str& Wea R_ASSERT2(m_pWeatherSelector, "m_pWeatherSelector == NULL"); m_pWeatherSelector->AddItem_(WeatherType.c_str(), 0)->SetTAG(_id); - m_mapWeather.resize(m_mapWeather.size() + 1); - m_mapWeather.back().weather_name = WeatherType; - m_mapWeather.back().weather_time = WeatherTime; + m_mapWeather.emplace_back(WeatherType, WeatherTime); } void CUIMapList::InitFromXml(CUIXml& xml_doc, const char* path) From 32fce3501d5d34e1c33090964e9c07f4ec616ce5 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 30 Mar 2026 17:06:53 +0300 Subject: [PATCH 219/261] The best suitable fix for misplaced weather buttons in MP This puts an end to a series of commits with different approaches: e1f1afe12cd9d59edda1ee3efb9fe97a6e962ac8 (first attemppt, breaks gamedata compatibility) 9fbc97bb76411b0b612601091ad0eb4849117864 (second attempt, lacks flexibility) --- src/xrGame/ui/ChangeWeatherDialog.cpp | 31 ++++++++++++--------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/xrGame/ui/ChangeWeatherDialog.cpp b/src/xrGame/ui/ChangeWeatherDialog.cpp index 34d73578cc7..7ca5b0bed33 100644 --- a/src/xrGame/ui/ChangeWeatherDialog.cpp +++ b/src/xrGame/ui/ChangeWeatherDialog.cpp @@ -87,26 +87,23 @@ void ChangeWeatherDialog::InitChangeWeather(CUIXml& xmlDoc) auto& gameWeathers = gMapListHelper.GetGameWeathers(); Initialize(gameWeathers.size()); weathers.resize(gameWeathers.size()); - - // There is no mistake. Vanilla algorithm sorted config keys by alphabet. - // We did it for vanilla bug fixes and compatibility with game data. - string256 path; for (u32 i = 0; i < weathers.size(); i++) { - xr_sprintf(path, "change_weather:btn_%d", i+1); - CUIXmlInit::Init3tButton(xmlDoc, path, 0, GetButton(i).Button); - xr_sprintf(path, "change_weather:txt_%d", i+1); - CUIXmlInit::InitStatic(xmlDoc, path, 0, GetButton(i).Text); + string256 temp; + xr_sprintf(temp, "change_weather:btn_%d", i + 1); + CUIXmlInit::Init3tButton(xmlDoc, temp, 0, GetButton(i).Button); + xr_sprintf(temp, "change_weather:txt_%d", i + 1); + CUIXmlInit::InitStatic(xmlDoc, temp, 0, GetButton(i).Text); + + // Because XML loader preserves the original order and CInifile doesn't, + // the order of elements in xmlDoc may differ from the one in gameWeathers, + // which results in a mess up. + // Override the text, to define only one source of truth. + xr_sprintf(temp, "%u. %s", i + 1, StringTable().translate(gameWeathers[i].Name).c_str()); + GetButton(i).Text->SetText(temp); + weathers[i].Name = gameWeathers[i].Name; + weathers[i].Time = gameWeathers[i].StartTime; } - Log("! Weathers name sorted by alphabet !"); - weathers[0].Name = gameWeathers[0].Name; - weathers[0].Time = gameWeathers[0].StartTime; - weathers[1].Name = gameWeathers[3].Name; - weathers[1].Time = gameWeathers[3].StartTime; - weathers[2].Name = gameWeathers[2].Name; - weathers[2].Time = gameWeathers[2].StartTime; - weathers[3].Name = gameWeathers[1].Name; - weathers[3].Time = gameWeathers[1].StartTime; } void ChangeWeatherDialog::OnButtonClick(int i) From f24ffecad1304ae83068c5c2f4c79026a4b61dec Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 30 Mar 2026 17:06:53 +0300 Subject: [PATCH 220/261] The best suitable fix for misplaced weather buttons in MP This puts an end to a series of commits with different approaches: e1f1afe12cd9d59edda1ee3efb9fe97a6e962ac8 (first attemppt, breaks gamedata compatibility) 9fbc97bb76411b0b612601091ad0eb4849117864 (second attempt, lacks flexibility) --- src/xrGame/ui/ChangeWeatherDialog.cpp | 31 ++++++++++++--------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/xrGame/ui/ChangeWeatherDialog.cpp b/src/xrGame/ui/ChangeWeatherDialog.cpp index 34d73578cc7..7ca5b0bed33 100644 --- a/src/xrGame/ui/ChangeWeatherDialog.cpp +++ b/src/xrGame/ui/ChangeWeatherDialog.cpp @@ -87,26 +87,23 @@ void ChangeWeatherDialog::InitChangeWeather(CUIXml& xmlDoc) auto& gameWeathers = gMapListHelper.GetGameWeathers(); Initialize(gameWeathers.size()); weathers.resize(gameWeathers.size()); - - // There is no mistake. Vanilla algorithm sorted config keys by alphabet. - // We did it for vanilla bug fixes and compatibility with game data. - string256 path; for (u32 i = 0; i < weathers.size(); i++) { - xr_sprintf(path, "change_weather:btn_%d", i+1); - CUIXmlInit::Init3tButton(xmlDoc, path, 0, GetButton(i).Button); - xr_sprintf(path, "change_weather:txt_%d", i+1); - CUIXmlInit::InitStatic(xmlDoc, path, 0, GetButton(i).Text); + string256 temp; + xr_sprintf(temp, "change_weather:btn_%d", i + 1); + CUIXmlInit::Init3tButton(xmlDoc, temp, 0, GetButton(i).Button); + xr_sprintf(temp, "change_weather:txt_%d", i + 1); + CUIXmlInit::InitStatic(xmlDoc, temp, 0, GetButton(i).Text); + + // Because XML loader preserves the original order and CInifile doesn't, + // the order of elements in xmlDoc may differ from the one in gameWeathers, + // which results in a mess up. + // Override the text, to define only one source of truth. + xr_sprintf(temp, "%u. %s", i + 1, StringTable().translate(gameWeathers[i].Name).c_str()); + GetButton(i).Text->SetText(temp); + weathers[i].Name = gameWeathers[i].Name; + weathers[i].Time = gameWeathers[i].StartTime; } - Log("! Weathers name sorted by alphabet !"); - weathers[0].Name = gameWeathers[0].Name; - weathers[0].Time = gameWeathers[0].StartTime; - weathers[1].Name = gameWeathers[3].Name; - weathers[1].Time = gameWeathers[3].StartTime; - weathers[2].Name = gameWeathers[2].Name; - weathers[2].Time = gameWeathers[2].StartTime; - weathers[3].Name = gameWeathers[1].Name; - weathers[3].Time = gameWeathers[1].StartTime; } void ChangeWeatherDialog::OnButtonClick(int i) From 0e1ec7b916da33ae3c61bd10d3271d91e4a07fc3 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 31 Mar 2026 02:57:08 +0300 Subject: [PATCH 221/261] Simplified game graph script export from EE (#1865) Not tested, no error checking. --- src/xrAICore/Navigation/game_graph_script.cpp | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/xrAICore/Navigation/game_graph_script.cpp b/src/xrAICore/Navigation/game_graph_script.cpp index 4e75854400f..457ca260607 100644 --- a/src/xrAICore/Navigation/game_graph_script.cpp +++ b/src/xrAICore/Navigation/game_graph_script.cpp @@ -61,6 +61,27 @@ void CGameGraph::script_register(lua_State* luaState) .def("level_point", &CVertex__level_point) .def("game_point", &CVertex__game_point) .def("level_id", &CGameVertex::level_id) - .def("level_vertex_id", &CGameVertex::level_vertex_id) + .def("level_vertex_id", &CGameVertex::level_vertex_id), + + def("gg_vertex_level_id", +[](u32 vertex_id) + { + return GEnv.AISpace->game_graph().vertex(vertex_id)->level_id(); + }), + def("gg_level_id", [](_LEVEL_ID idx) + { + const GameGraph::LEVEL_MAP& levels = GEnv.AISpace->game_graph().header().levels(); + return (levels.begin() + idx)->second.id(); + }), + def("gg_levels_count", +[]() + { + return GEnv.AISpace->game_graph().header().level_count(); + }), + def("gg_distance", +[](u32 vid1, u32 vid2) + { + const auto game_graph = GEnv.AISpace->game_graph(); + const auto p1 = game_graph.vertex(vid1)->game_point(); + const auto p2 = game_graph.vertex(vid2)->game_point(); + return p1.distance_to(p2); + }) ]; } From 422cb619c616122ff24ff6aebc77a9cc34218429 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 31 Mar 2026 02:57:08 +0300 Subject: [PATCH 222/261] Simplified game graph script export from EE (#1865) Not tested, no error checking. --- src/xrAICore/Navigation/game_graph_script.cpp | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/xrAICore/Navigation/game_graph_script.cpp b/src/xrAICore/Navigation/game_graph_script.cpp index 4e75854400f..457ca260607 100644 --- a/src/xrAICore/Navigation/game_graph_script.cpp +++ b/src/xrAICore/Navigation/game_graph_script.cpp @@ -61,6 +61,27 @@ void CGameGraph::script_register(lua_State* luaState) .def("level_point", &CVertex__level_point) .def("game_point", &CVertex__game_point) .def("level_id", &CGameVertex::level_id) - .def("level_vertex_id", &CGameVertex::level_vertex_id) + .def("level_vertex_id", &CGameVertex::level_vertex_id), + + def("gg_vertex_level_id", +[](u32 vertex_id) + { + return GEnv.AISpace->game_graph().vertex(vertex_id)->level_id(); + }), + def("gg_level_id", [](_LEVEL_ID idx) + { + const GameGraph::LEVEL_MAP& levels = GEnv.AISpace->game_graph().header().levels(); + return (levels.begin() + idx)->second.id(); + }), + def("gg_levels_count", +[]() + { + return GEnv.AISpace->game_graph().header().level_count(); + }), + def("gg_distance", +[](u32 vid1, u32 vid2) + { + const auto game_graph = GEnv.AISpace->game_graph(); + const auto p1 = game_graph.vertex(vid1)->game_point(); + const auto p2 = game_graph.vertex(vid2)->game_point(); + return p1.distance_to(p2); + }) ]; } From 961bf88c3d420d973ef7dc636b59a48123bb7a1b Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 30 Mar 2026 15:01:52 +0300 Subject: [PATCH 223/261] Fixed keybinds conflicts caused by input context system --- src/xrEngine/xr_level_controller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrEngine/xr_level_controller.cpp b/src/xrEngine/xr_level_controller.cpp index e59a1154bf0..35fc9a7760b 100644 --- a/src/xrEngine/xr_level_controller.cpp +++ b/src/xrEngine/xr_level_controller.cpp @@ -766,7 +766,7 @@ bool IsGroupMatching(EKeyGroup g1, EKeyGroup g2) bool IsContextNotConflicted(EKeyContext c1, EKeyContext c2) { - return c1 != c2; + return c1 != c2 || (c1 == EKeyContext::Undefined && c2 == EKeyContext::Undefined); } bool IsContextMatching(EKeyContext c1, EKeyContext c2) From dc1309d59cff7534de6cd90ac0ca9ca09040b423 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Mon, 30 Mar 2026 15:01:52 +0300 Subject: [PATCH 224/261] Fixed keybinds conflicts caused by input context system --- src/xrEngine/xr_level_controller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrEngine/xr_level_controller.cpp b/src/xrEngine/xr_level_controller.cpp index e59a1154bf0..35fc9a7760b 100644 --- a/src/xrEngine/xr_level_controller.cpp +++ b/src/xrEngine/xr_level_controller.cpp @@ -766,7 +766,7 @@ bool IsGroupMatching(EKeyGroup g1, EKeyGroup g2) bool IsContextNotConflicted(EKeyContext c1, EKeyContext c2) { - return c1 != c2; + return c1 != c2 || (c1 == EKeyContext::Undefined && c2 == EKeyContext::Undefined); } bool IsContextMatching(EKeyContext c1, EKeyContext c2) From 28ba830b9aaad623d718eb14890d722e9bbd5005 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 31 Mar 2026 16:46:56 +0300 Subject: [PATCH 225/261] Build fix --- src/xrAICore/Navigation/game_graph_script.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/xrAICore/Navigation/game_graph_script.cpp b/src/xrAICore/Navigation/game_graph_script.cpp index 457ca260607..8504b83f851 100644 --- a/src/xrAICore/Navigation/game_graph_script.cpp +++ b/src/xrAICore/Navigation/game_graph_script.cpp @@ -40,7 +40,6 @@ void CGameGraph::script_register(lua_State* luaState) using namespace luabind; using namespace luabind::policy; - typedef CGameGraph::CGameVertex CGameVertex; module(luaState) [ class_("GameGraph__LEVEL_MAP__value_type") @@ -67,7 +66,7 @@ void CGameGraph::script_register(lua_State* luaState) { return GEnv.AISpace->game_graph().vertex(vertex_id)->level_id(); }), - def("gg_level_id", [](_LEVEL_ID idx) + def("gg_level_id", +[](_LEVEL_ID idx) { const GameGraph::LEVEL_MAP& levels = GEnv.AISpace->game_graph().header().levels(); return (levels.begin() + idx)->second.id(); From 1f365f583f5b9714cde92333622f24f115b6e8e7 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Tue, 31 Mar 2026 16:46:56 +0300 Subject: [PATCH 226/261] Build fix --- src/xrAICore/Navigation/game_graph_script.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/xrAICore/Navigation/game_graph_script.cpp b/src/xrAICore/Navigation/game_graph_script.cpp index 457ca260607..8504b83f851 100644 --- a/src/xrAICore/Navigation/game_graph_script.cpp +++ b/src/xrAICore/Navigation/game_graph_script.cpp @@ -40,7 +40,6 @@ void CGameGraph::script_register(lua_State* luaState) using namespace luabind; using namespace luabind::policy; - typedef CGameGraph::CGameVertex CGameVertex; module(luaState) [ class_("GameGraph__LEVEL_MAP__value_type") @@ -67,7 +66,7 @@ void CGameGraph::script_register(lua_State* luaState) { return GEnv.AISpace->game_graph().vertex(vertex_id)->level_id(); }), - def("gg_level_id", [](_LEVEL_ID idx) + def("gg_level_id", +[](_LEVEL_ID idx) { const GameGraph::LEVEL_MAP& levels = GEnv.AISpace->game_graph().header().levels(); return (levels.begin() + idx)->second.id(); From 4a1bc7572df571cc8733daf11681360cd600a6e5 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 1 Apr 2026 00:00:00 +0300 Subject: [PATCH 227/261] Build fix --- src/xrGame/UIGameCustom.cpp | 4 ++-- src/xrGame/ui/UIMapList.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xrGame/UIGameCustom.cpp b/src/xrGame/UIGameCustom.cpp index fe8d5ee74b4..8bcc8ff1594 100644 --- a/src/xrGame/UIGameCustom.cpp +++ b/src/xrGame/UIGameCustom.cpp @@ -402,7 +402,7 @@ void CMapListHelper::LoadMapInfo(const char* cfgName, const xr_string& levelName if (!suitableLevels) { Msg("--unknown game type-%s", gameType.c_str()); - suitableLevels = &m_storage.emplace_back(gameType, ParseStringToGameType(gameType.c_str())); + suitableLevels = &m_storage.emplace_back(SGameTypeMaps{ gameType, ParseStringToGameType(gameType.c_str()) }); } MPLevelDesc levelDesc; levelDesc.map_name = shLevelName; @@ -474,7 +474,7 @@ void CMapListHelper::Load() m_weathers.reserve(weatherCfg.Data.size()); for (const auto& weatherDesc : weatherCfg.Data) { - m_weathers.emplace_back(weatherDesc.first, weatherDesc.second); + m_weathers.emplace_back(MPWeatherDesc{ weatherDesc.first, weatherDesc.second }); } if (m_weathers.empty()) diff --git a/src/xrGame/ui/UIMapList.cpp b/src/xrGame/ui/UIMapList.cpp index f6b424305df..7622bb235ae 100644 --- a/src/xrGame/ui/UIMapList.cpp +++ b/src/xrGame/ui/UIMapList.cpp @@ -292,7 +292,7 @@ void CUIMapList::AddWeather(const shared_str& WeatherType, const shared_str& Wea R_ASSERT2(m_pWeatherSelector, "m_pWeatherSelector == NULL"); m_pWeatherSelector->AddItem_(WeatherType.c_str(), 0)->SetTAG(_id); - m_mapWeather.emplace_back(WeatherType, WeatherTime); + m_mapWeather.emplace_back(CUIMapList::Sw{ WeatherType, WeatherTime }); } void CUIMapList::InitFromXml(CUIXml& xml_doc, const char* path) From de73e5687d79dee9250b881dd9426bc5a0713143 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 1 Apr 2026 00:00:00 +0300 Subject: [PATCH 228/261] Build fix --- src/xrGame/UIGameCustom.cpp | 4 ++-- src/xrGame/ui/UIMapList.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xrGame/UIGameCustom.cpp b/src/xrGame/UIGameCustom.cpp index fe8d5ee74b4..8bcc8ff1594 100644 --- a/src/xrGame/UIGameCustom.cpp +++ b/src/xrGame/UIGameCustom.cpp @@ -402,7 +402,7 @@ void CMapListHelper::LoadMapInfo(const char* cfgName, const xr_string& levelName if (!suitableLevels) { Msg("--unknown game type-%s", gameType.c_str()); - suitableLevels = &m_storage.emplace_back(gameType, ParseStringToGameType(gameType.c_str())); + suitableLevels = &m_storage.emplace_back(SGameTypeMaps{ gameType, ParseStringToGameType(gameType.c_str()) }); } MPLevelDesc levelDesc; levelDesc.map_name = shLevelName; @@ -474,7 +474,7 @@ void CMapListHelper::Load() m_weathers.reserve(weatherCfg.Data.size()); for (const auto& weatherDesc : weatherCfg.Data) { - m_weathers.emplace_back(weatherDesc.first, weatherDesc.second); + m_weathers.emplace_back(MPWeatherDesc{ weatherDesc.first, weatherDesc.second }); } if (m_weathers.empty()) diff --git a/src/xrGame/ui/UIMapList.cpp b/src/xrGame/ui/UIMapList.cpp index f6b424305df..7622bb235ae 100644 --- a/src/xrGame/ui/UIMapList.cpp +++ b/src/xrGame/ui/UIMapList.cpp @@ -292,7 +292,7 @@ void CUIMapList::AddWeather(const shared_str& WeatherType, const shared_str& Wea R_ASSERT2(m_pWeatherSelector, "m_pWeatherSelector == NULL"); m_pWeatherSelector->AddItem_(WeatherType.c_str(), 0)->SetTAG(_id); - m_mapWeather.emplace_back(WeatherType, WeatherTime); + m_mapWeather.emplace_back(CUIMapList::Sw{ WeatherType, WeatherTime }); } void CUIMapList::InitFromXml(CUIXml& xml_doc, const char* path) From f4d05a06852f5cac9d5c745e0218cfb6a3fc2015 Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 1 Apr 2026 00:08:23 +0300 Subject: [PATCH 229/261] Simplify UIMapList weather selection Remove excessive indirection. --- src/xrGame/ui/UIMapList.cpp | 31 ++++++++++++++++--------------- src/xrGame/ui/UIMapList.h | 10 +--------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/xrGame/ui/UIMapList.cpp b/src/xrGame/ui/UIMapList.cpp index 7622bb235ae..052df378ae8 100644 --- a/src/xrGame/ui/UIMapList.cpp +++ b/src/xrGame/ui/UIMapList.cpp @@ -1,5 +1,6 @@ #include "StdAfx.h" #include "UIMapList.h" +#include "UIGameCustom.h" #include "xrUICore/ListBox/UIListBox.h" #include "xrUICore/Windows/UIFrameWindow.h" #include "xrUICore/Windows/UIFrameLineWnd.h" @@ -205,11 +206,16 @@ const char* CUIMapList::GetCommandLine(LPCSTR player_name) m_command += "/ver="; m_command += M.map_ver.c_str(); - if (const auto selected = m_pWeatherSelector->m_list_box.GetSelectedItem()) + if (m_pWeatherSelector) { - m_command += "/estime="; - const u32 id = selected->GetTAG(); - m_command += m_mapWeather[id].weather_time.c_str(); + const auto selected = m_pWeatherSelector->GetSelectedIDX(); + if (selected != u32(-1)) + { + const auto& weathers = gMapListHelper.GetGameWeathers(); + + m_command += "/estime="; + m_command += weathers[selected].StartTime.c_str(); + } } m_command += ")"; @@ -236,15 +242,18 @@ const char* CUIMapList::GetCommandLine(LPCSTR player_name) return m_command.c_str(); } -#include "UIGameCustom.h" + void CUIMapList::LoadMapList() { + R_ASSERT2_CURE(m_pWeatherSelector, "m_pWeatherSelector == NULL", { return; }); const auto& weathers = gMapListHelper.GetGameWeathers(); - u32 cnt = 0; + if (m_pWeatherSelector->GetSize() > 0) m_pWeatherSelector->ClearList(); + for (const MPWeatherDesc& weather : weathers) - AddWeather(weather.Name, weather.StartTime, cnt++); + m_pWeatherSelector->AddItem_(weather.Name.c_str(), 0); + if (weathers.size() > 0) m_pWeatherSelector->SetItemIDX(0); } @@ -287,14 +296,6 @@ void CUIMapList::SetMapPic(CUIStatic* map_pic) { m_pMapPic = map_pic; } void CUIMapList::SetMapInfo(CUIMapInfo* map_info) { m_pMapInfo = map_info; } void CUIMapList::SetServerParams(LPCSTR params) { m_srv_params = params; } -void CUIMapList::AddWeather(const shared_str& WeatherType, const shared_str& WeatherTime, u32 _id) -{ - R_ASSERT2(m_pWeatherSelector, "m_pWeatherSelector == NULL"); - m_pWeatherSelector->AddItem_(WeatherType.c_str(), 0)->SetTAG(_id); - - m_mapWeather.emplace_back(CUIMapList::Sw{ WeatherType, WeatherTime }); -} - void CUIMapList::InitFromXml(CUIXml& xml_doc, const char* path) { CUIXmlInit::InitWindow(xml_doc, path, 0, this); diff --git a/src/xrGame/ui/UIMapList.h b/src/xrGame/ui/UIMapList.h index ee7af9c2c6c..042991be393 100644 --- a/src/xrGame/ui/UIMapList.h +++ b/src/xrGame/ui/UIMapList.h @@ -52,8 +52,6 @@ class CUIMapList final : public CUIWindow void OnBtnRightClick(); void OnBtnUpClick(); void OnBtnDownClick(); - void AddWeather(const shared_str& WeatherType, const shared_str& WeatherTime, u32 _id); - void ParseWeather(char** ps, char* e); CUIListBox* m_pList1; CUIListBox* m_pList2; @@ -71,13 +69,7 @@ class CUIMapList final : public CUIWindow // CUISpinText* m_pModeSelector; CUIStatic* m_pMapPic; CUIMapInfo* m_pMapInfo; - // XXX nitrocaster: use MPWeatherDesc - struct Sw - { - shared_str weather_name; - shared_str weather_time; - }; - xr_vector m_mapWeather; + xr_string m_command; xr_string m_srv_params; }; From 37706eb68efd951c331d93c92ce9f2d97db76c7b Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 1 Apr 2026 00:08:23 +0300 Subject: [PATCH 230/261] Simplify UIMapList weather selection Remove excessive indirection. --- src/xrGame/ui/UIMapList.cpp | 31 ++++++++++++++++--------------- src/xrGame/ui/UIMapList.h | 10 +--------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/xrGame/ui/UIMapList.cpp b/src/xrGame/ui/UIMapList.cpp index 7622bb235ae..052df378ae8 100644 --- a/src/xrGame/ui/UIMapList.cpp +++ b/src/xrGame/ui/UIMapList.cpp @@ -1,5 +1,6 @@ #include "StdAfx.h" #include "UIMapList.h" +#include "UIGameCustom.h" #include "xrUICore/ListBox/UIListBox.h" #include "xrUICore/Windows/UIFrameWindow.h" #include "xrUICore/Windows/UIFrameLineWnd.h" @@ -205,11 +206,16 @@ const char* CUIMapList::GetCommandLine(LPCSTR player_name) m_command += "/ver="; m_command += M.map_ver.c_str(); - if (const auto selected = m_pWeatherSelector->m_list_box.GetSelectedItem()) + if (m_pWeatherSelector) { - m_command += "/estime="; - const u32 id = selected->GetTAG(); - m_command += m_mapWeather[id].weather_time.c_str(); + const auto selected = m_pWeatherSelector->GetSelectedIDX(); + if (selected != u32(-1)) + { + const auto& weathers = gMapListHelper.GetGameWeathers(); + + m_command += "/estime="; + m_command += weathers[selected].StartTime.c_str(); + } } m_command += ")"; @@ -236,15 +242,18 @@ const char* CUIMapList::GetCommandLine(LPCSTR player_name) return m_command.c_str(); } -#include "UIGameCustom.h" + void CUIMapList::LoadMapList() { + R_ASSERT2_CURE(m_pWeatherSelector, "m_pWeatherSelector == NULL", { return; }); const auto& weathers = gMapListHelper.GetGameWeathers(); - u32 cnt = 0; + if (m_pWeatherSelector->GetSize() > 0) m_pWeatherSelector->ClearList(); + for (const MPWeatherDesc& weather : weathers) - AddWeather(weather.Name, weather.StartTime, cnt++); + m_pWeatherSelector->AddItem_(weather.Name.c_str(), 0); + if (weathers.size() > 0) m_pWeatherSelector->SetItemIDX(0); } @@ -287,14 +296,6 @@ void CUIMapList::SetMapPic(CUIStatic* map_pic) { m_pMapPic = map_pic; } void CUIMapList::SetMapInfo(CUIMapInfo* map_info) { m_pMapInfo = map_info; } void CUIMapList::SetServerParams(LPCSTR params) { m_srv_params = params; } -void CUIMapList::AddWeather(const shared_str& WeatherType, const shared_str& WeatherTime, u32 _id) -{ - R_ASSERT2(m_pWeatherSelector, "m_pWeatherSelector == NULL"); - m_pWeatherSelector->AddItem_(WeatherType.c_str(), 0)->SetTAG(_id); - - m_mapWeather.emplace_back(CUIMapList::Sw{ WeatherType, WeatherTime }); -} - void CUIMapList::InitFromXml(CUIXml& xml_doc, const char* path) { CUIXmlInit::InitWindow(xml_doc, path, 0, this); diff --git a/src/xrGame/ui/UIMapList.h b/src/xrGame/ui/UIMapList.h index ee7af9c2c6c..042991be393 100644 --- a/src/xrGame/ui/UIMapList.h +++ b/src/xrGame/ui/UIMapList.h @@ -52,8 +52,6 @@ class CUIMapList final : public CUIWindow void OnBtnRightClick(); void OnBtnUpClick(); void OnBtnDownClick(); - void AddWeather(const shared_str& WeatherType, const shared_str& WeatherTime, u32 _id); - void ParseWeather(char** ps, char* e); CUIListBox* m_pList1; CUIListBox* m_pList2; @@ -71,13 +69,7 @@ class CUIMapList final : public CUIWindow // CUISpinText* m_pModeSelector; CUIStatic* m_pMapPic; CUIMapInfo* m_pMapInfo; - // XXX nitrocaster: use MPWeatherDesc - struct Sw - { - shared_str weather_name; - shared_str weather_time; - }; - xr_vector m_mapWeather; + xr_string m_command; xr_string m_srv_params; }; From 189794b5ff32d45ab359609af2029967327f1f4f Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 1 Apr 2026 01:03:54 +0300 Subject: [PATCH 231/261] xrGame/ui/UITalkDialogWnd.cpp: don't set mechanic/trade hints if they are missing (closes #1888) --- src/xrGame/ui/UITalkDialogWnd.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/xrGame/ui/UITalkDialogWnd.cpp b/src/xrGame/ui/UITalkDialogWnd.cpp index aa510849c2d..66e8ef1834a 100644 --- a/src/xrGame/ui/UITalkDialogWnd.cpp +++ b/src/xrGame/ui/UITalkDialogWnd.cpp @@ -305,12 +305,14 @@ void CUITalkDialogWnd::SetOsoznanieMode(bool b) UIToTradeButton.Show(!b); if (mechanic_mode) { - UIToTradeButton.m_hint_text = "ui_st_upgrade_hint"; + if (StringTable().has_translation("ui_st_upgrade_hint")) + UIToTradeButton.m_hint_text = "ui_st_upgrade_hint"; UIToTradeButton.TextItemControl()->SetTextST("ui_st_upgrade"); } else { - UIToTradeButton.m_hint_text = "ui_st_trade_hint"; + if (StringTable().has_translation("ui_st_trade_hint")) + UIToTradeButton.m_hint_text = "ui_st_trade_hint"; UIToTradeButton.TextItemControl()->SetTextST("ui_st_trade"); } } From d80333c0e113908cd3ff56d414e1ffa5856581fc Mon Sep 17 00:00:00 2001 From: Xottab-DUTY Date: Wed, 1 Apr 2026 01:03:54 +0300 Subject: [PATCH 232/261] xrGame/ui/UITalkDialogWnd.cpp: don't set mechanic/trade hints if they are missing (closes #1888) --- src/xrGame/ui/UITalkDialogWnd.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/xrGame/ui/UITalkDialogWnd.cpp b/src/xrGame/ui/UITalkDialogWnd.cpp index aa510849c2d..66e8ef1834a 100644 --- a/src/xrGame/ui/UITalkDialogWnd.cpp +++ b/src/xrGame/ui/UITalkDialogWnd.cpp @@ -305,12 +305,14 @@ void CUITalkDialogWnd::SetOsoznanieMode(bool b) UIToTradeButton.Show(!b); if (mechanic_mode) { - UIToTradeButton.m_hint_text = "ui_st_upgrade_hint"; + if (StringTable().has_translation("ui_st_upgrade_hint")) + UIToTradeButton.m_hint_text = "ui_st_upgrade_hint"; UIToTradeButton.TextItemControl()->SetTextST("ui_st_upgrade"); } else { - UIToTradeButton.m_hint_text = "ui_st_trade_hint"; + if (StringTable().has_translation("ui_st_trade_hint")) + UIToTradeButton.m_hint_text = "ui_st_trade_hint"; UIToTradeButton.TextItemControl()->SetTextST("ui_st_trade"); } } From b6f7304b1f8a6e47df7557e98981a5c4b9c75124 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 16:07:00 +0300 Subject: [PATCH 233/261] build(deps): bump Externals/gli from `7efdcf8` to `3542f88` (#2082) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> skip ci --- Externals/gli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/gli b/Externals/gli index 7efdcf87dd5..3542f883017 160000 --- a/Externals/gli +++ b/Externals/gli @@ -1 +1 @@ -Subproject commit 7efdcf87dd51cfe828b0da1068f2072542b5631a +Subproject commit 3542f8830178061e0661f5df1e89d36ba7d7b0ab From c2b7a8c25416cd73dbaf93d7cb45b15fef051ae8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 16:07:00 +0300 Subject: [PATCH 234/261] build(deps): bump Externals/gli from `7efdcf8` to `3542f88` (#2082) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> skip ci --- Externals/gli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/gli b/Externals/gli index 7efdcf87dd5..3542f883017 160000 --- a/Externals/gli +++ b/Externals/gli @@ -1 +1 @@ -Subproject commit 7efdcf87dd51cfe828b0da1068f2072542b5631a +Subproject commit 3542f8830178061e0661f5df1e89d36ba7d7b0ab From addfc2d13ca281ef70d6cf4a91717475401909f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 16:07:48 +0300 Subject: [PATCH 235/261] build(deps): bump Externals/sse2rvv from `373f788` to `f1ab916` (#2084) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2rvv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2rvv b/Externals/sse2rvv index 373f7886c9d..f1ab91659cb 160000 --- a/Externals/sse2rvv +++ b/Externals/sse2rvv @@ -1 +1 @@ -Subproject commit 373f7886c9d70237e426bd7fb7b3a021085427a2 +Subproject commit f1ab91659cb803109b0f8ff22c61be37cdcc96ce From 003ad24feefc3dc0bf626ab6a2ab5562ace3140a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 16:07:48 +0300 Subject: [PATCH 236/261] build(deps): bump Externals/sse2rvv from `373f788` to `f1ab916` (#2084) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2rvv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2rvv b/Externals/sse2rvv index 373f7886c9d..f1ab91659cb 160000 --- a/Externals/sse2rvv +++ b/Externals/sse2rvv @@ -1 +1 @@ -Subproject commit 373f7886c9d70237e426bd7fb7b3a021085427a2 +Subproject commit f1ab91659cb803109b0f8ff22c61be37cdcc96ce From d99ed153bd28eceb6f339a109a7098587d297a18 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 16:08:16 +0300 Subject: [PATCH 237/261] build(deps): bump Externals/sse2neon from `f1bc16e` to `3b70b37` (#2083) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2neon b/Externals/sse2neon index f1bc16e4b10..3b70b3727ed 160000 --- a/Externals/sse2neon +++ b/Externals/sse2neon @@ -1 +1 @@ -Subproject commit f1bc16e4b107f368a6098edd0d423803657837bd +Subproject commit 3b70b3727edc9a151c113814129258c3423a771c From 20357976dd25da150f88337c14755dbd8a523097 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 16:08:16 +0300 Subject: [PATCH 238/261] build(deps): bump Externals/sse2neon from `f1bc16e` to `3b70b37` (#2083) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Externals/sse2neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Externals/sse2neon b/Externals/sse2neon index f1bc16e4b10..3b70b3727ed 160000 --- a/Externals/sse2neon +++ b/Externals/sse2neon @@ -1 +1 @@ -Subproject commit f1bc16e4b107f368a6098edd0d423803657837bd +Subproject commit 3b70b3727edc9a151c113814129258c3423a771c From dc5b295b464e4ea6f32215d8981d00a7d62ab8aa Mon Sep 17 00:00:00 2001 From: ChugunovRoman Date: Tue, 7 Jul 2026 10:07:23 +0300 Subject: [PATCH 239/261] Build fix --- .github/workflows/cibuild.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index 9d671267886..9c23c4fbf6f 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -103,8 +103,10 @@ jobs: - { name: AppImage, os: ubuntu-latest, arch: amd64, cc: gcc, flags: "-DBUILD_SHARED_LIBS=OFF", linuxdeploy: linuxdeploy-x86_64.AppImage } - { name: AppImage, os: ubuntu-24.04-arm, arch: arm64, cc: gcc, flags: "-DBUILD_SHARED_LIBS=OFF", linuxdeploy: linuxdeploy-aarch64.AppImage } # macOS images have tighter concurrency limits; keep the matrix minimal. - - { name: macOS, os: macos-latest, arch: arm64, cc: clang, threads: 3, flags: "-DCPACK_GENERATOR=ZIP" } - - { name: macOS Intel, os: macos-15-intel, arch: x86_64, cc: clang, flags: "-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCPACK_GENERATOR=ZIP" } + # Pin the deployment target so newer runner images don't pass an unsupported + # system version (e.g. 25.4.0) down to AppleClang/LuaJIT architecture detection. + - { name: macOS, os: macos-latest, arch: arm64, cc: clang, threads: 3, flags: "-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCPACK_GENERATOR=ZIP" } + - { name: macOS Intel, os: macos-15-intel, arch: x86_64, cc: clang, flags: "-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCPACK_GENERATOR=ZIP" } configuration: [Debug, Mixed, Release, ReleaseMasterGold] @@ -226,7 +228,9 @@ jobs: fail-fast: false matrix: platform: - - { name: FreeBSD, os: freebsd, os-version: '14.3', arch: x86_64, + # FreeBSD package repositories track the latest stable release; use the + # current version so `pkg update` doesn't fail on OS version mismatch. + - { name: FreeBSD, os: freebsd, os-version: '14.4', arch: x86_64, install-cmd: "sudo pkg update && sudo pkg install -y cmake sdl2 lzo2 jpeg-turbo openal-soft libogg libtheora libvorbis" } - { name: OpenBSD, os: openbsd, os-version: '7.6', arch: x86_64, From ac22ad5fb4267dbdcd2dbd6ab5bf6a3d51879b2f Mon Sep 17 00:00:00 2001 From: ChugunovRoman Date: Tue, 7 Jul 2026 10:07:23 +0300 Subject: [PATCH 240/261] Build fix --- .github/workflows/cibuild.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index 9d671267886..9c23c4fbf6f 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -103,8 +103,10 @@ jobs: - { name: AppImage, os: ubuntu-latest, arch: amd64, cc: gcc, flags: "-DBUILD_SHARED_LIBS=OFF", linuxdeploy: linuxdeploy-x86_64.AppImage } - { name: AppImage, os: ubuntu-24.04-arm, arch: arm64, cc: gcc, flags: "-DBUILD_SHARED_LIBS=OFF", linuxdeploy: linuxdeploy-aarch64.AppImage } # macOS images have tighter concurrency limits; keep the matrix minimal. - - { name: macOS, os: macos-latest, arch: arm64, cc: clang, threads: 3, flags: "-DCPACK_GENERATOR=ZIP" } - - { name: macOS Intel, os: macos-15-intel, arch: x86_64, cc: clang, flags: "-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCPACK_GENERATOR=ZIP" } + # Pin the deployment target so newer runner images don't pass an unsupported + # system version (e.g. 25.4.0) down to AppleClang/LuaJIT architecture detection. + - { name: macOS, os: macos-latest, arch: arm64, cc: clang, threads: 3, flags: "-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCPACK_GENERATOR=ZIP" } + - { name: macOS Intel, os: macos-15-intel, arch: x86_64, cc: clang, flags: "-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCPACK_GENERATOR=ZIP" } configuration: [Debug, Mixed, Release, ReleaseMasterGold] @@ -226,7 +228,9 @@ jobs: fail-fast: false matrix: platform: - - { name: FreeBSD, os: freebsd, os-version: '14.3', arch: x86_64, + # FreeBSD package repositories track the latest stable release; use the + # current version so `pkg update` doesn't fail on OS version mismatch. + - { name: FreeBSD, os: freebsd, os-version: '14.4', arch: x86_64, install-cmd: "sudo pkg update && sudo pkg install -y cmake sdl2 lzo2 jpeg-turbo openal-soft libogg libtheora libvorbis" } - { name: OpenBSD, os: openbsd, os-version: '7.6', arch: x86_64, From a220c75033335b02bc2b4a39d74f85b47ca8db8b Mon Sep 17 00:00:00 2001 From: ChugunovRoman Date: Tue, 7 Jul 2026 10:17:10 +0300 Subject: [PATCH 241/261] Build fix --- .github/workflows/cibuild.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index 9c23c4fbf6f..53d7faaeb24 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -228,10 +228,10 @@ jobs: fail-fast: false matrix: platform: - # FreeBSD package repositories track the latest stable release; use the - # current version so `pkg update` doesn't fail on OS version mismatch. - - { name: FreeBSD, os: freebsd, os-version: '14.4', arch: x86_64, - install-cmd: "sudo pkg update && sudo pkg install -y cmake sdl2 lzo2 jpeg-turbo openal-soft libogg libtheora libvorbis" + # FreeBSD package repositories may be ahead of the VM image; tell pkg to + # ignore the OS version mismatch so packages still install. + - { name: FreeBSD, os: freebsd, os-version: '14.3', arch: x86_64, + install-cmd: "sudo env IGNORE_OSVERSION=yes pkg update && sudo env IGNORE_OSVERSION=yes pkg install -y cmake sdl2 lzo2 jpeg-turbo openal-soft libogg libtheora libvorbis" } - { name: OpenBSD, os: openbsd, os-version: '7.6', arch: x86_64, install-cmd: "sudo pkg_add cmake SDL2 lzo2 jpeg openal libogg libtheora libvorbis", From ace5916548271ede5e1f1caa1070d349049aa4dc Mon Sep 17 00:00:00 2001 From: ChugunovRoman Date: Tue, 7 Jul 2026 10:17:10 +0300 Subject: [PATCH 242/261] Build fix --- .github/workflows/cibuild.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index 9c23c4fbf6f..53d7faaeb24 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -228,10 +228,10 @@ jobs: fail-fast: false matrix: platform: - # FreeBSD package repositories track the latest stable release; use the - # current version so `pkg update` doesn't fail on OS version mismatch. - - { name: FreeBSD, os: freebsd, os-version: '14.4', arch: x86_64, - install-cmd: "sudo pkg update && sudo pkg install -y cmake sdl2 lzo2 jpeg-turbo openal-soft libogg libtheora libvorbis" + # FreeBSD package repositories may be ahead of the VM image; tell pkg to + # ignore the OS version mismatch so packages still install. + - { name: FreeBSD, os: freebsd, os-version: '14.3', arch: x86_64, + install-cmd: "sudo env IGNORE_OSVERSION=yes pkg update && sudo env IGNORE_OSVERSION=yes pkg install -y cmake sdl2 lzo2 jpeg-turbo openal-soft libogg libtheora libvorbis" } - { name: OpenBSD, os: openbsd, os-version: '7.6', arch: x86_64, install-cmd: "sudo pkg_add cmake SDL2 lzo2 jpeg openal libogg libtheora libvorbis", From 495662ed87c0d79818e909f29c898021108db3a3 Mon Sep 17 00:00:00 2001 From: Alex Nguyen Date: Wed, 8 Jul 2026 23:38:06 -0700 Subject: [PATCH 243/261] xrRender: Temporary fix for flickering skinned meshes --- Externals/slang | 2 +- src/Layers/xrRender/FBasicVisual.h | 5 +++-- src/Layers/xrRender/GPUCullingManager.cpp | 17 +++++++++++------ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Externals/slang b/Externals/slang index 05308fe91e5..a84f48e6240 160000 --- a/Externals/slang +++ b/Externals/slang @@ -1 +1 @@ -Subproject commit 05308fe91e5d77a68797c4bef5c629a22cc1c7b5 +Subproject commit a84f48e6240e66891aece3e43d5e8aa09f9f0d3a diff --git a/src/Layers/xrRender/FBasicVisual.h b/src/Layers/xrRender/FBasicVisual.h index 990c707b8e5..506fb4ed71d 100644 --- a/src/Layers/xrRender/FBasicVisual.h +++ b/src/Layers/xrRender/FBasicVisual.h @@ -85,8 +85,9 @@ class ECORE_API dxRender_Visual : public IRenderVisual u32 bindless_material_id{UINT32_MAX}; u32 bindless_material_epoch{0}; - u32 skinned_cull_index{UINT32_MAX}; - u32 skinned_cull_frame{0}; + static constexpr u32 SKINNED_CULL_STAMP_SLOTS = 7; + u32 skinned_cull_index[SKINNED_CULL_STAMP_SLOTS]{UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX}; + u32 skinned_cull_frame[SKINNED_CULL_STAMP_SLOTS]{}; virtual void Load(const char* N, IReader* data, u32 dwFlags); virtual void Release(); // Shared memory release diff --git a/src/Layers/xrRender/GPUCullingManager.cpp b/src/Layers/xrRender/GPUCullingManager.cpp index 3f35d32426e..c09a31c7b81 100644 --- a/src/Layers/xrRender/GPUCullingManager.cpp +++ b/src/Layers/xrRender/GPUCullingManager.cpp @@ -2074,14 +2074,17 @@ void GPUCullingManager::ScheduleSkinnedVisibilityReadback(nvrhi::ICommandList* c m_skinnedVisibilityBuffer, 0, m_skinnedObjectCount * sizeof(u32)); + static_assert(dxRender_Visual::SKINNED_CULL_STAMP_SLOTS > SKINNED_READBACK_FRAMES); + ++m_skinnedSubmitFrameId; m_skinnedReadbackSubmitFrame[writeSlot] = m_skinnedSubmitFrameId; m_skinnedReadbackCounts[writeSlot] = m_skinnedObjectCount; + const u32 stampSlot = m_skinnedSubmitFrameId % dxRender_Visual::SKINNED_CULL_STAMP_SLOTS; for (u32 i = 0; i < m_skinnedBatchPointers.size(); ++i) { const GeometryBatch* batch = m_skinnedBatchPointers[i]; if (batch && batch->visual) { - batch->visual->skinned_cull_index = i; - batch->visual->skinned_cull_frame = m_skinnedSubmitFrameId; + batch->visual->skinned_cull_index[stampSlot] = i; + batch->visual->skinned_cull_frame[stampSlot] = m_skinnedSubmitFrameId; } } @@ -2132,10 +2135,12 @@ void GPUCullingManager::UpdateSkinnedCullingStats(u32 rendered, u32 culled) u32 GPUCullingManager::GetSkinnedVisibilityByVisual(const dxRender_Visual* visual) const { - if (visual && m_skinnedVisibilityFrame != 0 - && visual->skinned_cull_frame >= m_skinnedVisibilityFrame - && visual->skinned_cull_index < m_skinnedVisibilityValues.size()) { - return m_skinnedVisibilityValues[visual->skinned_cull_index]; + if (visual && m_skinnedVisibilityFrame != 0) { + const u32 slot = m_skinnedVisibilityFrame % dxRender_Visual::SKINNED_CULL_STAMP_SLOTS; + if (visual->skinned_cull_frame[slot] == m_skinnedVisibilityFrame + && visual->skinned_cull_index[slot] < m_skinnedVisibilityValues.size()) { + return m_skinnedVisibilityValues[visual->skinned_cull_index[slot]]; + } } return 0; // Not found = culled (conservative) } From 91a16361d722cc78ae4ccc110bddb9d40df4b523 Mon Sep 17 00:00:00 2001 From: Alex Nguyen Date: Thu, 9 Jul 2026 00:08:30 -0700 Subject: [PATCH 244/261] xrRender: gate skinned draws with GPU-written indirect args, drop visibility readback --- res/gamedata/shaders/r5/skinned_args_gate.cs | 32 ++ src/Layers/xrRender/FBasicVisual.h | 4 - .../FrameGraphPasses/SkinningPassSetup.cpp | 99 +++--- .../FrameGraphPasses/SkinningPassSetup.h | 26 +- src/Layers/xrRender/GPUCullingManager.cpp | 286 ++++++++++-------- src/Layers/xrRender/GPUCullingManager.h | 55 +--- src/Layers/xrRender/r_FrameGraphRenderer.cpp | 27 +- 7 files changed, 259 insertions(+), 270 deletions(-) create mode 100644 res/gamedata/shaders/r5/skinned_args_gate.cs diff --git a/res/gamedata/shaders/r5/skinned_args_gate.cs b/res/gamedata/shaders/r5/skinned_args_gate.cs new file mode 100644 index 00000000000..ba895669272 --- /dev/null +++ b/res/gamedata/shaders/r5/skinned_args_gate.cs @@ -0,0 +1,32 @@ +#define SM_5_0 +#include "common.h" + +cbuffer CullParams : register(b5) +{ + float4x4 g_ViewProj; + float4x4 g_PrevViewProj; + float3 g_CameraPos; + float g_MaxDistance; + float4 g_FrustumPlanes[6]; + uint g_ObjectCount; + uint g_HiZWidth; + uint g_HiZHeight; + uint g_HiZMipLevels; + uint g_FrameId; + uint3 g_Padding; +}; + +StructuredBuffer g_Visibility : register(t0); + +RWByteAddressBuffer g_DrawArgs : register(u0); + +[numthreads(64, 1, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + uint objectIdx = dtID.x; + if (objectIdx >= g_ObjectCount) + return; + + uint visible = (g_Visibility[objectIdx] == g_FrameId) ? 1u : 0u; + g_DrawArgs.Store(objectIdx * 20 + 4, visible); +} diff --git a/src/Layers/xrRender/FBasicVisual.h b/src/Layers/xrRender/FBasicVisual.h index 506fb4ed71d..3c101ea369d 100644 --- a/src/Layers/xrRender/FBasicVisual.h +++ b/src/Layers/xrRender/FBasicVisual.h @@ -85,10 +85,6 @@ class ECORE_API dxRender_Visual : public IRenderVisual u32 bindless_material_id{UINT32_MAX}; u32 bindless_material_epoch{0}; - static constexpr u32 SKINNED_CULL_STAMP_SLOTS = 7; - u32 skinned_cull_index[SKINNED_CULL_STAMP_SLOTS]{UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX}; - u32 skinned_cull_frame[SKINNED_CULL_STAMP_SLOTS]{}; - virtual void Load(const char* N, IReader* data, u32 dwFlags); virtual void Release(); // Shared memory release virtual void Copy(dxRender_Visual* from); diff --git a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp index 2854b737694..622bdf365cb 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp @@ -413,7 +413,9 @@ static void DrawSkinnedBatch( const GeometryBatch& batch, const Fmatrix& worldMatrix, u32 skeletonBoneOffset, - decals::OverlayManager::SplatRange splatRange = {0, 0}) + decals::OverlayManager::SplatRange splatRange = {0, 0}, + nvrhi::IBuffer* indirectArgs = nullptr, + u32 indirectOffset = 0) { using namespace fg; using namespace fg::bindless; @@ -466,13 +468,18 @@ static void DrawSkinnedBatch( gfxState.indexBuffer = { batch.indexBuffer, nvrhi::Format::R16_UINT, 0 }; gfxState.viewport.addViewport(ctx.viewport); gfxState.viewport.addScissorRect(ctx.scissor); + gfxState.indirectParams = indirectArgs; cmdList->setGraphicsState(gfxState); - cmdList->drawIndexed( - nvrhi::DrawArguments() - .setVertexCount(batch.indexCount) - .setStartIndexLocation(batch.startIndex) - .setStartVertexLocation(batch.baseVertex)); + if (indirectArgs) { + cmdList->drawIndexedIndirect(indirectOffset, 1); + } else { + cmdList->drawIndexed( + nvrhi::DrawArguments() + .setVertexCount(batch.indexCount) + .setStartIndexLocation(batch.startIndex) + .setStartVertexLocation(batch.baseVertex)); + } } } @@ -489,7 +496,8 @@ framegraph::DefaultOutputLayout setupSkinningPass( MaterialCache* materialCache, u32 width, u32 height, - const SkinnedVisibilityData& visibilityData, + fg::GPUCullingManager* gpuCulling, + framegraph::VirtualResourceHandle skinnedDrawArgs, SkinningPassState* state, decals::OverlayManager* overlayMgr) { @@ -511,7 +519,7 @@ framegraph::DefaultOutputLayout setupSkinningPass( // ═══════════════════════════════════════════════════════ // SETUP LAMBDA // ═══════════════════════════════════════════════════════ - [&, width, height, visibilityData, state, overlayMgr](FrameGraph& builder, PassHandle passHandle, SkinningPassData& data) { + [&, width, height, gpuCulling, skinnedDrawArgs, state, overlayMgr](FrameGraph& builder, PassHandle passHandle, SkinningPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.width = width; @@ -520,10 +528,13 @@ framegraph::DefaultOutputLayout setupSkinningPass( data.geometry = geometry; data.hudBatches = hudBatches; data.materialCache = materialCache; - data.visibilityData = visibilityData; + data.gpuCulling = gpuCulling; data.passState = state; data.overlayMgr = overlayMgr; + if (skinnedDrawArgs.is_valid()) + data.skinnedDrawArgs = passBuilder.read(skinnedDrawArgs, ResourceState::IndirectArgument); + data.color = passBuilder.readWrite(inputs.albedo, ResourceState::RenderTarget); data.normal = passBuilder.readWrite(inputs.normal, ResourceState::RenderTarget); if (inputs.baseColor.is_valid()) @@ -596,10 +607,7 @@ framegraph::DefaultOutputLayout setupSkinningPass( const auto& rtDesc = colorRT->getDesc(); - // Get GPUCullingManager for bone buffer (passed via visibility data userData) - GPUCullingManager* gpuCullMgr = data.visibilityData.visibilityByVisualUserData - ? static_cast(data.visibilityData.visibilityByVisualUserData) - : nullptr; + GPUCullingManager* gpuCullMgr = data.gpuCulling; if (!gpuCullMgr || !gpuCullMgr->GetGlobalBoneBuffer()) { Msg("! [SkinningPass] GPUCullingManager bone buffer not available!"); @@ -616,7 +624,7 @@ framegraph::DefaultOutputLayout setupSkinningPass( auto dynTransformsCB = cache.GetOrCreateVolatileCB("SkinningPass", "DynTransforms", sizeof(DynamicTransforms), data.device, 1024 * 8); auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); auto shaderParamsCB = cache.GetOrCreateVolatileCB("SkinningPass", "ShaderParams", sizeof(ShaderParams), data.device, 512); - auto materialIdCB = cache.GetOrCreateVolatileCB("SkinningPass", "MaterialId", sizeof(SkinnedMaterialCB), data.device, 1024); + auto materialIdCB = cache.GetOrCreateVolatileCB("SkinningPass", "MaterialId", sizeof(SkinnedMaterialCB), data.device, 1024 * 8); ShaderParams shaderParams = {}; shaderParams.m_AlphaRef = 0.5f; @@ -666,50 +674,25 @@ framegraph::DefaultOutputLayout setupSkinningPass( globalBoneBuffer, bindlessTable, bindlessLayout, splatBuffer, worldViewport, scissor, false); - const bool useGPUCulling = data.visibilityData.enabled && - data.visibilityData.visibilityByVisualCallback != nullptr; - - if (useGPUCulling) { - u32 renderedCount = 0; - u32 culledCount = 0; - - for (const auto& batch : data.geometry->GetBatches()) { - if (!batch.isSkinned) - continue; - - u32 visibilityValue = batch.visual - ? data.visibilityData.visibilityByVisualCallback( - batch.visual, data.visibilityData.visibilityByVisualUserData) - : 0; - - if (visibilityValue == 0) { - culledCount++; - continue; - } - - u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); - auto sr = GetSplatRange(batch, data.overlayMgr); - - DrawSkinnedBatch(*data.passState, cmdList, nvDevice, worldCtx, - batch, batch.worldMatrix, boneOffset, sr); - renderedCount++; - } - - if (data.visibilityData.statsCallback) { - data.visibilityData.statsCallback(renderedCount, culledCount, - data.visibilityData.statsUserData); - } - } else { - for (const auto& batch : data.geometry->GetBatches()) { - if (!batch.isSkinned) - continue; - - u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); - auto sr = GetSplatRange(batch, data.overlayMgr); - - DrawSkinnedBatch(*data.passState, cmdList, nvDevice, worldCtx, - batch, batch.worldMatrix, boneOffset, sr); - } + nvrhi::IBuffer* indirectArgs = nullptr; + if (data.skinnedDrawArgs.is_valid() + && gpuCullMgr->IsSkinnedCullingEnabled() + && gpuCullMgr->GetSkinnedObjectCount() == worldSkinnedCount) { + indirectArgs = gpuCullMgr->GetSkinnedDrawArgsBuffer(); + } + + u32 skinnedIdx = 0; + for (const auto& batch : data.geometry->GetBatches()) { + if (!batch.isSkinned) + continue; + + u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); + auto sr = GetSplatRange(batch, data.overlayMgr); + + DrawSkinnedBatch(*data.passState, cmdList, nvDevice, worldCtx, + batch, batch.worldMatrix, boneOffset, sr, + indirectArgs, skinnedIdx * (u32)sizeof(IndirectDrawArgs)); + ++skinnedIdx; } } diff --git a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h index f132f975b28..97bd5c3adc4 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h @@ -13,6 +13,7 @@ namespace xray::render { namespace fg { class dxRender_Visual; class RenderDevice; + class GPUCullingManager; } } @@ -63,38 +64,20 @@ struct SkinningPassState { void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::FramebufferInfoEx& fbInfo, SkinningPassState& state); -// Callback to update skinned culling stats -using SkinnedStatsCallback = void(*)(u32 rendered, u32 culled, void* userData); - -// Callback type for visual-based visibility lookup (handles batch reordering) -using VisibilityByVisualCallback = u32(*)(const dxRender_Visual* visual, void* userData); - -// Skinned mesh visibility data (from GPU culling) -struct SkinnedVisibilityData { - bool enabled = false; // Whether GPU culling is active - - // Visual-based visibility lookup (handles batch reordering correctly) - VisibilityByVisualCallback visibilityByVisualCallback = nullptr; - void* visibilityByVisualUserData = nullptr; - - // Stats callback (called at end of skinning pass) - SkinnedStatsCallback statsCallback = nullptr; - void* statsUserData = nullptr; -}; - struct SkinningPassData { framegraph::VirtualResourceHandle color; framegraph::VirtualResourceHandle normal; framegraph::VirtualResourceHandle baseColor; framegraph::VirtualResourceHandle worldPos; framegraph::VirtualResourceHandle depth; + framegraph::VirtualResourceHandle skinnedDrawArgs; fg::RenderDevice* device; const GeometryCollector* geometry; const xr_vector* hudBatches; MaterialCache* materialCache; + fg::GPUCullingManager* gpuCulling; u32 width, height; framegraph::DefaultOutputLayout outputs; - SkinnedVisibilityData visibilityData; SkinningPassState* passState; decals::OverlayManager* overlayMgr; }; @@ -110,7 +93,8 @@ framegraph::DefaultOutputLayout setupSkinningPass( MaterialCache* materialCache, u32 width, u32 height, - const SkinnedVisibilityData& visibilityData = {}, + fg::GPUCullingManager* gpuCulling = nullptr, + framegraph::VirtualResourceHandle skinnedDrawArgs = {}, SkinningPassState* state = nullptr, decals::OverlayManager* overlayMgr = nullptr ); diff --git a/src/Layers/xrRender/GPUCullingManager.cpp b/src/Layers/xrRender/GPUCullingManager.cpp index c09a31c7b81..7adbf193d73 100644 --- a/src/Layers/xrRender/GPUCullingManager.cpp +++ b/src/Layers/xrRender/GPUCullingManager.cpp @@ -498,28 +498,59 @@ void GPUCullingManager::CreateSkinnedCullingBuffers(fg::RenderDevice* device) } } - // Skinned visibility readback double-buffer (CPU-readable) - for (u32 i = 0; i < SKINNED_READBACK_FRAMES; ++i) { + // Indirect draw args, one slot per skinned batch (instanceCount gated by compute) + { nvrhi::BufferDesc desc; - desc.debugName = "GPUCull_SkinnedVisibilityReadback"; - desc.byteSize = m_maxSkinnedObjects * sizeof(u32); - desc.cpuAccess = nvrhi::CpuAccessMode::Read; - desc.initialState = nvrhi::ResourceStates::CopyDest; + desc.debugName = "GPUCull_SkinnedDrawArgs"; + desc.byteSize = m_maxSkinnedObjects * sizeof(IndirectDrawArgs); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; desc.keepInitialState = true; - m_skinnedReadbackBuffers[i] = nvDevice->createBuffer(desc); - if (!m_skinnedReadbackBuffers[i]) { - Msg("! [GPUCulling] Failed to create skinned visibility readback buffer %u", i); + m_skinnedDrawArgsBuffer = nvDevice->createBuffer(desc); + if (!m_skinnedDrawArgsBuffer) { + Msg("! [GPUCulling] Failed to create skinned draw args buffer"); return; } } - m_skinnedReadbackWriteIndex = 0; - m_skinnedReadbackFrameCount = 0; + // Visible counter (stats) + visible indices (debug), real targets for object_cull.cs + { + nvrhi::BufferDesc desc; + desc.debugName = "GPUCull_SkinnedVisibleCount"; + desc.byteSize = sizeof(u32); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + + m_skinnedVisibleCountBuffer = nvDevice->createBuffer(desc); + if (!m_skinnedVisibleCountBuffer) { + Msg("! [GPUCulling] Failed to create skinned visible count buffer"); + return; + } + } + { + nvrhi::BufferDesc desc; + desc.debugName = "GPUCull_SkinnedVisibleIndices"; + desc.byteSize = m_maxSkinnedObjects * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + + m_skinnedVisibleIndexBuffer = nvDevice->createBuffer(desc); + if (!m_skinnedVisibleIndexBuffer) { + Msg("! [GPUCulling] Failed to create skinned visible index buffer"); + return; + } + } // Reserve CPU-side vectors m_skinnedObjectData.reserve(m_maxSkinnedObjects); - m_skinnedBatchPointers.reserve(m_maxSkinnedObjects); + m_skinnedDrawArgsData.reserve(m_maxSkinnedObjects); // Global bone buffer for GPU-driven skinned rendering { @@ -582,26 +613,32 @@ void GPUCullingManager::EnsureSkinnedBufferCapacity(u32 count) m_skinnedVisibilityBuffer = nvDevice->createBuffer(desc); } - // Recreate readback double-buffer - for (u32 i = 0; i < SKINNED_READBACK_FRAMES; ++i) { + // Recreate indirect draw args buffer + { nvrhi::BufferDesc desc; - desc.debugName = "GPUCull_SkinnedVisibilityReadback"; - desc.byteSize = newCapacity * sizeof(u32); - desc.cpuAccess = nvrhi::CpuAccessMode::Read; - desc.initialState = nvrhi::ResourceStates::CopyDest; + desc.debugName = "GPUCull_SkinnedDrawArgs"; + desc.byteSize = newCapacity * sizeof(IndirectDrawArgs); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; desc.keepInitialState = true; - m_skinnedReadbackBuffers[i] = nvDevice->createBuffer(desc); + m_skinnedDrawArgsBuffer = nvDevice->createBuffer(desc); } - m_skinnedReadbackWriteIndex = 0; - m_skinnedReadbackFrameCount = 0; - for (u32 i = 0; i < SKINNED_READBACK_FRAMES; ++i) { - m_skinnedReadbackSubmitFrame[i] = 0; - m_skinnedReadbackCounts[i] = 0; + // Recreate visible index buffer (count buffer is capacity-independent) + { + nvrhi::BufferDesc desc; + desc.debugName = "GPUCull_SkinnedVisibleIndices"; + desc.byteSize = newCapacity * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + + m_skinnedVisibleIndexBuffer = nvDevice->createBuffer(desc); } - m_skinnedVisibilityValues.clear(); - m_skinnedVisibilityFrame = 0; m_maxSkinnedObjects = newCapacity; } @@ -1443,12 +1480,12 @@ void GPUCullingManager::ScheduleStatsReadback(nvrhi::ICommandList* cmdList) if (!nvDevice) return; - // Create readback buffer on first use (3 u32s: static, dynamic, terrain) + // Create readback buffer on first use (4 u32s: static, dynamic, terrain, skinned) nvrhi::BufferHandle& slot = m_statsReadbackBuffers[m_statsWriteSlot]; if (!slot) { nvrhi::BufferDesc desc; - desc.byteSize = sizeof(u32) * 3; + desc.byteSize = sizeof(u32) * 4; desc.debugName = "CullingStatsReadback"; desc.cpuAccess = nvrhi::CpuAccessMode::Read; desc.initialState = nvrhi::ResourceStates::CopyDest; @@ -1490,6 +1527,17 @@ void GPUCullingManager::ScheduleStatsReadback(nvrhi::ICommandList* cmdList) ); } + // Skinned visible count at offset 12 + if (m_skinnedVisibleCountBuffer) + { + cmdList->copyBuffer( + slot, sizeof(u32) * 3, + m_skinnedVisibleCountBuffer, 0, + sizeof(u32) + ); + } + m_statsSubmittedSkinned[m_statsWriteSlot] = m_skinnedObjectCount; + m_statsWriteSlot = (m_statsWriteSlot + 1) % STATS_READBACK_SLOTS; if (m_statsScheduled < STATS_READBACK_SLOTS) ++m_statsScheduled; @@ -1520,6 +1568,13 @@ void GPUCullingManager::ProcessStatsReadback() m_cullingStats.staticVisible = counts[0]; m_cullingStats.dynamicVisible = counts[1]; m_cullingStats.terrainVisible = counts[2]; + + const u32 skinnedSubmitted = m_statsSubmittedSkinned[m_statsWriteSlot]; + const u32 skinnedVisible = std::min(counts[3], skinnedSubmitted); + m_skinnedCullingStats.submitted = skinnedSubmitted; + m_skinnedCullingStats.visible = skinnedVisible; + m_skinnedCullingStats.culled = skinnedSubmitted - skinnedVisible; + nvDevice->unmapBuffer(oldest); } } @@ -2023,7 +2078,7 @@ void GPUCullingManager::UploadSkinnedObjects(fg::RenderContext* ctx, const Geome // Clear and rebuild skinned data m_skinnedObjectData.clear(); - m_skinnedBatchPointers.clear(); + m_skinnedDrawArgsData.clear(); // Collect skinned batches for (const auto& batch : batches) { @@ -2041,7 +2096,14 @@ void GPUCullingManager::UploadSkinnedObjects(fg::RenderContext* ctx, const Geome obj.pad0 = 0.0f; obj.pad1 = 0.0f; m_skinnedObjectData.push_back(obj); - m_skinnedBatchPointers.push_back(&batch); + + IndirectDrawArgs args; + args.indexCountPerInstance = batch.indexCount; + args.instanceCount = 1; + args.startIndexLocation = batch.startIndex; + args.baseVertexLocation = batch.baseVertex; + args.startInstanceLocation = 0; + m_skinnedDrawArgsData.push_back(args); } m_skinnedObjectCount = static_cast(m_skinnedObjectData.size()); @@ -2059,102 +2121,44 @@ void GPUCullingManager::UploadSkinnedObjects(fg::RenderContext* ctx, const Geome m_skinnedObjectData.data(), m_skinnedObjectCount * sizeof(GPUObjectData)); cmdList->setBufferState(m_skinnedObjectBuffer, nvrhi::ResourceStates::ShaderResource); -} - -void GPUCullingManager::ScheduleSkinnedVisibilityReadback(nvrhi::ICommandList* cmdList) -{ - if (!m_skinnedCullEnabled || m_skinnedObjectCount == 0) - return; - u32 writeSlot = m_skinnedReadbackWriteIndex; - - // Copy visibility buffer to current write slot - cmdList->copyBuffer( - m_skinnedReadbackBuffers[writeSlot], 0, - m_skinnedVisibilityBuffer, 0, - m_skinnedObjectCount * sizeof(u32)); - - static_assert(dxRender_Visual::SKINNED_CULL_STAMP_SLOTS > SKINNED_READBACK_FRAMES); - - ++m_skinnedSubmitFrameId; - m_skinnedReadbackSubmitFrame[writeSlot] = m_skinnedSubmitFrameId; - m_skinnedReadbackCounts[writeSlot] = m_skinnedObjectCount; - const u32 stampSlot = m_skinnedSubmitFrameId % dxRender_Visual::SKINNED_CULL_STAMP_SLOTS; - for (u32 i = 0; i < m_skinnedBatchPointers.size(); ++i) { - const GeometryBatch* batch = m_skinnedBatchPointers[i]; - if (batch && batch->visual) { - batch->visual->skinned_cull_index[stampSlot] = i; - batch->visual->skinned_cull_frame[stampSlot] = m_skinnedSubmitFrameId; - } - } - - m_skinnedReadbackWriteIndex = (m_skinnedReadbackWriteIndex + 1) % SKINNED_READBACK_FRAMES; - - // Track frame count (caps at SKINNED_READBACK_FRAMES) - if (m_skinnedReadbackFrameCount < SKINNED_READBACK_FRAMES) { - m_skinnedReadbackFrameCount++; - } + cmdList->writeBuffer(m_skinnedDrawArgsBuffer, + m_skinnedDrawArgsData.data(), + m_skinnedObjectCount * sizeof(IndirectDrawArgs)); } -void GPUCullingManager::ProcessSkinnedVisibilityReadback() +bool GPUCullingManager::EnsureSkinnedArgsGatePipeline(nvrhi::IDevice* nvDevice) { - // Need 2 frames of data before reading (ensures GPU finished writing) - // Frame 0: write to buffer[0] - // Frame 1: write to buffer[1], can now read buffer[0] (frame 0's data) - if (m_skinnedReadbackFrameCount < SKINNED_READBACK_FRAMES || m_skinnedObjectCount == 0) { - return; - } + if (m_skinnedArgsGatePipeline) + return true; - // Read from the buffer that was written 2 frames ago - // Current write index points to where we'll write THIS frame - // So readSlot = writeIndex means we read from where we're ABOUT to write - // That buffer was written 2 frames ago (Frame N-2 if we're at Frame N) - u32 readSlot = m_skinnedReadbackWriteIndex; + auto& cache = framegraph::GetPassResourceCache(); + auto* gateRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("skinned_args_gate", ".cs"); + if (!gateRefl) + return false; - void* mappedData = m_device->GetNVRHIDevice()->mapBuffer( - m_skinnedReadbackBuffers[readSlot], - nvrhi::CpuAccessMode::Read); - - if (mappedData) { - const u32* visibilityData = static_cast(mappedData); - - const u32 count = std::min(m_skinnedReadbackCounts[readSlot], m_maxSkinnedObjects); - m_skinnedVisibilityValues.assign(visibilityData, visibilityData + count); - m_skinnedVisibilityFrame = m_skinnedReadbackSubmitFrame[readSlot]; - - m_device->GetNVRHIDevice()->unmapBuffer(m_skinnedReadbackBuffers[readSlot]); + m_skinnedArgsGateLayout = cache.GetOrCreateBindingLayoutFromReflection( + "GPUCull_SkinnedArgsGate", *gateRefl, nvDevice); + if (!m_skinnedArgsGateLayout) { + Msg("! [GPUCulling] Failed to create skinned args gate binding layout"); + return false; } -} - -void GPUCullingManager::UpdateSkinnedCullingStats(u32 rendered, u32 culled) -{ - m_skinnedCullingStats.submitted = rendered + culled; - m_skinnedCullingStats.visible = rendered; - m_skinnedCullingStats.culled = culled; -} -u32 GPUCullingManager::GetSkinnedVisibilityByVisual(const dxRender_Visual* visual) const -{ - if (visual && m_skinnedVisibilityFrame != 0) { - const u32 slot = m_skinnedVisibilityFrame % dxRender_Visual::SKINNED_CULL_STAMP_SLOTS; - if (visual->skinned_cull_frame[slot] == m_skinnedVisibilityFrame - && visual->skinned_cull_index[slot] < m_skinnedVisibilityValues.size()) { - return m_skinnedVisibilityValues[visual->skinned_cull_index[slot]]; - } + nvrhi::ComputePipelineDesc pipeDesc; + auto gateShader = GEnv.Render->GetShaderLoader()->LoadComputeShader("skinned_args_gate"); + if (!gateShader.handle) { + Msg("! [GPUCulling] skinned_args_gate.cs not found"); + return false; } - return 0; // Not found = culled (conservative) -} + pipeDesc.CS = gateShader.handle; + pipeDesc.bindingLayouts = { m_skinnedArgsGateLayout }; -void GPUCullingManager::ClearSkinnedVisibilityData() -{ - for (u32 i = 0; i < SKINNED_READBACK_FRAMES; ++i) { - m_skinnedReadbackSubmitFrame[i] = 0; - m_skinnedReadbackCounts[i] = 0; - } - m_skinnedVisibilityValues.clear(); - m_skinnedVisibilityFrame = 0; - m_skinnedBatchPointers.clear(); - m_skinnedReadbackFrameCount = 0; // Reset to avoid reading stale data + m_skinnedArgsGatePipeline = nvDevice->createComputePipeline(pipeDesc); + if (!m_skinnedArgsGatePipeline) { + Msg("! [GPUCulling] Failed to create skinned args gate pipeline"); + return false; + } + return true; } // ═══════════════════════════════════════════════════════ @@ -2891,7 +2895,7 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( // SETUP SKINNED CULLING PASS // ═══════════════════════════════════════════════════════ -void GPUCullingManager::SetupSkinnedCullingPass( +framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( framegraph::FrameGraph& fg, framegraph::VirtualResourceHandle hizPyramid, u32 hizWidth, @@ -2903,13 +2907,14 @@ void GPUCullingManager::SetupSkinnedCullingPass( using namespace framegraph; // Early out if not enabled - if (!m_skinnedCullEnabled) { - return; + if (!m_skinnedCullEnabled || !m_skinnedDrawArgsBuffer) { + return VirtualResourceHandle{}; } struct SkinnedCullPassData { VirtualResourceHandle hizPyramid; VirtualResourceHandle visibilityBuffer; // For framegraph dependency tracking + VirtualResourceHandle drawArgsBuffer; GPUCullingManager* manager; const GeometryCollector* geometry; Fmatrix prevViewProj; @@ -2930,11 +2935,21 @@ void GPUCullingManager::SetupSkinnedCullingPass( VirtualResourceHandle visBufferHandle = fg.ImportBuffer( "skinned_visibility", m_skinnedVisibilityBuffer, visBufferDesc); + ResourceDesc argsBufferDesc; + argsBufferDesc.type = ResourceDesc::Type::Buffer; + argsBufferDesc.debugName = "GPUCull_SkinnedDrawArgs"; + argsBufferDesc.bufferSize = m_maxSkinnedObjects * sizeof(IndirectDrawArgs); + argsBufferDesc.isUAV = true; + argsBufferDesc.isTransient = false; + + VirtualResourceHandle argsBufferHandle = fg.ImportBuffer( + "skinned_draw_args", m_skinnedDrawArgsBuffer, argsBufferDesc); + auto& passData = fg.addCallbackPass( "Skinned GPU Culling", // Setup lambda - [&, hizWidth, hizHeight, hizMipLevels, geometry, prevViewProj, visBufferHandle](FrameGraph& builder, PassHandle passHandle, SkinnedCullPassData& data) { + [&, hizWidth, hizHeight, hizMipLevels, geometry, prevViewProj, visBufferHandle, argsBufferHandle](FrameGraph& builder, PassHandle passHandle, SkinnedCullPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); passBuilder.asyncCompute(); @@ -2950,6 +2965,9 @@ void GPUCullingManager::SetupSkinnedCullingPass( // Write visibility buffer (ensures pass isn't culled by framegraph) data.visibilityBuffer = passBuilder.write(visBufferHandle, ResourceState::UnorderedAccess); + + // Write draw args (skinning pass reads them as indirect params) + data.drawArgsBuffer = passBuilder.write(argsBufferHandle, ResourceState::UnorderedAccess); }, // Execute lambda @@ -2975,7 +2993,6 @@ void GPUCullingManager::SetupSkinnedCullingPass( u32 frameId = Device.dwFrame + 1u; if (frameId == 0) frameId = 1; - mgr->m_skinnedFrameId = frameId; // Get Hi-Z texture nvrhi::ITexture* hizTexture = fg.GetPhysicalTexture(data.hizPyramid); @@ -2984,8 +3001,12 @@ void GPUCullingManager::SetupSkinnedCullingPass( return; } + if (!mgr->EnsureSkinnedArgsGatePipeline(nvDevice)) + return; + // Clear visibility buffer to 0 (all culled initially) cmdList->clearBufferUInt(mgr->m_skinnedVisibilityBuffer, 0); + cmdList->clearBufferUInt(mgr->m_skinnedVisibleCountBuffer, 0); // Fill constant buffer (reuse m_cullParamsCB) CullParamsCB cb; @@ -3010,8 +3031,8 @@ void GPUCullingManager::SetupSkinnedCullingPass( bsb.ConstantBuffer("CullParams", mgr->m_device->GetNativeBuffer(mgr->m_cullParamsCB)) .BufferSRV("g_Objects", mgr->m_skinnedObjectBuffer) .Texture("g_HiZPyramid", hizTexture) - .BufferUAV("g_VisibleIndices", mgr->m_staticSet.visibleIndexBuffer) // Dummy - not used - .BufferUAV("g_VisibleCount", mgr->m_staticSet.visibleCountBuffer) // Dummy - not used + .BufferUAV("g_VisibleIndices", mgr->m_skinnedVisibleIndexBuffer) + .BufferUAV("g_VisibleCount", mgr->m_skinnedVisibleCountBuffer) .BufferUAV("g_Visibility", mgr->m_skinnedVisibilityBuffer); nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_cullLayout); @@ -3026,10 +3047,25 @@ void GPUCullingManager::SetupSkinnedCullingPass( u32 groupCount = (mgr->m_skinnedObjectCount + CULL_THREAD_GROUP_SIZE - 1) / CULL_THREAD_GROUP_SIZE; cmdList->dispatch(groupCount, 1, 1); - // Schedule visibility readback for CPU access - mgr->ScheduleSkinnedVisibilityReadback(cmdList); + // Gate indirect args: instanceCount = 1 for visible batches, 0 for culled + auto* gateRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("skinned_args_gate", ".cs"); + framegraph::BindingSetBuilder gateBsb(*gateRefl, nvDevice, "GPUCull.SkinnedArgsGate"); + gateBsb.ConstantBuffer("CullParams", mgr->m_device->GetNativeBuffer(mgr->m_cullParamsCB)) + .BufferSRV("g_Visibility", mgr->m_skinnedVisibilityBuffer) + .BufferUAV("g_DrawArgs", mgr->m_skinnedDrawArgsBuffer); + + nvrhi::BindingSetHandle gateBindingSet = nvDevice->createBindingSet(gateBsb.Build(), mgr->m_skinnedArgsGateLayout); + R_ASSERT2(gateBindingSet, "Failed to create skinned args gate binding set"); + + nvrhi::ComputeState gateState; + gateState.pipeline = mgr->m_skinnedArgsGatePipeline; + gateState.bindings = { gateBindingSet }; + cmdList->setComputeState(gateState); + cmdList->dispatch(groupCount, 1, 1); } ); + + return argsBufferHandle; } // ═══════════════════════════════════════════════════════ diff --git a/src/Layers/xrRender/GPUCullingManager.h b/src/Layers/xrRender/GPUCullingManager.h index d602abe6977..d351c2aa828 100644 --- a/src/Layers/xrRender/GPUCullingManager.h +++ b/src/Layers/xrRender/GPUCullingManager.h @@ -379,15 +379,17 @@ class GPUCullingManager { // ─────────────────────────────────────────────────────── // SKINNED MESH CULLING // ─────────────────────────────────────────────────────── - // Skinned meshes use per-draw rendering (bone matrices) and cannot - // use multi-draw compaction. Instead, we cull them and provide a - // visibility buffer that the skinning pass checks before each draw. + // Skinned meshes draw individually (per-draw bone offsets), so instead of + // multi-draw compaction each batch gets a persistent indirect-args slot; + // the gate compute zeroes instanceCount for culled batches same-frame. // Upload skinned mesh bounding spheres (call from UploadSceneObjects) void UploadSkinnedObjects(fg::RenderContext* ctx, const GeometryCollector* geometry); - // Setup skinned culling pass (uses same Hi-Z pyramid as static culling) - void SetupSkinnedCullingPass( + // Setup skinned culling pass (uses same Hi-Z pyramid as static culling). + // Returns the imported draw-args buffer handle (invalid if disabled) so the + // skinning pass can declare a read dependency on it. + framegraph::VirtualResourceHandle SetupSkinnedCullingPass( framegraph::FrameGraph& fg, framegraph::VirtualResourceHandle hizPyramid, u32 hizWidth, @@ -397,16 +399,9 @@ class GPUCullingManager { const Fmatrix& prevViewProj ); - // Get skinned visibility by visual pointer (handles batch reordering) - // Returns 0 (culled) if visual not found, non-zero (visible) otherwise - u32 GetSkinnedVisibilityByVisual(const dxRender_Visual* visual) const; - - // Check if skinned visibility data is available - bool HasSkinnedVisibilityData() const { return !m_skinnedVisibilityValues.empty(); } - u32 GetSkinnedObjectCount() const { return m_skinnedObjectCount; } bool IsSkinnedCullingEnabled() const { return m_initialized && m_skinnedCullEnabled; } - + nvrhi::IBuffer* GetSkinnedDrawArgsBuffer() const { return m_skinnedDrawArgsBuffer.Get(); } // Skinned culling stats (for profiler display) struct SkinnedCullingStats { @@ -415,13 +410,6 @@ class GPUCullingManager { u32 culled = 0; }; const SkinnedCullingStats& GetSkinnedCullingStats() const { return m_skinnedCullingStats; } - void UpdateSkinnedCullingStats(u32 rendered, u32 culled); - - // Schedule skinned visibility readback (call after skinned culling pass) - void ScheduleSkinnedVisibilityReadback(nvrhi::ICommandList* cmdList); - - // Process skinned visibility readback (call at frame start or before skinning pass) - void ProcessSkinnedVisibilityReadback(); // ─────────────────────────────────────────────────────── // SKELETON BONE BUFFER (for GPU-driven skinned rendering) @@ -439,9 +427,6 @@ class GPUCullingManager { // Get the global bone buffer for shader binding nvrhi::IBuffer* GetGlobalBoneBuffer() const { return m_globalBoneBuffer.Get(); } - // Clear skeleton visibility data (call on level unload to prevent dangling pointers) - void ClearSkinnedVisibilityData(); - // Process readback results from previous frame (call at frame start) void ProcessStatsReadback(); nvrhi::IBuffer* GetTerrainInstanceBuffer() const { return m_terrainInstanceBuffer.Get(); } @@ -628,20 +613,11 @@ class GPUCullingManager { nvrhi::BufferHandle m_skinnedObjectBuffer; // GPUObjectData for skinned batches nvrhi::BufferHandle m_skinnedVisibilityBuffer; // Frame stamp per batch (u32) - - // Double-buffer for async readback (no fence, trust GPU pipelining) - // By frame N, frame N-2's GPU work is guaranteed complete - // This gives n-2 latency (1 frame fresher than original n-3) - static constexpr u32 SKINNED_READBACK_FRAMES = 6; - nvrhi::BufferHandle m_skinnedReadbackBuffers[SKINNED_READBACK_FRAMES]; - u32 m_skinnedReadbackWriteIndex = 0; // Which buffer to write to next - u32 m_skinnedReadbackFrameCount = 0; // Frames accumulated (0, 1, or 2) - - u32 m_skinnedSubmitFrameId = 0; - u32 m_skinnedReadbackSubmitFrame[SKINNED_READBACK_FRAMES] = {}; - u32 m_skinnedReadbackCounts[SKINNED_READBACK_FRAMES] = {}; - xr_vector m_skinnedVisibilityValues; - u32 m_skinnedVisibilityFrame = 0; + nvrhi::BufferHandle m_skinnedDrawArgsBuffer; // IndirectDrawArgs per batch, instanceCount gated by compute + nvrhi::BufferHandle m_skinnedVisibleCountBuffer; // Atomic visible counter (stats) + nvrhi::BufferHandle m_skinnedVisibleIndexBuffer; // Visible batch indices (debug) + nvrhi::ComputePipelineHandle m_skinnedArgsGatePipeline; + nvrhi::BindingLayoutHandle m_skinnedArgsGateLayout; u32 m_skinnedObjectCount = 0; u32 m_maxSkinnedObjects = 0; @@ -649,8 +625,7 @@ class GPUCullingManager { // CPU-side data xr_vector m_skinnedObjectData; - xr_vector m_skinnedBatchPointers; // Batch pointers (parallel to object data) - u32 m_skinnedFrameId = 0; + xr_vector m_skinnedDrawArgsData; SkinnedCullingStats m_skinnedCullingStats; // Global bone buffer for GPU-driven skinned rendering @@ -665,6 +640,7 @@ class GPUCullingManager { void CreateSkinnedCullingBuffers(fg::RenderDevice* device); void EnsureSkinnedBufferCapacity(u32 count); + bool EnsureSkinnedArgsGatePipeline(nvrhi::IDevice* nvDevice); void UploadSkeletonBones(nvrhi::ICommandList* cmdList, CKinematics* skeleton, u32 boneOffset); // ─────────────────────────────────────────────────────── @@ -714,6 +690,7 @@ class GPUCullingManager { // ─────────────────────────────────────────────────────── static constexpr u32 STATS_READBACK_SLOTS = 6; nvrhi::BufferHandle m_statsReadbackBuffers[STATS_READBACK_SLOTS]; + u32 m_statsSubmittedSkinned[STATS_READBACK_SLOTS] = {}; CullingStats m_cullingStats; // Previous frame's stats u32 m_statsWriteSlot = 0; u32 m_statsScheduled = 0; diff --git a/src/Layers/xrRender/r_FrameGraphRenderer.cpp b/src/Layers/xrRender/r_FrameGraphRenderer.cpp index 9f9af81ba41..6daffe5b4e7 100644 --- a/src/Layers/xrRender/r_FrameGraphRenderer.cpp +++ b/src/Layers/xrRender/r_FrameGraphRenderer.cpp @@ -846,10 +846,6 @@ void FrameGraphRenderer::SetupFrame() { ZoneScopedN("Readback::CullStats"); m_gpuCullingManager->ProcessStatsReadback(); } - { - ZoneScopedN("Readback::SkinnedVisibility"); - m_gpuCullingManager->ProcessSkinnedVisibilityReadback(); - } } if (m_detailManager && m_device) { @@ -1103,6 +1099,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { // ═══════════════════════════════════════════════════════ framegraph::VirtualResourceHandle drawArgsBuffer; // Will be passed to forward pass + framegraph::VirtualResourceHandle skinnedDrawArgsBuffer; // Will be passed to skinning pass if (m_gpuCullingManager && hizOutput.pyramid.is_valid()) { m_gpuCullingManager->Initialize(m_device); @@ -1156,7 +1153,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } if (m_gpuCullingManager->IsSkinnedCullingEnabled()) { - m_gpuCullingManager->SetupSkinnedCullingPass( + skinnedDrawArgsBuffer = m_gpuCullingManager->SetupSkinnedCullingPass( *m_framegraph, m_hizPyramid, hizOutput.width, @@ -1309,23 +1306,6 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { // 2. Skinning Pass - Renders all skinned meshes (world + HUD) // World skinned: NPCs, monsters with normal depth [0.0, 1.0] // HUD skinned: First-person weapons/hands with depth [0.0, 0.1] - static auto skinnedStatsCallback = +[](u32 rendered, u32 culled, void* userData) { - static_cast(userData)->UpdateSkinnedCullingStats(rendered, culled); - }; - - static auto visibilityByVisualCallback = +[](const dxRender_Visual* visual, void* userData) -> u32 { - return static_cast(userData)->GetSkinnedVisibilityByVisual(visual); - }; - - passes::SkinnedVisibilityData skinnedVisibility; - if (m_gpuCullingManager && m_gpuCullingManager->IsSkinnedCullingEnabled()) { - skinnedVisibility.enabled = m_gpuCullingManager->HasSkinnedVisibilityData(); - skinnedVisibility.visibilityByVisualCallback = visibilityByVisualCallback; - skinnedVisibility.visibilityByVisualUserData = m_gpuCullingManager.get(); - skinnedVisibility.statsCallback = skinnedStatsCallback; - skinnedVisibility.statsUserData = m_gpuCullingManager.get(); - } - auto hudOutputs = passes::setupSkinningPass( *m_framegraph, m_device, @@ -1335,7 +1315,8 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { m_materialCache.get(), width, height, - skinnedVisibility, + m_gpuCullingManager.get(), + skinnedDrawArgsBuffer, &m_blackboard->get_or_add(), m_overlayManager.get() ); From 31185b721afc677c254c8f95691a551482034f51 Mon Sep 17 00:00:00 2001 From: Alex Nguyen Date: Thu, 9 Jul 2026 01:06:52 -0700 Subject: [PATCH 245/261] xrRender: pool skinned geometry and draw via compacted multi-draw-indirect --- .../shaders/r5/bindless_skinned_2w_mdi.vs | 55 +++ .../shaders/r5/bindless_skinned_3w_mdi.vs | 60 +++ .../shaders/r5/bindless_skinned_4w_mdi.vs | 62 +++ .../shaders/r5/bindless_skinned_hq_mdi.vs | 49 +++ .../shaders/r5/bindless_skinned_mdi.ps | Bin 0 -> 2265 bytes .../shaders/r5/bindless_skinned_mdi.vs | 50 +++ res/gamedata/shaders/r5/skinned_mdi_common.h | 111 +++++ src/Layers/xrRender/BufferUtils.h | 4 + src/Layers/xrRender/CMakeLists.txt | 2 + .../FrameGraphPasses/SkinningPassSetup.cpp | 128 +++++- .../FrameGraphPasses/SkinningPassSetup.h | 7 + src/Layers/xrRender/GPUCullingManager.cpp | 414 +++++++++++++++--- src/Layers/xrRender/GPUCullingManager.h | 51 ++- src/Layers/xrRender/Geometry/GeometryBatch.h | 4 + .../Geometry/SkinnedGeometryPools.cpp | 168 +++++++ .../xrRender/Geometry/SkinnedGeometryPools.h | 46 ++ src/Layers/xrRender/r_FrameGraphRenderer.cpp | 15 +- 17 files changed, 1141 insertions(+), 85 deletions(-) create mode 100644 res/gamedata/shaders/r5/bindless_skinned_2w_mdi.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_3w_mdi.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_4w_mdi.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_hq_mdi.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_mdi.ps create mode 100644 res/gamedata/shaders/r5/bindless_skinned_mdi.vs create mode 100644 res/gamedata/shaders/r5/skinned_mdi_common.h create mode 100644 src/Layers/xrRender/Geometry/SkinnedGeometryPools.cpp create mode 100644 src/Layers/xrRender/Geometry/SkinnedGeometryPools.h diff --git a/res/gamedata/shaders/r5/bindless_skinned_2w_mdi.vs b/res/gamedata/shaders/r5/bindless_skinned_2w_mdi.vs new file mode 100644 index 00000000000..12622611d55 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_2w_mdi.vs @@ -0,0 +1,55 @@ +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" +#include "skinned_mdi_common.h" + +struct VS_INPUT_2W +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float4 tc : TEXCOORD0; + uint drawIndex : DRAWINDEX; +}; + +VS_OUTPUT_MDI main(VS_INPUT_2W v) +{ + VS_OUTPUT_MDI o; + + SkinnedDrawRecord rec = skinned_mdi_record(v.drawIndex); + + float3 N = unpack_d3dcolor_normal(v.N.xyz); + float3 T = unpack_d3dcolor_normal(v.T.xyz); + float3 B = unpack_d3dcolor_normal(v.B.xyz); + + int id_0 = int(v.tc.z); + int id_1 = int(v.tc.w); + + float4x4 bone_0 = mdi_get_bone(rec.boneOffset, id_0); + float4x4 bone_1 = mdi_get_bone(rec.boneOffset, id_1); + + float w = v.N.w; + float4x4 bone = lerp(bone_0, bone_1, w); + + float4 skinnedPos = skinning_pos(v.P, bone); + float3 skinnedN = skinning_dir(N, bone); + float3 skinnedT = skinning_dir(T, bone); + float3 skinnedB = skinning_dir(B, bone); + + float3x3 worldRot = (float3x3)rec.world; + float3 worldPos3 = mul(rec.world, skinnedPos).xyz; + o.normal = normalize(mul(worldRot, skinnedN)); + + worldPos3 += mdi_apply_splat_deform(rec, worldPos3, o.normal); + o.worldPos = worldPos3; + o.position = mul(m_VP, float4(worldPos3, 1.0)); + o.tangent = normalize(mul(worldRot, skinnedT)); + o.bitangent = normalize(mul(worldRot, skinnedB)); + + o.materialID = skinned_mdi_material(v.drawIndex); + o.drawIndex = v.drawIndex; + o.texcoord = v.tc.xy; + + return o; +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_3w_mdi.vs b/res/gamedata/shaders/r5/bindless_skinned_3w_mdi.vs new file mode 100644 index 00000000000..41966154e60 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_3w_mdi.vs @@ -0,0 +1,60 @@ +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" +#include "skinned_mdi_common.h" + +struct VS_INPUT_3W +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float4 tc : TEXCOORD0; + uint drawIndex : DRAWINDEX; +}; + +VS_OUTPUT_MDI main(VS_INPUT_3W v) +{ + VS_OUTPUT_MDI o; + + SkinnedDrawRecord rec = skinned_mdi_record(v.drawIndex); + + float3 N = unpack_d3dcolor_normal(v.N.xyz); + float3 T = unpack_d3dcolor_normal(v.T.xyz); + float3 B = unpack_d3dcolor_normal(v.B.xyz); + + int id_0 = int(v.tc.z); + int id_1 = int(v.tc.w); + int id_2 = int(v.B.w * 255.0 + 0.3); + + float4x4 bone_0 = mdi_get_bone(rec.boneOffset, id_0); + float4x4 bone_1 = mdi_get_bone(rec.boneOffset, id_1); + float4x4 bone_2 = mdi_get_bone(rec.boneOffset, id_2); + + float w0 = v.N.w; + float w1 = v.T.w; + float w2 = 1.0 - w0 - w1; + + float4x4 bone = bone_0 * w0 + bone_1 * w1 + bone_2 * w2; + + float4 skinnedPos = skinning_pos(v.P, bone); + float3 skinnedN = skinning_dir(N, bone); + float3 skinnedT = skinning_dir(T, bone); + float3 skinnedB = skinning_dir(B, bone); + + float3x3 worldRot = (float3x3)rec.world; + float3 worldPos3 = mul(rec.world, skinnedPos).xyz; + o.normal = normalize(mul(worldRot, skinnedN)); + + worldPos3 += mdi_apply_splat_deform(rec, worldPos3, o.normal); + o.worldPos = worldPos3; + o.position = mul(m_VP, float4(worldPos3, 1.0)); + o.tangent = normalize(mul(worldRot, skinnedT)); + o.bitangent = normalize(mul(worldRot, skinnedB)); + + o.materialID = skinned_mdi_material(v.drawIndex); + o.drawIndex = v.drawIndex; + o.texcoord = v.tc.xy; + + return o; +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_4w_mdi.vs b/res/gamedata/shaders/r5/bindless_skinned_4w_mdi.vs new file mode 100644 index 00000000000..c78b7bb26b0 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_4w_mdi.vs @@ -0,0 +1,62 @@ +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" +#include "skinned_mdi_common.h" + +struct VS_INPUT_4W +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; + float4 ind : BLENDINDICES; + uint drawIndex : DRAWINDEX; +}; + +VS_OUTPUT_MDI main(VS_INPUT_4W v) +{ + VS_OUTPUT_MDI o; + + SkinnedDrawRecord rec = skinned_mdi_record(v.drawIndex); + + float3 N = unpack_d3dcolor_normal(v.N.xyz); + float3 T = unpack_d3dcolor_normal(v.T.xyz); + float3 B = unpack_d3dcolor_normal(v.B.xyz); + + float w0 = v.N.w; + float w1 = v.T.w; + float w2 = v.B.w; + float w3 = 1.0 - w0 - w1 - w2; + + int id0 = int(v.ind.x * 255.0 + 0.3); + int id1 = int(v.ind.y * 255.0 + 0.3); + int id2 = int(v.ind.z * 255.0 + 0.3); + int id3 = int(v.ind.w * 255.0 + 0.3); + + float4x4 bone = mdi_get_bone(rec.boneOffset, id0) * w0 + + mdi_get_bone(rec.boneOffset, id1) * w1 + + mdi_get_bone(rec.boneOffset, id2) * w2 + + mdi_get_bone(rec.boneOffset, id3) * w3; + + float4 skinnedPos = skinning_pos(v.P, bone); + float3 skinnedN = skinning_dir(N, bone); + float3 skinnedT = skinning_dir(T, bone); + float3 skinnedB = skinning_dir(B, bone); + + float3x3 worldRot = (float3x3)rec.world; + float3 worldPos3 = mul(rec.world, skinnedPos).xyz; + o.normal = normalize(mul(worldRot, skinnedN)); + + worldPos3 += mdi_apply_splat_deform(rec, worldPos3, o.normal); + o.worldPos = worldPos3; + o.position = mul(m_VP, float4(worldPos3, 1.0)); + o.tangent = normalize(mul(worldRot, skinnedT)); + o.bitangent = normalize(mul(worldRot, skinnedB)); + + o.materialID = skinned_mdi_material(v.drawIndex); + o.drawIndex = v.drawIndex; + o.texcoord = v.tc; + + return o; +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_hq_mdi.vs b/res/gamedata/shaders/r5/bindless_skinned_hq_mdi.vs new file mode 100644 index 00000000000..2eea1dc57a2 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_hq_mdi.vs @@ -0,0 +1,49 @@ +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" +#include "skinned_mdi_common.h" + +struct VS_INPUT_1W +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; + uint drawIndex : DRAWINDEX; +}; + +VS_OUTPUT_MDI main(VS_INPUT_1W v) +{ + VS_OUTPUT_MDI o; + + SkinnedDrawRecord rec = skinned_mdi_record(v.drawIndex); + + float3 N = unpack_d3dcolor_normal(v.N.xyz); + float3 T = unpack_d3dcolor_normal(v.T.xyz); + float3 B = unpack_d3dcolor_normal(v.B.xyz); + + int boneIdx = int(v.N.w * 255.0 + 0.3); + float4x4 bone = mdi_get_bone(rec.boneOffset, boneIdx); + + float4 skinnedPos = skinning_pos(v.P, bone); + float3 skinnedN = skinning_dir(N, bone); + float3 skinnedT = skinning_dir(T, bone); + float3 skinnedB = skinning_dir(B, bone); + + float3x3 worldRot = (float3x3)rec.world; + float3 worldPos3 = mul(rec.world, skinnedPos).xyz; + o.normal = normalize(mul(worldRot, skinnedN)); + + worldPos3 += mdi_apply_splat_deform(rec, worldPos3, o.normal); + o.worldPos = worldPos3; + o.position = mul(m_VP, float4(worldPos3, 1.0)); + o.tangent = normalize(mul(worldRot, skinnedT)); + o.bitangent = normalize(mul(worldRot, skinnedB)); + + o.materialID = skinned_mdi_material(v.drawIndex); + o.drawIndex = v.drawIndex; + o.texcoord = v.tc; + + return o; +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_mdi.ps b/res/gamedata/shaders/r5/bindless_skinned_mdi.ps new file mode 100644 index 0000000000000000000000000000000000000000..dee9e9d56033dadd34d01ea941842fab4f2f1baa GIT binary patch literal 2265 zcmah~ZEu?}5dO}uuvTdzRff_g>xWhSKug+2N|Pv|+oWki4j5|9*oYUUTh;%*Gd5u8 zTRudb@455c<9j)ZNK83FySv~lm{>-pR=$Xnn0FW3tO6FC?Gbw17$Qkulfy1ZMx$;%AwaY3$zE=+D_Ci$%sU`K`q52|8< zgekY1_eGRvRXNhn+<`<@!a3N<*B|n9p{$&aIz`u=rqaVDmb{`1lmyLT@vDRX){b>q zMKq49g6wdbF#<;F!&9d=n2q&?P3?@dJ&lo6J2Asafj*-<*AK3j?oHq>H@9x!&v*W) z&fuy3m$`(DW_IsF2cs`CS_iZ20dGkR-X-!`C11xlMVReLBp|rHZJzAST&8FR5LLnS zAF*q89VKhGwLg?FQ4|23IJyYlOy$Rl4Ox$iDskXR=^LlG%4Wy1=r4?iHXEe!o}jBL}H0~Laf(Wx_erL0g2H!wy+vl3K+ zj`5x}TfD(rm3p9uAr~h(&(D5K4TAAQUwoo5KJyvDKwxv}$~GJ{!8m9WfAq zXn@UD)zK^8j@lNwGiNs3tBzzrjR#W*_f1vXLYUG>35sCty4xfQ(#kzl6TkM;h_7Ot T1E8SBl` g_SkinnedRecords : register(t14); +StructuredBuffer g_SkinnedCompactIndices : register(t15); +StructuredBuffer g_SkinnedCompactMaterialIDs : register(t16); + +SkinnedDrawRecord skinned_mdi_record(uint drawIndex) +{ + return g_SkinnedRecords[g_SkinnedCompactIndices[drawIndex]]; +} + +uint skinned_mdi_material(uint drawIndex) +{ + return g_SkinnedCompactMaterialIDs[drawIndex]; +} + +float4x4 mdi_get_bone(uint boneOffset, int legacy_index) +{ + return g_BoneMatrices[boneOffset + (legacy_index / 3)]; +} + +float3 mdi_skin_splat_pos(uint boneOffset, PaintSplat splat) +{ + float3 result = 0; + for (uint i = 0; i < 4; i++) + { + float4x4 bone = g_BoneMatrices[boneOffset + splat.boneIdx[i]]; + result += mul(bone, float4(splat.posRadius.xyz, 1)).xyz * splat.boneWeights[i]; + } + return result; +} + +float3 mdi_apply_splat_deform(SkinnedDrawRecord rec, float3 worldPos, float3 worldNormal) +{ + float3 offset = 0; + for (uint i = 0; i < rec.splatCount; i++) + { + PaintSplat splat = g_PaintSplats[rec.splatOffset + i]; + float3 splatWorldPos = mul(rec.world, float4(mdi_skin_splat_pos(rec.boneOffset, splat), 1)).xyz; + float dist = distance(worldPos, splatWorldPos); + float r = splat.posRadius.w; + if (dist < r) + { + float fade = 1.0 - smoothstep(r * 0.5, r, dist); + offset -= worldNormal * (splat.color.a * fade * dev_param_1.x); + } + } + return offset; +} + +float3 mdi_apply_splat_color(SkinnedDrawRecord rec, float3 albedo, float3 worldPos, float2 meshUV) +{ + for (uint i = 0; i < rec.splatCount; i++) + { + PaintSplat splat = g_PaintSplats[rec.splatOffset + i]; + float3 splatWorldPos = mul(rec.world, float4(mdi_skin_splat_pos(rec.boneOffset, splat), 1)).xyz; + float dist = distance(worldPos, splatWorldPos); + float r = splat.posRadius.w; + uint mode = get_splat_mode(splat); + float reach = (mode == SPLAT_MODE_PROCEDURAL_BLOOD) ? (r * 1.8) : r; + if (dist < reach) + { + float fade = 1.0 - smoothstep(r * 0.5, reach, dist); + + if (mode == SPLAT_MODE_PROCEDURAL_BLOOD) + { + float4 proc = sample_procedural_blood(splat, meshUV); + if (proc.a <= 1e-5) + continue; + float procAlpha = saturate(fade * proc.a); + albedo = lerp(albedo, proc.rgb, procAlpha); + continue; + } + + float4 stamp = sample_decal_stamp(splat, meshUV); + if (stamp.a <= 1e-5) + continue; + + float3 splatColor = splat.color.rgb * stamp.rgb; + float splatAlpha = splat.color.a * fade * stamp.a; + albedo = lerp(albedo, splatColor, saturate(splatAlpha)); + } + } + return albedo; +} + +struct VS_OUTPUT_MDI +{ + float4 position : SV_Position; + float3 worldPos : TEXCOORD0; + float2 texcoord : TEXCOORD1; + float3 normal : TEXCOORD2; + float3 tangent : TEXCOORD3; + float3 bitangent : TEXCOORD4; + nointerpolation uint materialID : TEXCOORD5; + nointerpolation uint drawIndex : TEXCOORD6; +}; + +#endif diff --git a/src/Layers/xrRender/BufferUtils.h b/src/Layers/xrRender/BufferUtils.h index bfdf1573967..957d11586b7 100644 --- a/src/Layers/xrRender/BufferUtils.h +++ b/src/Layers/xrRender/BufferUtils.h @@ -87,6 +87,10 @@ class VertexStagingBuffer size_t GetSystemMemoryUsage() const; size_t GetVideoMemoryUsage() const; + u32 skinned_pool_format{UINT32_MAX}; + u32 skinned_pool_base_vertex{}; + u32 skinned_pool_first_index{}; + private: void Destroy(); diff --git a/src/Layers/xrRender/CMakeLists.txt b/src/Layers/xrRender/CMakeLists.txt index f2a21808d60..c257fc27e8a 100644 --- a/src/Layers/xrRender/CMakeLists.txt +++ b/src/Layers/xrRender/CMakeLists.txt @@ -148,6 +148,8 @@ target_sources(xrRender PRIVATE Geometry/GeometryBatch.h Geometry/GeometryBatch.cpp + Geometry/SkinnedGeometryPools.h + Geometry/SkinnedGeometryPools.cpp Geometry/MaterialCache.cpp UIGeometryBatch.h diff --git a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp index 622bdf365cb..8a01681ffa7 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp @@ -138,6 +138,36 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf hudVariant.pipeline = cache.GetOrCreatePipeline(cacheName, pipeDesc, fbInfo, nvDevice); }; + auto mdiPsResult = shaderLoader->LoadPixelShader("bindless_skinned_mdi", "main"); + auto mdiVsForReflection = shaderLoader->LoadVertexShader("bindless_skinned_mdi", "main"); + if (mdiPsResult.handle && mdiVsForReflection.handle) { + state.mdiPS = mdiPsResult.handle; + state.mdiLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_MDI", + *mdiVsForReflection.reflection, *mdiPsResult.reflection, nvDevice); + } + + auto initMDIVariant = [&](SkinningPipelineVariant& variant, const char* shaderName, const char* cacheName, + const nvrhi::VertexAttributeDesc* baseAttribs, u32 baseAttrCount) { + if (!state.mdiPS || !state.mdiLayout) + return; + auto vsResult = shaderLoader->LoadVertexShader(shaderName, "main"); + if (!vsResult.handle) + return; + + nvrhi::VertexAttributeDesc attribs[8]; + for (u32 i = 0; i < baseAttrCount; ++i) + attribs[i] = baseAttribs[i]; + attribs[baseAttrCount] = nvrhi::VertexAttributeDesc() + .setName("DRAWINDEX").setFormat(nvrhi::Format::R32_UINT) + .setBufferIndex(1).setOffset(0).setElementStride(4).setIsInstanced(true); + + variant.vs = vsResult.handle; + variant.inputLayout = nvDevice->createInputLayout(attribs, baseAttrCount + 1, variant.vs); + auto pipeDesc = buildPipelineDesc(variant.vs, variant.inputLayout, state.mdiPS); + pipeDesc.bindingLayouts[0] = state.mdiLayout; + variant.pipeline = cache.GetOrCreatePipeline(cacheName, pipeDesc, fbInfo, nvDevice); + }; + { constexpr u32 stride = 24; nvrhi::VertexAttributeDesc attribs[] = { @@ -148,6 +178,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG16_SNORM).setOffset(20).setElementStride(stride), }; initVariant(state.nonHQ, "bindless_skinned", "SkinningPass_nonHQ", attribs, 5); + initMDIVariant(state.mdiNonHQ, "bindless_skinned_mdi", "SkinningPass_mdi_nonHQ", attribs, 5); } { @@ -160,6 +191,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), }; initVariant(state.hq1w, "bindless_skinned_hq", "SkinningPass_hq1w", attribs, 5); + initMDIVariant(state.mdiHQ1w, "bindless_skinned_hq_mdi", "SkinningPass_mdi_hq1w", attribs, 5); } { @@ -173,6 +205,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BLENDINDICES").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(36).setElementStride(stride), }; initVariant(state.hq4w, "bindless_skinned_4w", "SkinningPass_hq4w", attribs, 6); + initMDIVariant(state.mdiHQ4w, "bindless_skinned_4w_mdi", "SkinningPass_mdi_hq4w", attribs, 6); } { @@ -185,6 +218,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), }; initVariant(state.hq2w, "bindless_skinned_2w", "SkinningPass_hq2w", attribs, 5); + initMDIVariant(state.mdiHQ2w, "bindless_skinned_2w_mdi", "SkinningPass_mdi_hq2w", attribs, 5); } { @@ -197,6 +231,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), }; initVariant(state.hq3w, "bindless_skinned_3w", "SkinningPass_hq3w", attribs, 5); + initMDIVariant(state.mdiHQ3w, "bindless_skinned_3w_mdi", "SkinningPass_mdi_hq3w", attribs, 5); } initHudVariant(state.hudNonHQ, state.nonHQ, "SkinningPass_hud_nonHQ"); @@ -292,6 +327,18 @@ static nvrhi::IInputLayout* GetSkinnedInputLayout(const SkinningPassState& state } } +static const SkinningPipelineVariant* SelectMDISkinnedVariant(const SkinningPassState& state, u32 fmt) +{ + switch (fmt) { + case VF_SKINNED_NONHQ: return &state.mdiNonHQ; + case VF_SKINNED_HQ1W: return &state.mdiHQ1w; + case VF_SKINNED_HQ2W: return &state.mdiHQ2w; + case VF_SKINNED_HQ3W: return &state.mdiHQ3w; + case VF_SKINNED_HQ4W: return &state.mdiHQ4w; + default: return nullptr; + } +} + // ═══════════════════════════════════════════════════════════════════════════ // SKELETON BONE OFFSET HELPER // ═══════════════════════════════════════════════════════════════════════════ @@ -614,9 +661,6 @@ framegraph::DefaultOutputLayout setupSkinningPass( return; } - // Begin frame - reset skeleton allocations - gpuCullMgr->BeginSkinnedFrame(); - // Get global bone buffer for shader binding nvrhi::IBuffer* globalBoneBuffer = gpuCullMgr->GetGlobalBoneBuffer(); @@ -674,25 +718,89 @@ framegraph::DefaultOutputLayout setupSkinningPass( globalBoneBuffer, bindlessTable, bindlessLayout, splatBuffer, worldViewport, scissor, false); - nvrhi::IBuffer* indirectArgs = nullptr; - if (data.skinnedDrawArgs.is_valid() + const bool cullActive = data.skinnedDrawArgs.is_valid() && gpuCullMgr->IsSkinnedCullingEnabled() - && gpuCullMgr->GetSkinnedObjectCount() == worldSkinnedCount) { - indirectArgs = gpuCullMgr->GetSkinnedDrawArgsBuffer(); + && gpuCullMgr->GetSkinnedObjectCount() == worldSkinnedCount; + const bool mdiActive = cullActive && gpuCullMgr->IsSkinnedMDIEnabled(); + + if (mdiActive) { + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* mdiVsRefl = shaderLoader->GetCachedReflection("bindless_skinned_mdi", ".vs"); + auto* mdiPsRefl = shaderLoader->GetCachedReflection("bindless_skinned_mdi", ".ps"); + nvrhi::IBuffer* drawIndexBuffer = GetOrCreateDrawIndexBuffer("SkinningPass", nvDevice); + auto& pools = gpuCullMgr->GetSkinnedPools(); + auto& matBuffer = MaterialBuffer::Instance(); + + for (u32 f = SkinnedGeometryPools::FIRST_FORMAT; f < SkinnedGeometryPools::FORMAT_COUNT; ++f) { + const auto& bucket = gpuCullMgr->GetSkinnedBucket(f); + if (bucket.count == 0) + continue; + + const SkinningPipelineVariant* variant = SelectMDISkinnedVariant(*data.passState, f); + nvrhi::IBuffer* poolVB = pools.GetVertexBuffer(f); + nvrhi::IBuffer* poolIB = pools.GetIndexBuffer(f); + if (!variant || !variant->pipeline || !poolVB || !poolIB + || !mdiVsRefl || !mdiPsRefl || !drawIndexBuffer || !data.passState->mdiLayout) { + Msg("! [SkinningPass] MDI bucket %u unavailable, %u batches dropped", f, bucket.count); + continue; + } + + framegraph::BindingSetBuilder bsb(*mdiVsRefl, *mdiPsRefl, nvDevice, "Skinning.MDI"); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.BufferSRV("g_BoneMatrices", globalBoneBuffer); + bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + bsb.BufferSRV("g_PaintSplats", splatBuffer); + bsb.BufferSRV("g_SkinnedRecords", bucket.recordsBuffer); + bsb.BufferSRV("g_SkinnedCompactIndices", bucket.compactBatchIndicesBuffer); + bsb.BufferSRV("g_SkinnedCompactMaterialIDs", bucket.compactMaterialIDBuffer); + bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); + bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); + bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); + + auto& cache = framegraph::GetPassResourceCache(); + nvrhi::BindingSetHandle mdiBindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.passState->mdiLayout, nvDevice); + if (!mdiBindingSet) + continue; + + nvrhi::GraphicsState gfxState; + gfxState.pipeline = variant->pipeline; + gfxState.framebuffer = framebuffer; + gfxState.bindings = { mdiBindingSet }; + if (bindlessTable) + gfxState.addBindingSet(bindlessTable); + gfxState.vertexBuffers = { {poolVB, 0, 0}, {drawIndexBuffer, 1, 0} }; + gfxState.indexBuffer = { poolIB, nvrhi::Format::R16_UINT, 0 }; + gfxState.viewport.addViewport(worldViewport); + gfxState.viewport.addScissorRect(scissor); + gfxState.indirectParams = bucket.compactDrawArgsBuffer; + gfxState.indirectCountBuffer = bucket.compactCountBuffer; + + cmdList->setGraphicsState(gfxState); + DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, bucket.count); + } } - u32 skinnedIdx = 0; + nvrhi::IBuffer* residualArgs = cullActive ? gpuCullMgr->GetSkinnedDrawArgsBuffer() : nullptr; + u32 residualIdx = 0; for (const auto& batch : data.geometry->GetBatches()) { if (!batch.isSkinned) continue; + if (mdiActive) { + const u32 variantIdx = MaterialBuffer::Instance().GetShaderVariant(batch.bindlessMaterialID); + const bool pooled = variantIdx == 0 + && batch.skinnedPoolFormat >= SkinnedGeometryPools::FIRST_FORMAT + && batch.skinnedPoolFormat < SkinnedGeometryPools::FORMAT_COUNT; + if (pooled) + continue; + } u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); auto sr = GetSplatRange(batch, data.overlayMgr); DrawSkinnedBatch(*data.passState, cmdList, nvDevice, worldCtx, batch, batch.worldMatrix, boneOffset, sr, - indirectArgs, skinnedIdx * (u32)sizeof(IndirectDrawArgs)); - ++skinnedIdx; + residualArgs, residualIdx * (u32)sizeof(IndirectDrawArgs)); + ++residualIdx; } } diff --git a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h index 97bd5c3adc4..1ec4287a04f 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h @@ -53,6 +53,13 @@ struct SkinningPassState { nvrhi::BindingLayoutHandle hudLayout; nvrhi::ShaderHandle ps; nvrhi::ShaderHandle hudPS; + SkinningPipelineVariant mdiNonHQ; + SkinningPipelineVariant mdiHQ1w; + SkinningPipelineVariant mdiHQ2w; + SkinningPipelineVariant mdiHQ3w; + SkinningPipelineVariant mdiHQ4w; + nvrhi::BindingLayoutHandle mdiLayout; + nvrhi::ShaderHandle mdiPS; SkinningPipelineVariant hudNonHQ; SkinningPipelineVariant hudHQ1w; SkinningPipelineVariant hudHQ2w; diff --git a/src/Layers/xrRender/GPUCullingManager.cpp b/src/Layers/xrRender/GPUCullingManager.cpp index 7adbf193d73..99c0a2be22b 100644 --- a/src/Layers/xrRender/GPUCullingManager.cpp +++ b/src/Layers/xrRender/GPUCullingManager.cpp @@ -12,6 +12,9 @@ #include "Layers/xrRender/FBasicVisual.h" #include "Layers/xrRender/Bindless/VertexConverter.h" #include "Layers/xrRender/SkeletonCustom.h" // For CKinematics bone access +#include "Layers/xrRender/FSkinned.h" +#include "Layers/xrRender/SkeletonX.h" +#include "Layers/xrRender/Decals/OverlayManager.h" #include "Layers/xrRender/ShaderVariant/ShaderVariantRegistry.h" #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" #include "Layers/xrRender/Bindless/MaterialBuffer.h" @@ -643,6 +646,180 @@ void GPUCullingManager::EnsureSkinnedBufferCapacity(u32 count) m_maxSkinnedObjects = newCapacity; } +void GPUCullingManager::EnsureSkinnedBucketCapacity(SkinnedBucket& bucket, const char* name, u32 capacity) +{ + if (bucket.objectBuffer + && bucket.objectBuffer->getDesc().byteSize >= capacity * sizeof(GPUObjectData)) + return; + + nvrhi::IDevice* nvDevice = m_device->GetNVRHIDevice(); + + static char nameBuf[128]; + + auto makeStructured = [&](const char* suffix, u64 byteSize, u32 stride, bool uav) { + nvrhi::BufferDesc desc; + snprintf(nameBuf, sizeof(nameBuf), "GPUCull_Skinned%s_%s", name, suffix); + desc.debugName = nameBuf; + desc.byteSize = byteSize; + desc.structStride = stride; + desc.canHaveUAVs = uav; + desc.initialState = uav ? nvrhi::ResourceStates::UnorderedAccess : nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + return nvDevice->createBuffer(desc); + }; + + bucket.objectBuffer = makeStructured("Objects", u64(capacity) * sizeof(GPUObjectData), sizeof(GPUObjectData), false); + bucket.visibilityBuffer = makeStructured("Visibility", u64(capacity) * sizeof(u32), sizeof(u32), true); + bucket.materialIDBuffer = makeStructured("MaterialIDs", u64(capacity) * sizeof(u32), sizeof(u32), false); + bucket.recordsBuffer = makeStructured("Records", u64(capacity) * sizeof(SkinnedDrawRecord), sizeof(SkinnedDrawRecord), false); + bucket.compactBatchIndicesBuffer = makeStructured("CompactIndices", u64(capacity) * sizeof(u32), sizeof(u32), true); + bucket.compactMaterialIDBuffer = makeStructured("CompactMaterialIDs", u64(capacity) * sizeof(u32), sizeof(u32), true); + bucket.compactLocalPrefixBuffer = makeStructured("LocalPrefix", u64(capacity) * sizeof(u32), sizeof(u32), true); + bucket.compactGroupCountsBuffer = makeStructured("GroupCounts", u64(COMPACT_THREAD_GROUP_SIZE) * sizeof(u32), sizeof(u32), true); + bucket.compactGroupOffsetsBuffer = makeStructured("GroupOffsets", u64(COMPACT_THREAD_GROUP_SIZE) * sizeof(u32), sizeof(u32), true); + + { + nvrhi::BufferDesc desc; + snprintf(nameBuf, sizeof(nameBuf), "GPUCull_Skinned%s_TemplateArgs", name); + desc.debugName = nameBuf; + desc.byteSize = u64(capacity) * sizeof(IndirectDrawArgs); + desc.canHaveRawViews = true; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + bucket.drawArgsBuffer = nvDevice->createBuffer(desc); + } + { + nvrhi::BufferDesc desc; + snprintf(nameBuf, sizeof(nameBuf), "GPUCull_Skinned%s_CompactArgs", name); + desc.debugName = nameBuf; + desc.byteSize = u64(capacity) * sizeof(IndirectDrawArgs); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + bucket.compactDrawArgsBuffer = nvDevice->createBuffer(desc); + } + { + nvrhi::BufferDesc desc; + snprintf(nameBuf, sizeof(nameBuf), "GPUCull_Skinned%s_CompactCount", name); + desc.debugName = nameBuf; + desc.byteSize = sizeof(u32); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + bucket.compactCountBuffer = nvDevice->createBuffer(desc); + } +} + +void GPUCullingManager::DispatchSkinnedBucketCompaction(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + SkinnedBucket& bucket, u32 frameId) +{ + const u32 compactGroupCount = (bucket.count + COMPACT_THREAD_GROUP_SIZE - 1) / COMPACT_THREAD_GROUP_SIZE; + R_ASSERT2(compactGroupCount <= COMPACT_THREAD_GROUP_SIZE, + "Skinned bucket compaction group count exceeds scan group size"); + + struct CompactParamsCB { + u32 batchCount; + u32 frameId; + u32 padding[2]; + }; + CompactParamsCB compactCB; + compactCB.batchCount = bucket.count; + compactCB.frameId = frameId; + compactCB.padding[0] = compactCB.padding[1] = 0; + cmdList->writeBuffer(m_device->GetNativeBuffer(m_compactParamsCB), &compactCB, sizeof(compactCB)); + + cmdList->setBufferState(bucket.visibilityBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(bucket.compactLocalPrefixBuffer, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(bucket.compactGroupCountsBuffer, nvrhi::ResourceStates::UnorderedAccess); + + auto* compactCountRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("batch_compact_count", ".cs"); + framegraph::BindingSetBuilder countBsb(*compactCountRefl, nvDevice); + countBsb.ConstantBuffer("CompactParams", m_device->GetNativeBuffer(m_compactParamsCB)) + .BufferSRV("g_Visibility", bucket.visibilityBuffer) + .BufferUAV("g_LocalPrefix", bucket.compactLocalPrefixBuffer) + .BufferUAV("g_GroupCounts", bucket.compactGroupCountsBuffer); + + nvrhi::BindingSetHandle countBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(countBsb.Build(), m_compactCountLayout, nvDevice); + R_ASSERT2(countBindingSet, "Failed to create skinned bucket compaction count binding set"); + + nvrhi::ComputeState countState; + countState.pipeline = m_compactCountPipeline; + countState.bindings = { countBindingSet }; + cmdList->setComputeState(countState); + cmdList->dispatch(compactGroupCount, 1, 1); + + cmdList->setBufferState(bucket.compactGroupCountsBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(bucket.compactGroupOffsetsBuffer, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(bucket.compactCountBuffer, nvrhi::ResourceStates::UnorderedAccess); + + auto* compactScanRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("batch_compact_scan", ".cs"); + framegraph::BindingSetBuilder scanBsb(*compactScanRefl, nvDevice); + if (!m_skinnedDispatchArgsDummy) { + nvrhi::BufferDesc desc; + desc.debugName = "GPUCull_SkinnedDispatchArgsDummy"; + desc.byteSize = sizeof(u32) * 3; + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_skinnedDispatchArgsDummy = nvDevice->createBuffer(desc); + } + + scanBsb.ConstantBuffer("CompactParams", m_device->GetNativeBuffer(m_compactParamsCB)) + .BufferSRV("g_GroupCounts", bucket.compactGroupCountsBuffer) + .BufferUAV("g_GroupOffsets", bucket.compactGroupOffsetsBuffer) + .BufferUAV("g_VisibleCount", bucket.compactCountBuffer) + .BufferUAV("g_DispatchArgs", m_skinnedDispatchArgsDummy); + + nvrhi::BindingSetHandle scanBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(scanBsb.Build(), m_compactScanLayout, nvDevice); + R_ASSERT2(scanBindingSet, "Failed to create skinned bucket compaction scan binding set"); + + nvrhi::ComputeState scanState; + scanState.pipeline = m_compactScanPipeline; + scanState.bindings = { scanBindingSet }; + cmdList->setComputeState(scanState); + cmdList->dispatch(1, 1, 1); + + cmdList->setBufferState(bucket.compactLocalPrefixBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(bucket.compactGroupOffsetsBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(bucket.drawArgsBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(bucket.materialIDBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(bucket.compactDrawArgsBuffer, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(bucket.compactBatchIndicesBuffer, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(bucket.compactMaterialIDBuffer, nvrhi::ResourceStates::UnorderedAccess); + + auto* compactScatterRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("batch_compact", ".cs"); + framegraph::BindingSetBuilder scatterBsb(*compactScatterRefl, nvDevice); + scatterBsb.ConstantBuffer("CompactParams", m_device->GetNativeBuffer(m_compactParamsCB)) + .BufferSRV("g_InputDrawArgs", bucket.drawArgsBuffer) + .BufferSRV("g_InputMaterialIDs", bucket.materialIDBuffer) + .BufferSRV("g_Visibility", bucket.visibilityBuffer) + .BufferSRV("g_LocalPrefix", bucket.compactLocalPrefixBuffer) + .BufferSRV("g_GroupOffsets", bucket.compactGroupOffsetsBuffer) + .BufferUAV("g_OutputDrawArgs", bucket.compactDrawArgsBuffer) + .BufferUAV("g_VisibleBatchIndices", bucket.compactBatchIndicesBuffer) + .BufferUAV("g_OutputMaterialIDs", bucket.compactMaterialIDBuffer); + + nvrhi::BindingSetHandle scatterBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(scatterBsb.Build(), m_compactScatterLayout, nvDevice); + R_ASSERT2(scatterBindingSet, "Failed to create skinned bucket compaction scatter binding set"); + + nvrhi::ComputeState scatterState; + scatterState.pipeline = m_compactScatterPipeline; + scatterState.bindings = { scatterBindingSet }; + cmdList->setComputeState(scatterState); + cmdList->dispatch(compactGroupCount, 1, 1); + + cmdList->setBufferState(bucket.compactDrawArgsBuffer, nvrhi::ResourceStates::IndirectArgument); + cmdList->setBufferState(bucket.compactCountBuffer, nvrhi::ResourceStates::IndirectArgument); + cmdList->setBufferState(bucket.compactBatchIndicesBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(bucket.compactMaterialIDBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(bucket.recordsBuffer, nvrhi::ResourceStates::ShaderResource); +} + void GPUCullingManager::CreateComputePipeline(fg::RenderDevice* device) { if (!m_computeEnabled) @@ -2065,66 +2242,138 @@ void GPUCullingManager::InvalidateShadersAndPipelines() // UPLOAD SKINNED OBJECTS // ═══════════════════════════════════════════════════════ -void GPUCullingManager::UploadSkinnedObjects(fg::RenderContext* ctx, const GeometryCollector* geometry) +void GPUCullingManager::UploadSkinnedObjects(fg::RenderContext* ctx, const GeometryCollector* geometry, + decals::OverlayManager* overlayMgr) { ZoneScopedN("GPUCull::UploadSkinnedObjects"); + m_skinnedObjectData.clear(); + m_skinnedDrawArgsData.clear(); + for (u32 f = SkinnedGeometryPools::FIRST_FORMAT; f < SkinnedGeometryPools::FORMAT_COUNT; ++f) { + SkinnedBucket& bucket = m_skinnedBuckets[f]; + bucket.objects.clear(); + bucket.args.clear(); + bucket.records.clear(); + bucket.materialIDs.clear(); + bucket.count = 0; + } + m_skinnedResidualCount = 0; + if (!m_skinnedCullEnabled || !geometry) { m_skinnedObjectCount = 0; return; } - const auto& batches = geometry->GetBatches(); + auto cmdList = ctx->GetCommandList(); + m_skinnedPools.FlushUploads(m_device->GetNVRHIDevice(), cmdList); - // Clear and rebuild skinned data - m_skinnedObjectData.clear(); - m_skinnedDrawArgsData.clear(); + const bool mdiEnabled = IsSkinnedMDIEnabled(); + const auto& batches = geometry->GetBatches(); - // Collect skinned batches for (const auto& batch : batches) { if (!batch.isSkinned) continue; - u32 batchIdx = static_cast(m_skinnedObjectData.size()); - - // Object data for culling (bounding sphere) GPUObjectData obj; obj.position = batch.worldBoundsCenter; obj.radius = batch.worldBoundsRadius; - obj.batchIndex = batchIdx; obj.flags = 0; // Skinned meshes use their own alpha handling obj.pad0 = 0.0f; obj.pad1 = 0.0f; - m_skinnedObjectData.push_back(obj); + + const u32 variantIdx = bindless::MaterialBuffer::Instance().GetShaderVariant(batch.bindlessMaterialID); + const bool pooled = mdiEnabled && variantIdx == 0 + && batch.skinnedPoolFormat >= SkinnedGeometryPools::FIRST_FORMAT + && batch.skinnedPoolFormat < SkinnedGeometryPools::FORMAT_COUNT; IndirectDrawArgs args; args.indexCountPerInstance = batch.indexCount; args.instanceCount = 1; - args.startIndexLocation = batch.startIndex; - args.baseVertexLocation = batch.baseVertex; args.startInstanceLocation = 0; - m_skinnedDrawArgsData.push_back(args); + + if (pooled) { + SkinnedBucket& bucket = m_skinnedBuckets[batch.skinnedPoolFormat]; + obj.batchIndex = static_cast(bucket.objects.size()); + bucket.objects.push_back(obj); + + args.startIndexLocation = batch.skinnedPoolFirstIndex; + args.baseVertexLocation = batch.skinnedPoolBaseVertex; + bucket.args.push_back(args); + + CKinematics* skeleton = nullptr; + u32 visualType = batch.visual ? batch.visual->getType() : 0; + if (visualType == MT_SKELETON_GEOMDEF_ST) + skeleton = static_cast(batch.visual)->GetParent(); + else if (visualType == MT_SKELETON_GEOMDEF_PM) + skeleton = static_cast(batch.visual)->GetParent(); + + SkinnedDrawRecord rec; + rec.world = batch.worldMatrix; + rec.boneOffset = GetOrUploadSkeleton(cmdList, skeleton); + rec.splatOffset = 0; + rec.splatCount = 0; + if (overlayMgr && skeleton) { + auto sr = overlayMgr->GetSplatRange(skeleton); + rec.splatOffset = sr.offset; + rec.splatCount = sr.count; + } + rec.pad = 0; + bucket.records.push_back(rec); + bucket.materialIDs.push_back(batch.bindlessMaterialID); + } else { + obj.batchIndex = static_cast(m_skinnedObjectData.size()); + m_skinnedObjectData.push_back(obj); + + args.startIndexLocation = batch.startIndex; + args.baseVertexLocation = batch.baseVertex; + m_skinnedDrawArgsData.push_back(args); + } } - m_skinnedObjectCount = static_cast(m_skinnedObjectData.size()); + m_skinnedResidualCount = static_cast(m_skinnedObjectData.size()); + u32 total = m_skinnedResidualCount; + for (u32 f = SkinnedGeometryPools::FIRST_FORMAT; f < SkinnedGeometryPools::FORMAT_COUNT; ++f) { + m_skinnedBuckets[f].count = static_cast(m_skinnedBuckets[f].objects.size()); + total += m_skinnedBuckets[f].count; + } + m_skinnedObjectCount = total; - if (m_skinnedObjectCount == 0) + if (total == 0) return; - // Ensure buffer capacity - EnsureSkinnedBufferCapacity(m_skinnedObjectCount); + EnsureSkinnedBufferCapacity(total); - // Upload to GPU - auto cmdList = ctx->GetCommandList(); + if (m_skinnedResidualCount > 0) { + cmdList->writeBuffer(m_skinnedObjectBuffer, + m_skinnedObjectData.data(), + m_skinnedResidualCount * sizeof(GPUObjectData)); + cmdList->setBufferState(m_skinnedObjectBuffer, nvrhi::ResourceStates::ShaderResource); + + cmdList->writeBuffer(m_skinnedDrawArgsBuffer, + m_skinnedDrawArgsData.data(), + m_skinnedResidualCount * sizeof(IndirectDrawArgs)); + } + + static const char* bucketNames[SkinnedGeometryPools::FORMAT_COUNT] = { + "MDI", "NonHQ", "HQ1W", "HQ4W", "HQ2W", "HQ3W" + }; + for (u32 f = SkinnedGeometryPools::FIRST_FORMAT; f < SkinnedGeometryPools::FORMAT_COUNT; ++f) { + SkinnedBucket& bucket = m_skinnedBuckets[f]; + if (bucket.count == 0) + continue; - cmdList->writeBuffer(m_skinnedObjectBuffer, - m_skinnedObjectData.data(), - m_skinnedObjectCount * sizeof(GPUObjectData)); - cmdList->setBufferState(m_skinnedObjectBuffer, nvrhi::ResourceStates::ShaderResource); + EnsureSkinnedBucketCapacity(bucket, bucketNames[f], m_maxSkinnedObjects); - cmdList->writeBuffer(m_skinnedDrawArgsBuffer, - m_skinnedDrawArgsData.data(), - m_skinnedObjectCount * sizeof(IndirectDrawArgs)); + cmdList->writeBuffer(bucket.objectBuffer, bucket.objects.data(), + bucket.count * sizeof(GPUObjectData)); + cmdList->setBufferState(bucket.objectBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->writeBuffer(bucket.drawArgsBuffer, bucket.args.data(), + bucket.count * sizeof(IndirectDrawArgs)); + cmdList->writeBuffer(bucket.recordsBuffer, bucket.records.data(), + bucket.count * sizeof(SkinnedDrawRecord)); + cmdList->writeBuffer(bucket.materialIDBuffer, bucket.materialIDs.data(), + bucket.count * sizeof(u32)); + } } bool GPUCullingManager::EnsureSkinnedArgsGatePipeline(nvrhi::IDevice* nvDevice) @@ -2132,10 +2381,18 @@ bool GPUCullingManager::EnsureSkinnedArgsGatePipeline(nvrhi::IDevice* nvDevice) if (m_skinnedArgsGatePipeline) return true; + auto gateShader = GEnv.Render->GetShaderLoader()->LoadComputeShader("skinned_args_gate"); + if (!gateShader.handle) { + Msg("! [GPUCulling] skinned_args_gate.cs failed to load"); + return false; + } + auto& cache = framegraph::GetPassResourceCache(); auto* gateRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("skinned_args_gate", ".cs"); - if (!gateRefl) + if (!gateRefl) { + Msg("! [GPUCulling] skinned_args_gate reflection unavailable"); return false; + } m_skinnedArgsGateLayout = cache.GetOrCreateBindingLayoutFromReflection( "GPUCull_SkinnedArgsGate", *gateRefl, nvDevice); @@ -2145,11 +2402,6 @@ bool GPUCullingManager::EnsureSkinnedArgsGatePipeline(nvrhi::IDevice* nvDevice) } nvrhi::ComputePipelineDesc pipeDesc; - auto gateShader = GEnv.Render->GetShaderLoader()->LoadComputeShader("skinned_args_gate"); - if (!gateShader.handle) { - Msg("! [GPUCulling] skinned_args_gate.cs not found"); - return false; - } pipeDesc.CS = gateShader.handle; pipeDesc.bindingLayouts = { m_skinnedArgsGateLayout }; @@ -2902,7 +3154,8 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( u32 hizHeight, u32 hizMipLevels, const GeometryCollector* geometry, - const Fmatrix& prevViewProj) + const Fmatrix& prevViewProj, + decals::OverlayManager* overlayMgr) { using namespace framegraph; @@ -2917,6 +3170,7 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( VirtualResourceHandle drawArgsBuffer; GPUCullingManager* manager; const GeometryCollector* geometry; + decals::OverlayManager* overlayMgr; Fmatrix prevViewProj; u32 hizWidth; u32 hizHeight; @@ -2949,12 +3203,13 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( "Skinned GPU Culling", // Setup lambda - [&, hizWidth, hizHeight, hizMipLevels, geometry, prevViewProj, visBufferHandle, argsBufferHandle](FrameGraph& builder, PassHandle passHandle, SkinnedCullPassData& data) { + [&, hizWidth, hizHeight, hizMipLevels, geometry, prevViewProj, visBufferHandle, argsBufferHandle, overlayMgr](FrameGraph& builder, PassHandle passHandle, SkinnedCullPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); passBuilder.asyncCompute(); data.manager = this; data.geometry = geometry; + data.overlayMgr = overlayMgr; data.prevViewProj = prevViewProj; data.hizWidth = hizWidth; data.hizHeight = hizHeight; @@ -2980,7 +3235,7 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( return; // Upload skinned objects (must happen during execute with correct command list) - mgr->UploadSkinnedObjects(ctx, data.geometry); + mgr->UploadSkinnedObjects(ctx, data.geometry, data.overlayMgr); // Early out if no skinned objects after upload if (mgr->m_skinnedObjectCount == 0) @@ -3004,64 +3259,81 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( if (!mgr->EnsureSkinnedArgsGatePipeline(nvDevice)) return; - // Clear visibility buffer to 0 (all culled initially) - cmdList->clearBufferUInt(mgr->m_skinnedVisibilityBuffer, 0); cmdList->clearBufferUInt(mgr->m_skinnedVisibleCountBuffer, 0); - // Fill constant buffer (reuse m_cullParamsCB) + // Fill constant buffer (reuse m_cullParamsCB); objectCount rewritten per dispatch CullParamsCB cb; cb.viewProj = Device.mFullTransform; cb.prevViewProj = data.prevViewProj; cb.cameraPos = Device.vCameraPosition; float farPlane = g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.far_plane : 300.0f; cb.maxDistanceSq = farPlane * farPlane; - cb.objectCount = mgr->m_skinnedObjectCount; cb.hizWidth = data.hizWidth; cb.hizHeight = data.hizHeight; cb.hizMipLevels = data.hizMipLevels; cb.frameId = frameId; cb.padding[0] = cb.padding[1] = cb.padding[2] = 0; - mgr->ExtractFrustumPlanes(Device.mFullTransform, cb.frustumPlanes); - cmdList->writeBuffer(mgr->m_device->GetNativeBuffer(mgr->m_cullParamsCB), &cb, sizeof(cb)); - auto* objectCullRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("object_cull", ".cs"); + + auto dispatchSkinnedCull = [&](nvrhi::IBuffer* objects, nvrhi::IBuffer* visibility, u32 count) { + cb.objectCount = count; + cmdList->writeBuffer(mgr->m_device->GetNativeBuffer(mgr->m_cullParamsCB), &cb, sizeof(cb)); + + cmdList->clearBufferUInt(visibility, 0); + framegraph::BindingSetBuilder bsb(*objectCullRefl, nvDevice, "GPUCull.DynamicCull"); - bsb.ConstantBuffer("CullParams", mgr->m_device->GetNativeBuffer(mgr->m_cullParamsCB)) - .BufferSRV("g_Objects", mgr->m_skinnedObjectBuffer) - .Texture("g_HiZPyramid", hizTexture) - .BufferUAV("g_VisibleIndices", mgr->m_skinnedVisibleIndexBuffer) - .BufferUAV("g_VisibleCount", mgr->m_skinnedVisibleCountBuffer) - .BufferUAV("g_Visibility", mgr->m_skinnedVisibilityBuffer); + bsb.ConstantBuffer("CullParams", mgr->m_device->GetNativeBuffer(mgr->m_cullParamsCB)) + .BufferSRV("g_Objects", objects) + .Texture("g_HiZPyramid", hizTexture) + .BufferUAV("g_VisibleIndices", mgr->m_skinnedVisibleIndexBuffer) + .BufferUAV("g_VisibleCount", mgr->m_skinnedVisibleCountBuffer) + .BufferUAV("g_Visibility", visibility); - nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_cullLayout); - R_ASSERT2(bindingSet, "Failed to create skinned culling binding set"); + nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_cullLayout); + R_ASSERT2(bindingSet, "Failed to create skinned culling binding set"); - // Set compute state and dispatch culling - nvrhi::ComputeState state; - state.pipeline = mgr->m_cullPipeline; - state.bindings = { bindingSet }; - cmdList->setComputeState(state); + nvrhi::ComputeState state; + state.pipeline = mgr->m_cullPipeline; + state.bindings = { bindingSet }; + cmdList->setComputeState(state); - u32 groupCount = (mgr->m_skinnedObjectCount + CULL_THREAD_GROUP_SIZE - 1) / CULL_THREAD_GROUP_SIZE; - cmdList->dispatch(groupCount, 1, 1); + u32 groupCount = (count + CULL_THREAD_GROUP_SIZE - 1) / CULL_THREAD_GROUP_SIZE; + cmdList->dispatch(groupCount, 1, 1); + }; - // Gate indirect args: instanceCount = 1 for visible batches, 0 for culled - auto* gateRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("skinned_args_gate", ".cs"); - framegraph::BindingSetBuilder gateBsb(*gateRefl, nvDevice, "GPUCull.SkinnedArgsGate"); - gateBsb.ConstantBuffer("CullParams", mgr->m_device->GetNativeBuffer(mgr->m_cullParamsCB)) - .BufferSRV("g_Visibility", mgr->m_skinnedVisibilityBuffer) - .BufferUAV("g_DrawArgs", mgr->m_skinnedDrawArgsBuffer); + for (u32 f = SkinnedGeometryPools::FIRST_FORMAT; f < SkinnedGeometryPools::FORMAT_COUNT; ++f) { + SkinnedBucket& bucket = mgr->m_skinnedBuckets[f]; + if (bucket.count == 0) + continue; - nvrhi::BindingSetHandle gateBindingSet = nvDevice->createBindingSet(gateBsb.Build(), mgr->m_skinnedArgsGateLayout); - R_ASSERT2(gateBindingSet, "Failed to create skinned args gate binding set"); + dispatchSkinnedCull(bucket.objectBuffer, bucket.visibilityBuffer, bucket.count); + mgr->DispatchSkinnedBucketCompaction(cmdList, nvDevice, bucket, frameId); + } - nvrhi::ComputeState gateState; - gateState.pipeline = mgr->m_skinnedArgsGatePipeline; - gateState.bindings = { gateBindingSet }; - cmdList->setComputeState(gateState); - cmdList->dispatch(groupCount, 1, 1); + if (mgr->m_skinnedResidualCount > 0) { + dispatchSkinnedCull(mgr->m_skinnedObjectBuffer, mgr->m_skinnedVisibilityBuffer, + mgr->m_skinnedResidualCount); + + // Gate residual indirect args: instanceCount = 1 for visible batches, 0 for culled + auto* gateRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("skinned_args_gate", ".cs"); + framegraph::BindingSetBuilder gateBsb(*gateRefl, nvDevice, "GPUCull.SkinnedArgsGate"); + gateBsb.ConstantBuffer("CullParams", mgr->m_device->GetNativeBuffer(mgr->m_cullParamsCB)) + .BufferSRV("g_Visibility", mgr->m_skinnedVisibilityBuffer) + .BufferUAV("g_DrawArgs", mgr->m_skinnedDrawArgsBuffer); + + nvrhi::BindingSetHandle gateBindingSet = nvDevice->createBindingSet(gateBsb.Build(), mgr->m_skinnedArgsGateLayout); + R_ASSERT2(gateBindingSet, "Failed to create skinned args gate binding set"); + + nvrhi::ComputeState gateState; + gateState.pipeline = mgr->m_skinnedArgsGatePipeline; + gateState.bindings = { gateBindingSet }; + cmdList->setComputeState(gateState); + + u32 gateGroups = (mgr->m_skinnedResidualCount + CULL_THREAD_GROUP_SIZE - 1) / CULL_THREAD_GROUP_SIZE; + cmdList->dispatch(gateGroups, 1, 1); + } } ); diff --git a/src/Layers/xrRender/GPUCullingManager.h b/src/Layers/xrRender/GPUCullingManager.h index d351c2aa828..7f8d7667546 100644 --- a/src/Layers/xrRender/GPUCullingManager.h +++ b/src/Layers/xrRender/GPUCullingManager.h @@ -7,6 +7,7 @@ #include "Layers/xrRender/RenderContext/ResourceHandle.h" #include "Layers/xrRender/Bindless/UnifiedVertex.h" #include "Layers/xrRender/ShaderVariant/VariantPartitionConfig.h" +#include "Layers/xrRender/Geometry/SkinnedGeometryPools.h" namespace xray::render::fg::passes { struct ParticleBatch; @@ -28,6 +29,9 @@ namespace xray::render::fg { class dxRender_Visual; // Forward declaration for visual pointer map class CKinematics; class RTAccelStructManager; + namespace decals { + class OverlayManager; + } } namespace xray::render::fg { @@ -384,7 +388,8 @@ class GPUCullingManager { // the gate compute zeroes instanceCount for culled batches same-frame. // Upload skinned mesh bounding spheres (call from UploadSceneObjects) - void UploadSkinnedObjects(fg::RenderContext* ctx, const GeometryCollector* geometry); + void UploadSkinnedObjects(fg::RenderContext* ctx, const GeometryCollector* geometry, + decals::OverlayManager* overlayMgr); // Setup skinned culling pass (uses same Hi-Z pyramid as static culling). // Returns the imported draw-args buffer handle (invalid if disabled) so the @@ -396,12 +401,45 @@ class GPUCullingManager { u32 hizHeight, u32 hizMipLevels, const GeometryCollector* geometry, - const Fmatrix& prevViewProj + const Fmatrix& prevViewProj, + decals::OverlayManager* overlayMgr ); u32 GetSkinnedObjectCount() const { return m_skinnedObjectCount; } bool IsSkinnedCullingEnabled() const { return m_initialized && m_skinnedCullEnabled; } + bool IsSkinnedMDIEnabled() const { return IsSkinnedCullingEnabled() && m_compactEnabled; } nvrhi::IBuffer* GetSkinnedDrawArgsBuffer() const { return m_skinnedDrawArgsBuffer.Get(); } + SkinnedGeometryPools& GetSkinnedPools() { return m_skinnedPools; } + + struct SkinnedDrawRecord { + Fmatrix world; + u32 boneOffset; + u32 splatOffset; + u32 splatCount; + u32 pad; + }; + static_assert(sizeof(SkinnedDrawRecord) == 80, "SkinnedDrawRecord must be 80 bytes"); + + struct SkinnedBucket { + nvrhi::BufferHandle objectBuffer; + nvrhi::BufferHandle visibilityBuffer; + nvrhi::BufferHandle drawArgsBuffer; + nvrhi::BufferHandle materialIDBuffer; + nvrhi::BufferHandle recordsBuffer; + nvrhi::BufferHandle compactDrawArgsBuffer; + nvrhi::BufferHandle compactBatchIndicesBuffer; + nvrhi::BufferHandle compactMaterialIDBuffer; + nvrhi::BufferHandle compactCountBuffer; + nvrhi::BufferHandle compactLocalPrefixBuffer; + nvrhi::BufferHandle compactGroupCountsBuffer; + nvrhi::BufferHandle compactGroupOffsetsBuffer; + xr_vector objects; + xr_vector args; + xr_vector records; + xr_vector materialIDs; + u32 count = 0; + }; + const SkinnedBucket& GetSkinnedBucket(u32 formatID) const { return m_skinnedBuckets[formatID]; } // Skinned culling stats (for profiler display) struct SkinnedCullingStats { @@ -616,6 +654,7 @@ class GPUCullingManager { nvrhi::BufferHandle m_skinnedDrawArgsBuffer; // IndirectDrawArgs per batch, instanceCount gated by compute nvrhi::BufferHandle m_skinnedVisibleCountBuffer; // Atomic visible counter (stats) nvrhi::BufferHandle m_skinnedVisibleIndexBuffer; // Visible batch indices (debug) + nvrhi::BufferHandle m_skinnedDispatchArgsDummy; // Scan stage dispatch-args sink (unused) nvrhi::ComputePipelineHandle m_skinnedArgsGatePipeline; nvrhi::BindingLayoutHandle m_skinnedArgsGateLayout; @@ -628,6 +667,14 @@ class GPUCullingManager { xr_vector m_skinnedDrawArgsData; SkinnedCullingStats m_skinnedCullingStats; + SkinnedGeometryPools m_skinnedPools; + SkinnedBucket m_skinnedBuckets[SkinnedGeometryPools::FORMAT_COUNT]; + u32 m_skinnedResidualCount = 0; + + void EnsureSkinnedBucketCapacity(SkinnedBucket& bucket, const char* name, u32 capacity); + void DispatchSkinnedBucketCompaction(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + SkinnedBucket& bucket, u32 frameId); + // Global bone buffer for GPU-driven skinned rendering // All skeleton bones are uploaded here each frame, indexed by per-instance offset static constexpr u32 MAX_TOTAL_BONES = 8192; // ~100 skeletons * 78 bones diff --git a/src/Layers/xrRender/Geometry/GeometryBatch.h b/src/Layers/xrRender/Geometry/GeometryBatch.h index f791c134dc2..4a6821455d7 100644 --- a/src/Layers/xrRender/Geometry/GeometryBatch.h +++ b/src/Layers/xrRender/Geometry/GeometryBatch.h @@ -88,6 +88,10 @@ struct GeometryBatch { // Used to select correct shader (1B, 2B, 3B, 4B variants) u16 skinningRenderMode = 0; + u32 skinnedPoolFormat = UINT32_MAX; + s32 skinnedPoolBaseVertex = 0; + u32 skinnedPoolFirstIndex = 0; + // Terrain flag - determines which pipeline to use // Terrain uses bindless_terrain.ps with 4-layer detail blending bool isTerrain = false; diff --git a/src/Layers/xrRender/Geometry/SkinnedGeometryPools.cpp b/src/Layers/xrRender/Geometry/SkinnedGeometryPools.cpp new file mode 100644 index 00000000000..ed22ee10b92 --- /dev/null +++ b/src/Layers/xrRender/Geometry/SkinnedGeometryPools.cpp @@ -0,0 +1,168 @@ +#include "stdafx.h" +#include "SkinnedGeometryPools.h" +#include "Layers/xrRender/BufferUtils.h" +#include "Layers/xrRender/ShaderVariant/VariantPSOCache.h" + +namespace xray::render::fg { + +u32 SkinnedFormatFromRenderMode(u16 renderMode, u32 vertexStride) +{ + enum { + RM_SKINNING_SOFT = 0, + RM_SINGLE = 1, + RM_SINGLE_HQ = 2, + RM_SKINNING_1B = 3, + RM_SKINNING_1B_HQ = 4, + RM_SKINNING_2B = 5, + RM_SKINNING_2B_HQ = 6, + RM_SKINNING_3B = 7, + RM_SKINNING_3B_HQ = 8, + RM_SKINNING_4B = 9, + RM_SKINNING_4B_HQ = 10 + }; + + if (renderMode == RM_SKINNING_3B || renderMode == RM_SKINNING_3B_HQ) return VF_SKINNED_HQ3W; + if (renderMode == RM_SKINNING_2B || renderMode == RM_SKINNING_2B_HQ) return VF_SKINNED_HQ2W; + if (renderMode == RM_SKINNING_4B || renderMode == RM_SKINNING_4B_HQ) return VF_SKINNED_HQ4W; + if (renderMode == RM_SKINNING_1B_HQ || renderMode == RM_SINGLE_HQ) return VF_SKINNED_HQ1W; + if (renderMode == RM_SKINNING_1B || renderMode == RM_SINGLE) return VF_SKINNED_NONHQ; + if (vertexStride == 36) return VF_SKINNED_HQ1W; + if (vertexStride == 40) return VF_SKINNED_HQ4W; + if (vertexStride == 44) return VF_SKINNED_HQ2W; + return VF_SKINNED_NONHQ; +} + +u32 SkinnedFormatStride(u32 formatID) +{ + switch (formatID) { + case VF_SKINNED_NONHQ: return 24; + case VF_SKINNED_HQ1W: return 36; + case VF_SKINNED_HQ4W: return 40; + case VF_SKINNED_HQ2W: return 44; + case VF_SKINNED_HQ3W: return 44; + default: return 0; + } +} + +bool SkinnedGeometryPools::Register(VertexStagingBuffer* vsb, IndexStagingBuffer* isb, + u32 vCount, u32 vStride, u32 iCount, u32 formatID) +{ + if (!vsb || !isb || vCount == 0 || iCount == 0) + return false; + if (formatID < FIRST_FORMAT || formatID >= FORMAT_COUNT) + return false; + if (vStride != SkinnedFormatStride(formatID)) + return false; + + if (vsb->skinned_pool_format == formatID) + return true; + if (vsb->skinned_pool_format != UINT32_MAX) + return false; + + const void* vdata = vsb->Map(0, 0, true); + if (!vdata) + return false; + const void* idata = isb->Map(0, 0, true); + if (!idata) { + vsb->Unmap(); + return false; + } + + Pool& pool = m_pools[formatID]; + const u32 baseVertex = pool.vertexCount; + const u32 firstIndex = pool.indexCount; + + const size_t vBytes = size_t(vCount) * vStride; + const size_t iBytes = size_t(iCount) * sizeof(u16); + const u8* vsrc = static_cast(vdata); + const u8* isrc = static_cast(idata); + pool.vertexData.insert(pool.vertexData.end(), vsrc, vsrc + vBytes); + pool.indexData.insert(pool.indexData.end(), isrc, isrc + iBytes); + pool.vertexCount += vCount; + pool.indexCount += iCount; + + vsb->Unmap(); + isb->Unmap(); + + vsb->skinned_pool_format = formatID; + vsb->skinned_pool_base_vertex = baseVertex; + vsb->skinned_pool_first_index = firstIndex; + return true; +} + +void SkinnedGeometryPools::FlushUploads(nvrhi::IDevice* nvDevice, nvrhi::ICommandList* cmdList) +{ + for (u32 f = FIRST_FORMAT; f < FORMAT_COUNT; ++f) { + Pool& pool = m_pools[f]; + + if (!pool.vertexData.empty()) { + if (!pool.vertexBuffer || pool.vertexBuffer->getDesc().byteSize < pool.vertexData.size()) { + size_t capacity = 256 * 1024; + while (capacity < pool.vertexData.size()) + capacity *= 2; + + nvrhi::BufferDesc desc; + desc.debugName = "SkinnedPool_VB"; + desc.byteSize = capacity; + desc.isVertexBuffer = true; + desc.initialState = nvrhi::ResourceStates::VertexBuffer; + desc.keepInitialState = true; + pool.vertexBuffer = nvDevice->createBuffer(desc); + pool.vertexBytesUploaded = 0; + } + if (pool.vertexBuffer && pool.vertexBytesUploaded < pool.vertexData.size()) { + cmdList->writeBuffer(pool.vertexBuffer, + pool.vertexData.data() + pool.vertexBytesUploaded, + pool.vertexData.size() - pool.vertexBytesUploaded, + pool.vertexBytesUploaded); + pool.vertexBytesUploaded = pool.vertexData.size(); + } + } + + if (!pool.indexData.empty()) { + if (!pool.indexBuffer || pool.indexBuffer->getDesc().byteSize < pool.indexData.size()) { + size_t capacity = 128 * 1024; + while (capacity < pool.indexData.size()) + capacity *= 2; + + nvrhi::BufferDesc desc; + desc.debugName = "SkinnedPool_IB"; + desc.byteSize = capacity; + desc.isIndexBuffer = true; + desc.initialState = nvrhi::ResourceStates::IndexBuffer; + desc.keepInitialState = true; + pool.indexBuffer = nvDevice->createBuffer(desc); + pool.indexBytesUploaded = 0; + } + if (pool.indexBuffer && pool.indexBytesUploaded < pool.indexData.size()) { + cmdList->writeBuffer(pool.indexBuffer, + pool.indexData.data() + pool.indexBytesUploaded, + pool.indexData.size() - pool.indexBytesUploaded, + pool.indexBytesUploaded); + pool.indexBytesUploaded = pool.indexData.size(); + } + } + } +} + +nvrhi::IBuffer* SkinnedGeometryPools::GetVertexBuffer(u32 formatID) const +{ + if (formatID >= FORMAT_COUNT) + return nullptr; + return m_pools[formatID].vertexBuffer.Get(); +} + +nvrhi::IBuffer* SkinnedGeometryPools::GetIndexBuffer(u32 formatID) const +{ + if (formatID >= FORMAT_COUNT) + return nullptr; + return m_pools[formatID].indexBuffer.Get(); +} + +void SkinnedGeometryPools::Reset() +{ + for (auto& pool : m_pools) + pool = Pool{}; +} + +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/Geometry/SkinnedGeometryPools.h b/src/Layers/xrRender/Geometry/SkinnedGeometryPools.h new file mode 100644 index 00000000000..b389ad6d13f --- /dev/null +++ b/src/Layers/xrRender/Geometry/SkinnedGeometryPools.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include "xrCore/xrCore.h" + +class VertexStagingBuffer; +class IndexStagingBuffer; + +namespace xray::render::fg { + +u32 SkinnedFormatFromRenderMode(u16 renderMode, u32 vertexStride); +u32 SkinnedFormatStride(u32 formatID); + +class SkinnedGeometryPools +{ +public: + static constexpr u32 FIRST_FORMAT = 1; + static constexpr u32 FORMAT_COUNT = 6; + + bool Register(VertexStagingBuffer* vsb, IndexStagingBuffer* isb, + u32 vCount, u32 vStride, u32 iCount, u32 formatID); + + void FlushUploads(nvrhi::IDevice* nvDevice, nvrhi::ICommandList* cmdList); + + nvrhi::IBuffer* GetVertexBuffer(u32 formatID) const; + nvrhi::IBuffer* GetIndexBuffer(u32 formatID) const; + + void Reset(); + +private: + struct Pool + { + xr_vector vertexData; + xr_vector indexData; + u32 vertexCount = 0; + u32 indexCount = 0; + nvrhi::BufferHandle vertexBuffer; + nvrhi::BufferHandle indexBuffer; + size_t vertexBytesUploaded = 0; + size_t indexBytesUploaded = 0; + }; + + Pool m_pools[FORMAT_COUNT]; +}; + +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/r_FrameGraphRenderer.cpp b/src/Layers/xrRender/r_FrameGraphRenderer.cpp index 6daffe5b4e7..8e0ee308982 100644 --- a/src/Layers/xrRender/r_FrameGraphRenderer.cpp +++ b/src/Layers/xrRender/r_FrameGraphRenderer.cpp @@ -846,6 +846,7 @@ void FrameGraphRenderer::SetupFrame() { ZoneScopedN("Readback::CullStats"); m_gpuCullingManager->ProcessStatsReadback(); } + m_gpuCullingManager->BeginSkinnedFrame(); } if (m_detailManager && m_device) { @@ -1160,7 +1161,8 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { hizOutput.height, hizOutput.mipLevels, m_geometryCollector.get(), - m_prevViewProj + m_prevViewProj, + m_overlayManager.get() ); } } @@ -1578,7 +1580,6 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } } - data.gpuCulling->BeginSkinnedFrame(); data.accelMgr->BuildSkinnedBLAS(cmdList, data.gpuCulling, worldSkinned, hudSkinned); data.accelMgr->BuildGrassBLAS(cmdList, data.detailMgr); data.accelMgr->RebuildDynamic(cmdList, data.gpuCulling); @@ -2073,6 +2074,16 @@ bool FrameGraphRenderer::ProcessVisualGeometry(dxRender_Visual* visual, const Fm } else { batch.skinningRenderMode = static_cast(visual)->RenderMode; } + if (m_gpuCullingManager && meshVisual->p_rm_Vertices && meshVisual->p_rm_Indices) { + u32 fmt = fg::SkinnedFormatFromRenderMode(batch.skinningRenderMode, meshVisual->vStride); + if (m_gpuCullingManager->GetSkinnedPools().Register( + meshVisual->p_rm_Vertices, meshVisual->p_rm_Indices, + meshVisual->vCount, meshVisual->vStride, meshVisual->iCount, fmt)) { + batch.skinnedPoolFormat = fmt; + batch.skinnedPoolBaseVertex = (s32)meshVisual->p_rm_Vertices->skinned_pool_base_vertex; + batch.skinnedPoolFirstIndex = meshVisual->p_rm_Vertices->skinned_pool_first_index + batch.startIndex; + } + } } if (visualType == MT_TREE_ST || visualType == MT_TREE_PM) { batch.worldBoundsCenter = visual->vis.sphere.P; From b20ad10d5137c149224fee6ab00979c7857bff06 Mon Sep 17 00:00:00 2001 From: FoxConED Date: Thu, 23 Jul 2026 22:35:15 +0300 Subject: [PATCH 246/261] =?UTF-8?q?xrRender:=20FrameGraph=20lighting=20sta?= =?UTF-8?q?ck=20=E2=80=94=20CSM,=20local=20atlas=20shadows,=20postFX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire sun CSM and spot/OMNIPART local shadow atlas into forward+, with classic-style PCF sampling and sticky tile assignment so indoor furniture and character shadows stay stable. Also land the supporting FrameGraph passes (SSR/SSGI/AO/bloom/TAA/volumetrics/rain) and ignore local build artifacts (bin-rafx, rafx checkouts). --- .gitignore | 10 + .../FORWARD_PLUS_MIGRATION_PLAN.md | 2 +- res/gamedata/configs/text/eng/openxray.xml | 3 + res/gamedata/configs/text/rus/openxray.xml | 3 + res/gamedata/configs/ui/ui_mm_opt.xml | 17 +- res/gamedata/configs/ui/ui_mm_opt_16.xml | 9 + .../scripts/ui_mm_opt_video_adv.script | 6 + res/gamedata/shaders/r5/ao/ao_apply.ps | Bin 0 -> 876 bytes res/gamedata/shaders/r5/ao/ao_blur.ps | Bin 0 -> 1322 bytes res/gamedata/shaders/r5/ao/ao_common.h | 107 ++ res/gamedata/shaders/r5/ao/gtao_filter.ps | Bin 0 -> 1383 bytes res/gamedata/shaders/r5/ao/gtao_render.ps | Bin 0 -> 4154 bytes res/gamedata/shaders/r5/ao/hbao.ps | Bin 0 -> 2302 bytes res/gamedata/shaders/r5/ao/hdao.ps | Bin 0 -> 3450 bytes res/gamedata/shaders/r5/ao/ssao.ps | Bin 0 -> 2783 bytes res/gamedata/shaders/r5/bindless_common.h | 93 +- res/gamedata/shaders/r5/bindless_depth.ps | Bin 0 -> 609 bytes res/gamedata/shaders/r5/bindless_depth.vs | 48 +- .../shaders/r5/bindless_depth_opaque.ps | Bin 0 -> 232 bytes res/gamedata/shaders/r5/bindless_forward.ps | Bin 3511 -> 5979 bytes res/gamedata/shaders/r5/bindless_forward.vs | 6 + res/gamedata/shaders/r5/bindless_particle.ps | Bin 1570 -> 1878 bytes res/gamedata/shaders/r5/bindless_skinned.ps | Bin 2512 -> 2627 bytes .../shaders/r5/bindless_skinned_hud.ps | Bin 2482 -> 2796 bytes .../shaders/r5/bindless_skinned_mdi.ps | Bin 2265 -> 2380 bytes .../shaders/r5/bindless_skinned_tess.ds | 113 ++ .../shaders/r5/bindless_skinned_tess.hs | 83 + res/gamedata/shaders/r5/bindless_terrain.ps | Bin 7194 -> 9569 bytes res/gamedata/shaders/r5/bindless_tess.ds | 125 ++ res/gamedata/shaders/r5/bindless_tess.hs | 99 ++ res/gamedata/shaders/r5/bloom/composite.ps | Bin 0 -> 607 bytes res/gamedata/shaders/r5/bloom/downsample.ps | Bin 0 -> 1191 bytes res/gamedata/shaders/r5/bloom/extract.ps | Bin 0 -> 851 bytes res/gamedata/shaders/r5/bloom/upsample.ps | Bin 0 -> 1336 bytes res/gamedata/shaders/r5/cas.ps | Bin 0 -> 1302 bytes res/gamedata/shaders/r5/common_functions.h | 84 +- res/gamedata/shaders/r5/contact_shadows.ps | Bin 0 -> 4963 bytes res/gamedata/shaders/r5/cull_utils.h | 34 +- res/gamedata/shaders/r5/detail_billboard.ps | Bin 1770 -> 1810 bytes res/gamedata/shaders/r5/detail_billboard.vs | 2 + .../shaders/r5/detail_billboard_shadow.ps | Bin 0 -> 527 bytes .../shaders/r5/detail_billboard_shadow.vs | 86 + res/gamedata/shaders/r5/detail_decal.ps | Bin 1178 -> 1445 bytes res/gamedata/shaders/r5/detail_decal.vs | 2 + res/gamedata/shaders/r5/detail_gpu.ps | Bin 8354 -> 7589 bytes res/gamedata/shaders/r5/detail_gpu.vs | 5 +- res/gamedata/shaders/r5/detail_gpu_shadow.ps | Bin 0 -> 206 bytes res/gamedata/shaders/r5/detail_gpu_shadow.vs | 62 + res/gamedata/shaders/r5/dof.ps | Bin 0 -> 3091 bytes res/gamedata/shaders/r5/effects/flare.ps | Bin 0 -> 265 bytes res/gamedata/shaders/r5/effects/flare.vs | 13 + res/gamedata/shaders/r5/effects/sun.ps | Bin 0 -> 265 bytes res/gamedata/shaders/r5/effects/sun.vs | 13 + res/gamedata/shaders/r5/effects_world_soft.ps | Bin 0 -> 1239 bytes res/gamedata/shaders/r5/effects_world_soft.vs | 31 + res/gamedata/shaders/r5/exposure_adapt.cs | 165 +- res/gamedata/shaders/r5/hud/cursor.ps | Bin 0 -> 217 bytes res/gamedata/shaders/r5/hud/cursor.vs | 21 + res/gamedata/shaders/r5/hud/default.ps | Bin 0 -> 217 bytes res/gamedata/shaders/r5/hud/default.vs | 21 + res/gamedata/shaders/r5/hud/movie.ps | Bin 0 -> 766 bytes res/gamedata/shaders/r5/hud/movie.vs | 21 + .../shaders/r5/luminance_histogram.cs | 14 +- res/gamedata/shaders/r5/object_cull.cs | 12 +- .../shaders/r5/rain/rain_billboard.cs | 96 ++ res/gamedata/shaders/r5/rain/rain_update.cs | 170 ++ .../shaders/r5/shadow/shadow_cascade.ps | Bin 0 -> 621 bytes .../shaders/r5/shadow/shadow_cascade.vs | 56 + .../r5/shadow/shadow_cascade_foliage.ps | Bin 0 -> 1482 bytes .../r5/shadow/shadow_cascade_skinned.vs | 46 + .../r5/shadow/shadow_cascade_skinned_2w.vs | 41 + .../r5/shadow/shadow_cascade_skinned_3w.vs | 45 + .../r5/shadow/shadow_cascade_skinned_4w.vs | 54 + .../r5/shadow/shadow_cascade_skinned_hq.vs | 41 + .../r5/shadow/shadow_cascade_terrain.ps | Bin 0 -> 336 bytes .../shaders/r5/shared/clustered_lighting.h | 9 +- res/gamedata/shaders/r5/shared/common.h | 6 +- .../shaders/r5/shared/contact_shadows.h | 37 + res/gamedata/shaders/r5/shared/foliage_sss.h | 66 + .../shaders/r5/shared/shadow_sampling.h | 600 ++++++- res/gamedata/shaders/r5/shared/sky_ibl.h | 83 + res/gamedata/shaders/r5/shared/ssr.h | 306 ++++ res/gamedata/shaders/r5/shared/wmark.h | 2 +- res/gamedata/shaders/r5/sky_forward.ps | Bin 1120 -> 1443 bytes res/gamedata/shaders/r5/sky_forward.vs | 4 + res/gamedata/shaders/r5/smoke_trail.ps | Bin 488 -> 1383 bytes res/gamedata/shaders/r5/smoke_trail.vs | 2 +- res/gamedata/shaders/r5/ssgi_apply.ps | Bin 0 -> 1665 bytes res/gamedata/shaders/r5/ssgi_blur.ps | Bin 0 -> 1216 bytes res/gamedata/shaders/r5/ssgi_temporal.ps | Bin 0 -> 1367 bytes res/gamedata/shaders/r5/ssgi_trace.ps | Bin 0 -> 4000 bytes res/gamedata/shaders/r5/ssr_apply.ps | Bin 0 -> 769 bytes res/gamedata/shaders/r5/ssr_blur.ps | Bin 0 -> 2188 bytes res/gamedata/shaders/r5/ssr_resolve.ps | Bin 0 -> 4713 bytes res/gamedata/shaders/r5/sun_forward.vs | 4 +- res/gamedata/shaders/r5/taa.ps | Bin 0 -> 7528 bytes res/gamedata/shaders/r5/tonemap.ps | Bin 1346 -> 1412 bytes res/gamedata/shaders/r5/trail.ps | Bin 479 -> 760 bytes res/gamedata/shaders/r5/trail.vs | 5 +- .../shaders/r5/volumetric/apply_lighting.cs | 25 + .../shaders/r5/volumetric/clear_volume.cs | 12 + .../shaders/r5/volumetric/particle_inject.cs | 45 + .../shaders/r5/volumetric/ray_march.ps | Bin 0 -> 2403 bytes .../shaders/r5/volumetric/ray_march.vs | 14 + .../shaders/r5/volumetric/shared_froxel.h | 58 + .../shaders/r5/volumetric/sunshafts.ps | Bin 0 -> 2260 bytes .../r5/volumetric/sunshafts_combine.ps | Bin 0 -> 1258 bytes .../r5/volumetric/temporal_reproject.cs | 39 + .../shaders/r5/volumetric/world_fog.cs | 20 + res/gamedata/shaders/r5/water.ps | Bin 0 -> 4684 bytes res/gamedata/shaders/r5/water.vs | 107 ++ res/gamedata/shaders/r5/wet/rain_apply.ps | Bin 0 -> 4130 bytes .../shaders/r5/wet/rain_patch_normal.ps | Bin 0 -> 3406 bytes .../shaders/r5/wet/rain_write_normal.ps | Bin 0 -> 1313 bytes res/gamedata/shaders/r5/wet/ssfx_ripples.h | 46 + res/gamedata/shaders/r5/wet/ssfx_ssr.h | 104 ++ res/gamedata/shaders/r5/wet/ssfx_waterfall.h | 104 ++ res/gamedata/textures/fx/puddles_perlin.dds | Bin 0 -> 699192 bytes res/gamedata/textures/fx/rain_drop.dds | Bin 0 -> 5588 bytes res/gamedata/textures/fx/rain_splash.dds | Bin 0 -> 240 bytes res/gamedata/textures/fx/water_normal.dds | Bin 0 -> 699192 bytes .../textures/fx/water_sbumpvolume.dds | Bin 0 -> 349652 bytes src/Layers/xrRender/Backend/D3D12Backend.cpp | 4 + src/Layers/xrRender/Backend/VulkanBackend.cpp | 36 +- src/Layers/xrRender/Backend/VulkanBackend.h | 6 + src/Layers/xrRender/Bindless/BindlessTypes.h | 60 +- src/Layers/xrRender/Bindless/MaterialBuffer.h | 5 +- .../Bindless/TerrainMaterialBuffer.cpp | 4 + .../xrRender/Bindless/TerrainMaterialBuffer.h | 5 +- src/Layers/xrRender/CMakeLists.txt | 42 + src/Layers/xrRender/ClusteredLightManager.cpp | 292 +++- src/Layers/xrRender/ClusteredLightManager.h | 25 + src/Layers/xrRender/FGDetailManager.cpp | 112 +- src/Layers/xrRender/FGDetailManager.h | 14 + src/Layers/xrRender/FGRenderBase.h | 3 +- .../xrRender/FrameGraph/BindingSetBuilder.cpp | 16 +- src/Layers/xrRender/FrameGraph/FrameGraph.cpp | 15 + .../xrRender/FrameGraph/PassResourceCache.cpp | 80 +- .../xrRender/FrameGraph/PassResourceCache.h | 8 + src/Layers/xrRender/FrameGraph/ShaderCache.h | 5 +- .../xrRender/FrameGraph/ShaderLoader.cpp | 153 ++ src/Layers/xrRender/FrameGraph/ShaderLoader.h | 16 + .../xrRender/FrameGraph/ShaderReflection.cpp | 1 + .../xrRender/FrameGraph/ShaderReflection.h | 3 +- .../AmbientOcclusionPassSetup.cpp | 486 ++++++ .../AmbientOcclusionPassSetup.h | 49 + .../FrameGraphPasses/BloomPassSetup.cpp | 329 ++++ .../FrameGraphPasses/BloomPassSetup.h | 36 + .../FrameGraphPasses/CASPassSetup.cpp | 154 ++ .../xrRender/FrameGraphPasses/CASPassSetup.h | 31 + .../ClusterLightPassSetup.cpp | 11 +- .../ContactShadowsPassSetup.cpp | 264 ++++ .../ContactShadowsPassSetup.h | 39 + .../FrameGraphPasses/DepthPrepassSetup.cpp | 295 ++++ .../FrameGraphPasses/DepthPrepassSetup.h | 46 + .../FrameGraphPasses/DetailCullPassSetup.cpp | 3 +- .../FrameGraphPasses/DetailPassSetup.cpp | 99 +- .../FrameGraphPasses/DetailPassSetup.h | 15 +- .../FrameGraphPasses/DofPassSetup.cpp | 173 +++ .../xrRender/FrameGraphPasses/DofPassSetup.h | 29 + .../FrameGraphPasses/ExposurePassSetup.cpp | 113 +- .../FrameGraphPasses/ExposurePassSetup.h | 60 +- .../ForwardColorPassSetup.cpp | 270 +++- .../FrameGraphPasses/ForwardColorPassSetup.h | 68 + .../FrameGraphPasses/LocalShadowPassSetup.cpp | 522 +++++++ .../FrameGraphPasses/LocalShadowPassSetup.h | 58 + .../FrameGraphPasses/ParticlePassSetup.cpp | 18 +- .../xrRender/FrameGraphPasses/PassCommon.cpp | 70 + .../xrRender/FrameGraphPasses/PassCommon.h | 14 +- .../FrameGraphPasses/PassVertexFormats.h | 16 +- .../FrameGraphPasses/RainGPUSimManager.cpp | 342 ++++ .../FrameGraphPasses/RainGPUSimManager.h | 98 ++ .../FrameGraphPasses/RainPassSetup.cpp | 39 +- .../xrRender/FrameGraphPasses/RainPassSetup.h | 13 +- .../FrameGraphPasses/RainShadowPassSetup.cpp | 353 +++++ .../FrameGraphPasses/RainShadowPassSetup.h | 52 + .../FrameGraphPasses/SSGIPassSetup.cpp | 391 +++++ .../xrRender/FrameGraphPasses/SSGIPassSetup.h | 45 + .../FrameGraphPasses/SSRPassSetup.cpp | 535 +++++++ .../xrRender/FrameGraphPasses/SSRPassSetup.h | 53 + .../SceneReflectionPassSetup.cpp | 75 + .../SceneReflectionPassSetup.h | 23 + .../FrameGraphPasses/ShaderConstants.cpp | 6 +- .../FrameGraphPasses/ShaderConstants.h | 228 ++- .../FrameGraphPasses/ShadowCascadeFit.cpp | 164 ++ .../FrameGraphPasses/ShadowCascadeFit.h | 22 + .../FrameGraphPasses/ShadowPassSetup.cpp | 1380 +++++++++++++++++ .../FrameGraphPasses/ShadowPassSetup.h | 142 ++ .../FrameGraphPasses/SkinningPassSetup.cpp | 209 ++- .../FrameGraphPasses/SkinningPassSetup.h | 28 +- .../FrameGraphPasses/SmokeTrailPassSetup.cpp | 1 + .../FrameGraphPasses/SunPassSetup.cpp | 7 +- .../xrRender/FrameGraphPasses/SunPassSetup.h | 3 + .../FrameGraphPasses/SunShaftsPassSetup.cpp | 346 +++++ .../FrameGraphPasses/SunShaftsPassSetup.h | 44 + .../FrameGraphPasses/TAAPassSetup.cpp | 300 ++++ .../xrRender/FrameGraphPasses/TAAPassSetup.h | 59 + .../FrameGraphPasses/ThunderboltPassSetup.cpp | 33 +- .../FrameGraphPasses/ThunderboltPassSetup.h | 9 +- .../FrameGraphPasses/TonemapPassSetup.cpp | 27 +- .../FrameGraphPasses/TonemapPassSetup.h | 3 +- .../FrameGraphPasses/TransparentPassSetup.cpp | 261 +++- .../FrameGraphPasses/TransparentPassSetup.h | 16 + .../xrRender/FrameGraphPasses/UIPassSetup.cpp | 16 +- .../FrameGraphPasses/VolumetricPassSetup.cpp | 455 ++++++ .../FrameGraphPasses/VolumetricPassSetup.h | 125 ++ .../FrameGraphPasses/WetSurfacesPassSetup.cpp | 497 ++++++ .../FrameGraphPasses/WetSurfacesPassSetup.h | 52 + src/Layers/xrRender/GPUCullingManager.cpp | 808 +++++++++- src/Layers/xrRender/GPUCullingManager.h | 73 +- .../xrRender/Geometry/MaterialCache.cpp | 320 +++- src/Layers/xrRender/Geometry/MaterialCache.h | 2 +- src/Layers/xrRender/HOM.cpp | 2 +- .../xrRender/Materials/MaterialSystem.cpp | 21 +- .../xrRender/RenderContext/RenderDevice.h | 2 +- .../ShaderVariant/VariantPSOCache.cpp | 185 ++- .../xrRender/ShaderVariant/VariantPSOCache.h | 8 + src/Layers/xrRender/Shaders/SlangCompiler.cpp | 13 +- .../xrRender/Volumetrics/IVolumetricSource.h | 30 + .../Volumetrics/VolumetricRenderer.cpp | 100 ++ .../xrRender/Volumetrics/VolumetricRenderer.h | 51 + .../xrRender/Volumetrics/WorldFogSource.cpp | 34 + .../xrRender/Volumetrics/WorldFogSource.h | 30 + src/Layers/xrRender/fgEnvironmentRender.cpp | 13 +- src/Layers/xrRender/fgRainRender.cpp | 351 ++++- src/Layers/xrRender/fgRainRender.h | 33 +- src/Layers/xrRender/fgThunderboltRender.cpp | 59 +- src/Layers/xrRender/fgThunderboltRender.h | 4 +- src/Layers/xrRender/fgUIRender.cpp | 6 +- src/Layers/xrRender/light.cpp | 79 +- src/Layers/xrRender/light.h | 2 + src/Layers/xrRender/r_FrameGraphRenderer.cpp | 1175 +++++++++++--- src/Layers/xrRender/r_FrameGraphRenderer.h | 19 +- .../xrRender/r_FrameGraphRenderer_Loader.cpp | 7 +- src/Layers/xrRender/r__pixel_calculator.cpp | 3 +- src/Layers/xrRender/r__scene.cpp | 6 +- src/Layers/xrRender/r__sector.cpp | 15 +- src/Layers/xrRender/r__sector.h | 62 +- src/Layers/xrRender/r__sector_traversal.cpp | 191 +++ src/Layers/xrRender/xrRender_console.cpp | 94 +- src/Layers/xrRender/xrRender_console.h | 18 + src/xrCore/CMakeLists.txt | 13 + src/xrCore/Profiler/Profiler.cpp | 10 + src/xrEngine/Environment_editor.cpp | 4 +- src/xrEngine/IRenderBackend.h | 4 + src/xrEngine/Rain.cpp | 64 +- src/xrEngine/Render.h | 3 + src/xrEngine/thunderbolt.cpp | 31 +- src/xrEngine/xr_ioc_cmd.cpp | 18 + src/xrEngine/xr_ioc_cmd.h | 17 + src/xr_3da/entry_point.cpp | 10 +- 251 files changed, 17006 insertions(+), 1166 deletions(-) create mode 100644 res/gamedata/shaders/r5/ao/ao_apply.ps create mode 100644 res/gamedata/shaders/r5/ao/ao_blur.ps create mode 100644 res/gamedata/shaders/r5/ao/ao_common.h create mode 100644 res/gamedata/shaders/r5/ao/gtao_filter.ps create mode 100644 res/gamedata/shaders/r5/ao/gtao_render.ps create mode 100644 res/gamedata/shaders/r5/ao/hbao.ps create mode 100644 res/gamedata/shaders/r5/ao/hdao.ps create mode 100644 res/gamedata/shaders/r5/ao/ssao.ps create mode 100644 res/gamedata/shaders/r5/bindless_depth.ps create mode 100644 res/gamedata/shaders/r5/bindless_depth_opaque.ps create mode 100644 res/gamedata/shaders/r5/bindless_skinned_tess.ds create mode 100644 res/gamedata/shaders/r5/bindless_skinned_tess.hs create mode 100644 res/gamedata/shaders/r5/bindless_tess.ds create mode 100644 res/gamedata/shaders/r5/bindless_tess.hs create mode 100644 res/gamedata/shaders/r5/bloom/composite.ps create mode 100644 res/gamedata/shaders/r5/bloom/downsample.ps create mode 100644 res/gamedata/shaders/r5/bloom/extract.ps create mode 100644 res/gamedata/shaders/r5/bloom/upsample.ps create mode 100644 res/gamedata/shaders/r5/cas.ps create mode 100644 res/gamedata/shaders/r5/contact_shadows.ps create mode 100644 res/gamedata/shaders/r5/detail_billboard_shadow.ps create mode 100644 res/gamedata/shaders/r5/detail_billboard_shadow.vs create mode 100644 res/gamedata/shaders/r5/detail_gpu_shadow.ps create mode 100644 res/gamedata/shaders/r5/detail_gpu_shadow.vs create mode 100644 res/gamedata/shaders/r5/dof.ps create mode 100644 res/gamedata/shaders/r5/effects/flare.ps create mode 100644 res/gamedata/shaders/r5/effects/flare.vs create mode 100644 res/gamedata/shaders/r5/effects/sun.ps create mode 100644 res/gamedata/shaders/r5/effects/sun.vs create mode 100644 res/gamedata/shaders/r5/effects_world_soft.ps create mode 100644 res/gamedata/shaders/r5/effects_world_soft.vs create mode 100644 res/gamedata/shaders/r5/hud/cursor.ps create mode 100644 res/gamedata/shaders/r5/hud/cursor.vs create mode 100644 res/gamedata/shaders/r5/hud/default.ps create mode 100644 res/gamedata/shaders/r5/hud/default.vs create mode 100644 res/gamedata/shaders/r5/hud/movie.ps create mode 100644 res/gamedata/shaders/r5/hud/movie.vs create mode 100644 res/gamedata/shaders/r5/rain/rain_billboard.cs create mode 100644 res/gamedata/shaders/r5/rain/rain_update.cs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade.ps create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_foliage.ps create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_2w.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_3w.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_4w.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_hq.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_terrain.ps create mode 100644 res/gamedata/shaders/r5/shared/contact_shadows.h create mode 100644 res/gamedata/shaders/r5/shared/foliage_sss.h create mode 100644 res/gamedata/shaders/r5/shared/sky_ibl.h create mode 100644 res/gamedata/shaders/r5/shared/ssr.h create mode 100644 res/gamedata/shaders/r5/ssgi_apply.ps create mode 100644 res/gamedata/shaders/r5/ssgi_blur.ps create mode 100644 res/gamedata/shaders/r5/ssgi_temporal.ps create mode 100644 res/gamedata/shaders/r5/ssgi_trace.ps create mode 100644 res/gamedata/shaders/r5/ssr_apply.ps create mode 100644 res/gamedata/shaders/r5/ssr_blur.ps create mode 100644 res/gamedata/shaders/r5/ssr_resolve.ps create mode 100644 res/gamedata/shaders/r5/taa.ps create mode 100644 res/gamedata/shaders/r5/volumetric/apply_lighting.cs create mode 100644 res/gamedata/shaders/r5/volumetric/clear_volume.cs create mode 100644 res/gamedata/shaders/r5/volumetric/particle_inject.cs create mode 100644 res/gamedata/shaders/r5/volumetric/ray_march.ps create mode 100644 res/gamedata/shaders/r5/volumetric/ray_march.vs create mode 100644 res/gamedata/shaders/r5/volumetric/shared_froxel.h create mode 100644 res/gamedata/shaders/r5/volumetric/sunshafts.ps create mode 100644 res/gamedata/shaders/r5/volumetric/sunshafts_combine.ps create mode 100644 res/gamedata/shaders/r5/volumetric/temporal_reproject.cs create mode 100644 res/gamedata/shaders/r5/volumetric/world_fog.cs create mode 100644 res/gamedata/shaders/r5/water.ps create mode 100644 res/gamedata/shaders/r5/water.vs create mode 100644 res/gamedata/shaders/r5/wet/rain_apply.ps create mode 100644 res/gamedata/shaders/r5/wet/rain_patch_normal.ps create mode 100644 res/gamedata/shaders/r5/wet/rain_write_normal.ps create mode 100644 res/gamedata/shaders/r5/wet/ssfx_ripples.h create mode 100644 res/gamedata/shaders/r5/wet/ssfx_ssr.h create mode 100644 res/gamedata/shaders/r5/wet/ssfx_waterfall.h create mode 100644 res/gamedata/textures/fx/puddles_perlin.dds create mode 100644 res/gamedata/textures/fx/rain_drop.dds create mode 100644 res/gamedata/textures/fx/rain_splash.dds create mode 100644 res/gamedata/textures/fx/water_normal.dds create mode 100644 res/gamedata/textures/fx/water_sbumpvolume.dds create mode 100644 src/Layers/xrRender/FrameGraphPasses/AmbientOcclusionPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/AmbientOcclusionPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/CASPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/CASPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/DofPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/DofPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/RainGPUSimManager.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/RainGPUSimManager.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/ShadowCascadeFit.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/ShadowCascadeFit.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.h create mode 100644 src/Layers/xrRender/Volumetrics/IVolumetricSource.h create mode 100644 src/Layers/xrRender/Volumetrics/VolumetricRenderer.cpp create mode 100644 src/Layers/xrRender/Volumetrics/VolumetricRenderer.h create mode 100644 src/Layers/xrRender/Volumetrics/WorldFogSource.cpp create mode 100644 src/Layers/xrRender/Volumetrics/WorldFogSource.h create mode 100644 src/Layers/xrRender/r__sector_traversal.cpp diff --git a/.gitignore b/.gitignore index 21665f8b85b..358c57496e4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,14 @@ # exclude binaries and temporary files [Bb]in/ [Bb]uild*/ +bin-rafx/ cmake_builds/ +# Local RAFX checkouts / scratch (not vendored for this branch) +Externals/rafx/ +Externals/rafx-proj/ +.tmp_classic_sun/ + # User-specific files *.user *.user.props @@ -23,3 +29,7 @@ cmake_builds/ # exclude doxygen output doc/doxygen/html/* + +# Screen Space Shaders reference +refs/ssfx/ +ScreenSpaceShaders_Update23_5.zip diff --git a/framegraph_docs/FORWARD_PLUS_MIGRATION_PLAN.md b/framegraph_docs/FORWARD_PLUS_MIGRATION_PLAN.md index e191143daa0..47adbca7467 100644 --- a/framegraph_docs/FORWARD_PLUS_MIGRATION_PLAN.md +++ b/framegraph_docs/FORWARD_PLUS_MIGRATION_PLAN.md @@ -1,7 +1,7 @@ # Forward+ Renderer Implementation Plan ## X-Ray Engine FrameGraph Migration -**Status:** Ready to implement +**Status:** Largely implemented in-tree (see `STATUS.md`). This document remains a design/history reference; do not treat the 24-week checklist as unfinished work. **Timeline:** 24 weeks to full production-ready forward+ renderer with volumetrics and particles **Target Performance:** 100-165fps @ 1920×1080 (from current ~90-110fps) **Key Features:** PBR lighting, CSM shadows, clustered lights, froxel volumetrics, four-tier particles diff --git a/res/gamedata/configs/text/eng/openxray.xml b/res/gamedata/configs/text/eng/openxray.xml index 1d3a8778b69..81eaf6d4632 100644 --- a/res/gamedata/configs/text/eng/openxray.xml +++ b/res/gamedata/configs/text/eng/openxray.xml @@ -123,4 +123,7 @@ Ukrainian + + Grass shadows + diff --git a/res/gamedata/configs/text/rus/openxray.xml b/res/gamedata/configs/text/rus/openxray.xml index c1dddd46179..24389b53094 100644 --- a/res/gamedata/configs/text/rus/openxray.xml +++ b/res/gamedata/configs/text/rus/openxray.xml @@ -132,4 +132,7 @@ (Ukrainian) + + + diff --git a/res/gamedata/configs/ui/ui_mm_opt.xml b/res/gamedata/configs/ui/ui_mm_opt.xml index 0ad4fe22b87..57a499e072c 100644 --- a/res/gamedata/configs/ui/ui_mm_opt.xml +++ b/res/gamedata/configs/ui/ui_mm_opt.xml @@ -238,6 +238,14 @@ + + ui_mm_r2_sun_details + + + ui_inGame2_checkbox + + + ui_mm_r2_steep_parallax @@ -387,7 +395,7 @@ ui_mm_ssao_text - + + diff --git a/res/gamedata/configs/ui/ui_mm_opt_16.xml b/res/gamedata/configs/ui/ui_mm_opt_16.xml index 53229dfabed..7f26722d053 100644 --- a/res/gamedata/configs/ui/ui_mm_opt_16.xml +++ b/res/gamedata/configs/ui/ui_mm_opt_16.xml @@ -241,6 +241,15 @@ + + ui_mm_r2_sun_details + + + ui_inGame2_checkbox + + + + ui_mm_r2_steep_parallax diff --git a/res/gamedata/scripts/ui_mm_opt_video_adv.script b/res/gamedata/scripts/ui_mm_opt_video_adv.script index 8dd7fe1144e..2e60eba81a5 100644 --- a/res/gamedata/scripts/ui_mm_opt_video_adv.script +++ b/res/gamedata/scripts/ui_mm_opt_video_adv.script @@ -150,6 +150,12 @@ function opt_video_adv:InitControls(x, y, xml, handler) ctl = xml:InitCheck("video_adv:check_r2_detail_bump", _st) table.insert(handler.m_preconditions, {func=mode_ge_2, control=_st}) +-- r2_sun_details (grass cast shadows) + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_r2_sun_details", _st) + ctl = xml:InitCheck("video_adv:check_r2_sun_details", _st) + table.insert(handler.m_preconditions, {func=mode_ge_2, control=_st}) + -- r2_steep_parallax >r2 _st = xml:InitStatic("video_adv:templ_item", nil) xml:InitStatic("video_adv:cap_r2_steep_parallax", _st) diff --git a/res/gamedata/shaders/r5/ao/ao_apply.ps b/res/gamedata/shaders/r5/ao/ao_apply.ps new file mode 100644 index 0000000000000000000000000000000000000000..f77b87dbba4b6301b36d4079caa89be6579459ef GIT binary patch literal 876 zcmb7?O^?$s5Qgvm6{Fotib9gDR!FoeQndwv1hi_y$1UB0ry5|7_^-e)F`VQH=U3d=R1f!C`o+n~R46ls4;jVz1d-@OFDv7X7XV7{9m`vs;6_oJL>x)O%D`BxdshCm)^XS?fVYAELyx)9C**6AsN@X}+f;C>EG@736 zK4zQoYyMC&1r&$e7{W4x^yTNP^?JKX=IoY~2^pck&pr|YUvo#mODIR4sv{mI3@$BZHtp3O3Ga& zFgZ<|_lOrq*wKzRR*ARbi%@}YY^F^q6`>CYspuFzgl-h~*r4c1cRD@u|I&#LgOV&E z7<68Ap`O#3^cJ0s6SNkZmX}t7(mptI1D)-1{gU3;+NC literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/ao/ao_blur.ps b/res/gamedata/shaders/r5/ao/ao_blur.ps new file mode 100644 index 0000000000000000000000000000000000000000..778edde0c2a75ede05ed9842833a46c5b65211f7 GIT binary patch literal 1322 zcmbVMZHv=D5dNNDG2FqCzNKkBMXeR|&JTjxVynoBgl)R%ZQ1OG&2C$I^uIg1d1<1@ z35LS9JM%pA%*>Mxhwz?pqNySr-pydejq)znG+~0m;&b#O3cX7vVs6$HE@GLcQUv!G zo)@o7l2C=18!2*4gwEkLD7s-elKHD=9q+*dd=f5+z6K(rJXVy7$se~FQ6$a(x>a&J z@lF%ZThfCzijHq#6QRIH4IOqSt?(_)^nEL1gc)A0m5DXX7tzNr^KZ+xu1w}kGbs?Z z_#Vy6Uky29(DV>Xsn(dX{P5#uHv4)T7TU_*Qo@8^(E}5i(Vc>CNz{hwuvrpfDsV#B z%G7lfEJ&Jh`ibr+_wzK1giIL6jDEosROR~+c7j7gS;=(gh+Oag#l;fv3r%Z$tpaaL@U{kT&mEo#EXIxlYvCZ^>Ix8u z-_E%tirGEaz-cFAsXf1jjz>!?pum>nh40f#U=bZN2`)HqEZ- zhE{nvI{3f*@D#(|QqV)@56CL_k1(j5W_7;|1N+`pHSOtM1K8&kDh!@uqPxXWc5!7r z9Ap>!ejJE6EZIedHm@~C&sxgH+978ej5V%LPSdI3N{~jo_skLvAteXj;?Tl$a9v7i Q+uH&91+G5qpUBDk4bq!*D*ylh literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/ao/ao_common.h b/res/gamedata/shaders/r5/ao/ao_common.h new file mode 100644 index 00000000000..984a12606ac --- /dev/null +++ b/res/gamedata/shaders/r5/ao/ao_common.h @@ -0,0 +1,107 @@ +// Shared helpers for FrameGraph ambient occlusion passes +#ifndef AO_COMMON_H +#define AO_COMMON_H + +#define SM_6_0 +#include "common.h" + +cbuffer AOConstants : register(b3) +{ + float4 ao_screen; // xy = size, zw = 1/size + float4 ao_params; // x=radius, y=bias, z=strength, w=quality (1..4) + float4 ao_proj; // x=gtao_parameters (focal), y=frame, zw=unused +}; + +Texture2D g_Depth : register(t0); +Texture2D g_Normal : register(t1); +Texture2D g_WorldPos : register(t2); + +struct PS_INPUT +{ + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; +}; + +float AoQuality() +{ + return max(ao_params.w, 1.0); +} + +float AoRadius() +{ + return ao_params.x; +} + +float AoBias() +{ + return ao_params.y; +} + +float AoStrength() +{ + return ao_params.z; +} + +float SampleDepth(float2 uv) +{ + return g_Depth.SampleLevel(smp_nofilter, uv, 0).x; +} + +float3 SampleWorldNormal(float2 uv) +{ + // Forward FrameGraph writes signed world normals in RGBA16F (xyz = N, w = roughness). + float3 n = g_Normal.SampleLevel(smp_nofilter, uv, 0).xyz; + return normalize(n); +} + +// GTAO/HBAO focal: ao_proj.x is already IX-Ray half-focal; full focal for world→UV helpers. +float AoFocalPixels() +{ + return max(ao_proj.x * 2.0, 1.0); +} + +float4 SampleWorldPos(float2 uv) +{ + return g_WorldPos.SampleLevel(smp_nofilter, uv, 0); +} + +float3 WorldToView(float3 worldPos) +{ + return mul(m_V, float4(worldPos, 1.0)).xyz; +} + +// X-Ray view space: camera looks along +Z (depth increases with distance). +float ViewDepth(float3 viewPos) +{ + return max(abs(viewPos.z), 0.5); +} + +// World radius → UV radius using full focal (pixels at z=1). +float2 WorldRadiusToUV(float worldRadius, float viewZ) +{ + float px = (worldRadius * AoFocalPixels()) / max(viewZ, 0.5); + return px * ao_screen.zw; +} + +// Interleaved gradient noise (blue-noise substitute) +float Ign(float2 pixel) +{ + return frac(52.9829189 * frac(dot(pixel, float2(0.06711056, 0.00583715)))); +} + +float2 Ign2(float2 pixel) +{ + float n = Ign(pixel); + return float2(n, Ign(pixel + 19.19)); +} + +bool IsSky(float depth, float4 worldPos) +{ + if (depth >= 0.9999) + return true; + if (worldPos.w < 0.5) + return true; + return false; +} + +#endif diff --git a/res/gamedata/shaders/r5/ao/gtao_filter.ps b/res/gamedata/shaders/r5/ao/gtao_filter.ps new file mode 100644 index 0000000000000000000000000000000000000000..afba7aa362994e922a51c6b3510158dd59d69d86 GIT binary patch literal 1383 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 TBIiGRe!IKY0{?Yb{4D(sf5m+U literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/ao/hdao.ps b/res/gamedata/shaders/r5/ao/hdao.ps new file mode 100644 index 0000000000000000000000000000000000000000..677c8f17097cc89d9727135797dd0899b25f2cd5 GIT binary patch literal 3450 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 TeH7? g_InstanceData : register(t14); +StructuredBuffer g_CompactBatchIndices : register(t15); +StructuredBuffer g_CompactMaterialIDs : register(t16); + VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT output; + uint drawID = input.drawIndex; + uint batchIndex = g_CompactBatchIndices[drawID]; + InstanceData instanceData = g_InstanceData[batchIndex]; + uint materialID = g_CompactMaterialIDs[drawID]; - // Transform position - IDENTICAL to bindless_forward.vs - float4 worldPos = mul(g_World, float4(input.position.xyz, 1.0)); + float4 worldPos = mul(instanceData.world, float4(input.position.xyz, 1.0)); output.position = mul(m_VP, worldPos); - + output.texcoord = input.texcoord; + output.materialID = materialID; return output; } diff --git a/res/gamedata/shaders/r5/bindless_depth_opaque.ps b/res/gamedata/shaders/r5/bindless_depth_opaque.ps new file mode 100644 index 0000000000000000000000000000000000000000..ee139cc2c52af8fe5a763ecbbe3063763390bdf3 GIT binary patch literal 232 zcmYk0K@Y(|5QXpiE9O`Uia6;}9b9O%#I0HFib>YY>TZQd6Fu%dR68@j3m^}p+Qfn)TofeCvISV_^#aJgXVkhlxQ5dvD+u9>bf+#0W+5+d- zd4R<};dzq%hNLcblCDuSk~kdBd~@MP?(WhwjDtujHJK&ZgUA9eRXe-8G`jpiT{}c} zh|+K^BT@?y$c%+wGsp?T*=(hx*4&^Ni;2j- z$lUkb(nytyJoG2?D4B`~6Q?a#TM&iwMIOd;YLXI5N%taJL9tL2$+_?!DU+}_gS$I# z0yzs~31lW8CVS=U$>4T0KD{~ZO$Pn*i*f(zd~$Yu^Qm*wt3)tmbkXTue|k1FxIXC& zUK|?4QVLYJwP_X%U+*Q9lPQqyf zlN}3zyR)P6Yz04uqe=g2cst(twnO;MqD15elqM?7!z89d8r@BX_U)(=Y12cJMFB=2 zGCuu$a(#W%+p7t*I2rvU$%2}|eodfl)2lYz+8E9m-8ip}w>QS8;d6-tBM~QI4BXNr zLYBgd6<$lM`@NdV_eVs)uoq?=^dcMt87EG3UGM;OxRvjynPphh@!@Fa$59>7>)Y`& zMExW}foeF%BAd&+4%R`11>iR0b*v6(DwI6gR#C_6pdOG?2@qq`;NkW3-|V5vrA!GK zB%(;H$w9Ix75bzQ_W%jf84OTWD8SRW2-@tJ$Z&n9%xHo+EgzM4u zY^G$c==P4L5`9S8M#OzUB`EnQlrtn_@}OAP1J*`DXGv-jxECrhjgYcVCegmopH}y- zuC~L<(quLxr?A^we>~oExj`Aj8Z^(u6P9DskT#KQP!OU=#lEB&YP&%IA4`iUU5I8b zQQEucS5YYD5~UtS(KJD?R0?L8Y$-Ryawe|6Ux^m`N3_W1VZ6iuRc`uV3T z))#cj6+46BMF(0&WBOj%adtiEcg|1U7Zp~O)}}i;wzUyHNk=})N!gm3WyZT3ec=w7 zSf6C@b%G>!?iw^k+Stc#^?2pFRtaMV4c-%dWQ<$?$3nDO`S35SC#K3-t|DhS!6K%M zIz@|Y&_1H&dQWx@?88VvErfy;S$@Xb&oTYYvrS=BAW7|b3kFpYHY@*n3>8M=byMs5 zmSR2PJq)|@G3|dSG;WB5aV1hp7<=PUF0mIKV?WPQBw3-XLCbPh1G^*&8C#cuvcR_R zSf(RHlb84QP=`ikEru<+@5u;+<(N1W>qmM^PO~b07w`LCz5BHY{b49GHV(7U<;)Uq zttRw9u9LChN39+$)(|_Fc|pnSh_0G?Db!P|MgA&7ufxou(u_y=e1VT;bjq*YWEBS( zfuGibFHw@DUrmOiC4s%=6!P37{Q0QgU5f|3dl#CJek*$iXQy86Bio|YS5dFT*jH=o zyW<)(24f!OpEY~2_Dr-LwM-}AHd15_JKgofvpL48W`%Mk@k1N>!+@<(Xi=KUdjp!a zsQJtwgBJ@jj4#_`)szOAIYA2(7!hOCyE5`F2&h(QjI+}FkS>Lr8i0*)87E7KKx>nN z`UXs5ne$3q7i-*-h9DxG;Gi(K$Ko~m(8wB%7S9!7f zR6@xr$9J2{m&{Fzvjw_844qGkkLrt7;o-X$>i53dY1pFw{O|AhXF2d|tQXYunw%}h z3wn-D6=r)|aNy)Mg?0TvjNR%00Os9|E^6?D3IuQr!)+<;jP(#vf)b^sQn z4|Jx$v4VN%nXe3311lGEUd6E5#J3xghI3;PPaw}Y5}Iq>$eH!C-WA#)VpAC;Flcjb zHlJ<=<_aR(uXX*WWcp?;Dn>+b?pE9SPh&Lf+Wzyc?Dkqoo6>2v6&=XF5Sp5)``bm8XW?^nOH?R<%`h&jbS+6KcXb%Q!oxi-35pO|++dYwK>z@sav9_P;}JRn74~K$CVE+&A&|- zL5Hg|>J$62RV)K3X66uj$Uw$1?trjy=GCCrqlvkeyF`|~O4UuN>)g{PSNA0eM6Coj}sJ~Q%2KtR_xTzH}Rsx8&no^gG5pY7N_3gov)zuv9h{X zsHGyvp(!r&PAl8e0&~A3Og8>vAC@ literal 3511 zcmbVPZExE)5dQ98acHqY?i@LBoDN9Q4^HhS0USH1WKZ1Vzelb(rmBDf8X&< zmYt*><`02LK0Mw%_uP@`b|GeY%BfP(O2{pdX;`RMw+r)!_i*Euz%4<+c9er!laxvg zS}LkwMwO^!LWfi(k`<+pca#)gCn8_w40mok;66|3EXJB`J_3K@YUW z2U1eW2p^J?KqB&zuv|e}n}#*o2DHI&2Lvfwttv&00%QH9wMK%=vCcHw!L;SWc0d=zVlv4?m5U z^TlX38b;&$+q=d6Va-`rZQkt+*g%9}1yZ$wUs zrVYSF190g$Y%KSVmP?XnG%p+Lmq+U{dyeQ^L@rp4$t?uORA3Yp4q@Z+eLrlNy!rru zS{O1(kqi$3#00r8$=3^LquqM?&}xu2T`r!nmI#hYGn6fe%xKvpq<@GPDYffOKKc+7 zML(UIXmZkTl3p7@Rwhb*kV*hbt*ID03?gqsnW&=M2V?s`AMA-f@GJ$!H4! z&Kp^44B5m?XCrVQZh`J;REs4@P}h>UrIpB{f=IGa9n^njPk;bF+pQML&N7Icvpn!B zM8=D%Y&$U?K%y#f9;4J6)%hCTEN`#j3sV*0>>motFQ$@g21)@~J}GS1SWXaglKcfk zQmi!mi$@?DR6!Ez%`J0Hj1F%)8G%?qxu%K&+DU0#W!Lq&)57TK0&~0;TW!ZkoaLx3 zpMj58M zHKQ1@1o<8+rMsv11Z&jr`Os|RxCkF1i~OYedg|mi6p>H+oNNk?*|ArHatG+^$Tv&~ z{Ec#a<=9lZX6bGQXYC`{kDMMn@!&QZj34g?(PA`T z*sOdvaNQ^p&I*5T+oXg9iPTnzHa?>(S3yrUA%>buAUvk2z+jIu8_FzpiF3FKLhM5T zTacsPj-6RYH5W=D^xWs*)llvR^Jp@iJq*UDq1;rP!U-(KPqb!76Y~yvMP!--{j=`w zE@5$_UG~GECQ5@&196vYyYoX-eF>l7CiN~yg+Ah#uT8}R9ZP6W?r87Y4KzNW3~dHu z23A=vSNay9_-5n2e$9zLq;@!34DO#%doZMQKhQlUFc+xSNotOm>n@4?4l6GwobPRR zbV!EFK3)^r6PF;L8|&Ura)YPeaD4bV6R7cum#pCXv)&G`@I9x8!)jf+k9YVgZ5*rx z3h#6wzMyK@Ad+*IG=*sODMgj7b9@!!(gjxW88q0o5TdIA*PQ-;$nxXO>{Pv0aZtqa zgd)WLgrA1n_4ZI&CFle+oj(Aw1Hia`Siut3+kCd diff --git a/res/gamedata/shaders/r5/bindless_forward.vs b/res/gamedata/shaders/r5/bindless_forward.vs index a3d68da597e..d4e17910fe0 100644 --- a/res/gamedata/shaders/r5/bindless_forward.vs +++ b/res/gamedata/shaders/r5/bindless_forward.vs @@ -42,6 +42,8 @@ struct VS_OUTPUT float3 tangent : TEXCOORD3; float3 bitangent: TEXCOORD4; nointerpolation uint materialID : TEXCOORD5; // Direct material ID (no indirection) + float hemi : TEXCOORD6; // Vertex hemisphere (normal.a) + float2 lmUV : TEXCOORD7; // Lightmap UV (texcoord1) }; // ═══════════════════════════════════════════════════════ @@ -100,9 +102,13 @@ VS_OUTPUT main(VS_INPUT input) // UVs are pre-unpacked in UnifiedVertex format - pass through directly output.texcoord = input.texcoord; + output.lmUV = input.texcoord1; // Pass material ID to pixel shader output.materialID = materialID; + // Packed normal.a stores hemisphere occlusion (BGRA8_UNORM) + output.hemi = input.normal.a; + return output; } diff --git a/res/gamedata/shaders/r5/bindless_particle.ps b/res/gamedata/shaders/r5/bindless_particle.ps index d67d948cc0d9dbc09fa316b2b994f364d102edf0..608e8ec98a02e11aa44226c9dcadfcb7ee3a8074 100644 GIT binary patch delta 380 zcmZXQ!AiqG5Qf1dmm=N;l>rZ?ha@GXcqodrR#DW73LeY2o!Eu#ChTr0C?cLc?i=_7 z9z6OSBKRcY#cXP!;4-i<^UePc|I6~n%InY4W0F8upw^thW$y|mnlzxI4AMrU4V{zT z+5W642~~7QDhKTkeR gv)c3f_StXQcYo7<`5n6&Y}jV77C5N~dmMcJ0*94#6aWAK delta 94 zcmV-k0HOcZ4x$XO`vM|kY-w;Pa%FRMY;-PRAT1y$FfK4LATJ;?H8n0UDJeS&3X|aj zO_MSN6ayf83X@d?HpF diff --git a/res/gamedata/shaders/r5/bindless_skinned.ps b/res/gamedata/shaders/r5/bindless_skinned.ps index d92e44f9fc82f4e2f2a39211681b3cd9ea850a2d..61da7f6e6c37b20c1f07ad3137ee51540fafbdf9 100644 GIT binary patch delta 130 zcmca0d{|_I0Ha$nfH2 delta 20 ccmX>sazS{50ORHe#wPa3|2a}7XL8;G08wfO?f?J) diff --git a/res/gamedata/shaders/r5/bindless_skinned_hud.ps b/res/gamedata/shaders/r5/bindless_skinned_hud.ps index e91d8e1948e6067a37961dcf71850b40add758da..2a27890b44818dc030e5c2ac0bd90911c9391aef 100644 GIT binary patch delta 328 zcmZ{eu};G<6h+slD`MtJED37eR#m5}LugbLH4sz_DyGQT&m_pMqc|ZWzkt*)fgj-` zy7nV*Lf5|W-luc!&&BcbYr+J|e21{0_bFY4QL;^!$z~gFqTcnXT0KSQsyJDM@wt+& zU(&nV_oA?jGZ2#fu&wlq1%zEH%I0TAH4s6DRX+V}n zgFumUR-phl2{bFfUnb83Cf;k?rnX=lU0(4Ft<%^l=veaKUJIHzJ!QI3=m@k!`}wY@ sE7F)rsl=nPC%jbW?DG@y6;5_Q@O^DU*vhZUF#B1_u=Y diff --git a/res/gamedata/shaders/r5/bindless_skinned_tess.ds b/res/gamedata/shaders/r5/bindless_skinned_tess.ds new file mode 100644 index 00000000000..4d479ab76e5 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_tess.ds @@ -0,0 +1,113 @@ +// bindless_skinned_tess.ds — Domain for skinned PatchList (PN + optional HM) +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" + +struct HS_CONTROL_POINT +{ + float3 worldPos : TEXCOORD0; + float2 texcoord : TEXCOORD1; + float3 normal : TEXCOORD2; + float3 tangent : TEXCOORD3; + float3 bitangent : TEXCOORD4; + nointerpolation uint materialID : TEXCOORD5; +}; + +struct HS_CONSTANT_DATA +{ + float Edges[3] : SV_TessFactor; + float Inside : SV_InsideTessFactor; +}; + +struct DS_OUTPUT +{ + float4 position : SV_Position; + float3 worldPos : TEXCOORD0; + float2 texcoord : TEXCOORD1; + float3 normal : TEXCOORD2; + float3 tangent : TEXCOORD3; + float3 bitangent : TEXCOORD4; + nointerpolation uint materialID : TEXCOORD5; +}; + +[domain("tri")] +[partitioning("integer")] +DS_OUTPUT main(HS_CONSTANT_DATA input, float3 bary : SV_DomainLocation, + const OutputPatch patch) +{ + DS_OUTPUT o; + + float u = bary.x; + float v = bary.y; + float w = bary.z; + + MaterialData mat = g_Materials[patch[0].materialID]; + bool usePN = (mat.tessMethod == TESS_METHOD_PN || mat.tessMethod == TESS_METHOD_PN_HM); + + float3 pos; + float3 N; + if (usePN) + { + float3 b300 = patch[0].worldPos; + float3 b030 = patch[1].worldPos; + float3 b003 = patch[2].worldPos; + float3 n200 = normalize(patch[0].normal); + float3 n020 = normalize(patch[1].normal); + float3 n002 = normalize(patch[2].normal); + + float3 b210 = (2.0 * b300 + b030 - dot(b030 - b300, n200) * n200) / 3.0; + float3 b120 = (2.0 * b030 + b300 - dot(b300 - b030, n020) * n020) / 3.0; + float3 b021 = (2.0 * b030 + b003 - dot(b003 - b030, n020) * n020) / 3.0; + float3 b012 = (2.0 * b003 + b030 - dot(b030 - b003, n002) * n002) / 3.0; + float3 b102 = (2.0 * b003 + b300 - dot(b300 - b003, n002) * n002) / 3.0; + float3 b201 = (2.0 * b300 + b003 - dot(b003 - b300, n200) * n200) / 3.0; + + float3 E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0; + float3 V = (b300 + b030 + b003) / 3.0; + float3 b111 = E + (E - V) * 0.5; + + float v12 = 2.0 * dot(b030 - b300, n200 + n020) / max(dot(b030 - b300, b030 - b300), 1e-8); + float v23 = 2.0 * dot(b003 - b030, n020 + n002) / max(dot(b003 - b030, b003 - b030), 1e-8); + float v31 = 2.0 * dot(b300 - b003, n002 + n200) / max(dot(b300 - b003, b300 - b003), 1e-8); + float3 n110 = normalize(n200 + n020 - v12 * (b030 - b300)); + float3 n011 = normalize(n020 + n002 - v23 * (b003 - b030)); + float3 n101 = normalize(n002 + n200 - v31 * (b300 - b003)); + + float uu = u * u, vv = v * v, ww = w * w; + pos = b300 * (uu * u) + b030 * (vv * v) + b003 * (ww * w) + + b210 * (3.0 * uu * v) + b120 * (3.0 * u * vv) + + b201 * (3.0 * uu * w) + b102 * (3.0 * u * ww) + + b021 * (3.0 * vv * w) + b012 * (3.0 * v * ww) + + b111 * (6.0 * u * v * w); + + N = n200 * uu + n020 * vv + n002 * ww + + n110 * (u * v) + n011 * (v * w) + n101 * (w * u); + N = normalize(N); + } + else + { + pos = patch[0].worldPos * u + patch[1].worldPos * v + patch[2].worldPos * w; + N = normalize(patch[0].normal * u + patch[1].normal * v + patch[2].normal * w); + } + + float2 tc = patch[0].texcoord * u + patch[1].texcoord * v + patch[2].texcoord * w; + float3 T = normalize(patch[0].tangent * u + patch[1].tangent * v + patch[2].tangent * w); + float3 B = normalize(patch[0].bitangent * u + patch[1].bitangent * v + patch[2].bitangent * w); + + bool useHM = (mat.tessMethod == TESS_METHOD_HM || mat.tessMethod == TESS_METHOD_PN_HM); + if (useHM && ((mat.flags & MAT_FLAG_HAS_NORMAL_X) != 0) && mat.normalXIndex != INVALID_TEXTURE_INDEX) + { + float height = GetBindlessTexture(mat.normalXIndex).SampleLevel(smp_linear, tc, 0).a; + float hmScale = (parallax.x > 0.0) ? (parallax.x * 2.0) : 0.04; + pos += N * ((height - 0.5) * hmScale); + } + + o.worldPos = pos; + o.position = mul(m_VP, float4(pos, 1.0)); + o.texcoord = tc; + o.normal = N; + o.tangent = T; + o.bitangent = B; + o.materialID = patch[0].materialID; + return o; +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_tess.hs b/res/gamedata/shaders/r5/bindless_skinned_tess.hs new file mode 100644 index 00000000000..82c9e911531 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_tess.hs @@ -0,0 +1,83 @@ +// bindless_skinned_tess.hs — Hull for skinned PatchList (NPC / mutant tessellation) +// Matches bindless_skinned*.vs IO (no hemi). Same MoltenVK rules as bindless_tess.hs: +// do not put user varyings in the patch-constant struct. +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" + +struct VS_CONTROL_POINT +{ + float4 position : SV_Position; + float3 worldPos : TEXCOORD0; + float2 texcoord : TEXCOORD1; + float3 normal : TEXCOORD2; + float3 tangent : TEXCOORD3; + float3 bitangent : TEXCOORD4; + nointerpolation uint materialID : TEXCOORD5; +}; + +struct HS_CONTROL_POINT +{ + float3 worldPos : TEXCOORD0; + float2 texcoord : TEXCOORD1; + float3 normal : TEXCOORD2; + float3 tangent : TEXCOORD3; + float3 bitangent : TEXCOORD4; + nointerpolation uint materialID : TEXCOORD5; +}; + +struct HS_CONSTANT_DATA +{ + float Edges[3] : SV_TessFactor; + float Inside : SV_InsideTessFactor; +}; + +float ComputeEdgeFactor(float3 p0, float3 p1) +{ + float3 mid = 0.5 * (p0 + p1); + float dist = max(length(mid - eye_position), 0.5); + float edgeLen = length(p0 - p1); + float pixels = (edgeLen / dist) * 720.0; + float f = saturate((pixels - 4.0) / 20.0); + float distFade = saturate(1.0 - (dist - 8.0) / 25.0); + return max(1.0, round(lerp(1.0, 4.0, f * f * distFade))); +} + +HS_CONSTANT_DATA PatchConstant(InputPatch patch) +{ + HS_CONSTANT_DATA o; + MaterialData mat = g_Materials[patch[0].materialID]; + bool doTess = (mat.tessMethod != TESS_METHOD_OFF) && + ((mat.flags & MAT_FLAG_WATER) == 0); + + float f0 = 1.0, f1 = 1.0, f2 = 1.0; + if (doTess) + { + f0 = ComputeEdgeFactor(patch[1].worldPos, patch[2].worldPos); + f1 = ComputeEdgeFactor(patch[2].worldPos, patch[0].worldPos); + f2 = ComputeEdgeFactor(patch[0].worldPos, patch[1].worldPos); + } + o.Edges[0] = f0; + o.Edges[1] = f1; + o.Edges[2] = f2; + o.Inside = max(1.0, round((f0 + f1 + f2) / 3.0)); + return o; +} + +[domain("tri")] +[partitioning("integer")] +[outputtopology("triangle_cw")] +[outputcontrolpoints(3)] +[patchconstantfunc("PatchConstant")] +[maxtessfactor(4.0)] +HS_CONTROL_POINT main(InputPatch patch, uint id : SV_OutputControlPointID) +{ + HS_CONTROL_POINT o; + o.worldPos = patch[id].worldPos; + o.texcoord = patch[id].texcoord; + o.normal = patch[id].normal; + o.tangent = patch[id].tangent; + o.bitangent = patch[id].bitangent; + o.materialID = patch[id].materialID; + return o; +} diff --git a/res/gamedata/shaders/r5/bindless_terrain.ps b/res/gamedata/shaders/r5/bindless_terrain.ps index 29ef3e27ee81829471587df34a2eddd496d39211..9051597841ebc643dba95789e983028fd283bf6b 100644 GIT binary patch literal 9569 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 F{{kOQT?POE literal 7194 zcmdT}ZExE~4F2w4LA1c2bdHkPZa=i}0J(NuV~!lyPS;@=gtK*agvpXC$$4q9|FAFn zgZm{Tk9T^poTeBspbi2j5_vox@8Lt<`Roi9LF9)rO}$Jei3lQln>NnQV0QTmdRhcp z1l!ro099W=pXe3>qjV4I;;^fFk=w9$C(%T*9b^xb>q zo#&5l$Jev@U^?h~8Ce^N*nItB=k{&D)Yg|x0A`V ze_m2((E^uoocJY$i;_YcqBz-zuq19(#F>cJGRjKwc16Aj>KYx^h~gl^uy5l~s8#In z5jT%_V$?60eE9|dw|Er!@*d-##jy7Fb}nuF`KD0{*W`L$i`Oy^asO1X<|0|ktQ?9? zu~G~s%gd4Iz(S<*?Y@a}G&<#Q=SDCd~z!-ZHfQ3TPg2m_x|Ti!QxcJXKpvpMsCr7)`0nw*6# z*xjD!v|O%E5r&JQRS^nQsf|xT3g3lN6l5-@&5#^jX^u7py7v#*MEwN-fMS< z0sLBM%ACp(F&swhXjA;%oh{%6wCr;@C66i!53I0wRN*r!q@R+^N(BM~Nzg(QXox4n z30@Tc=5HU0w+|=PeKtJdULi+JOuCU3V?h^?NTY}Pt zbCuWQyHz%u>l#4YPNp4Yc|sff249C2 zzvcz?zn9qV?mI!Lt8zn`nC&*08-AQw)Eeu;)v+#+IQiFb2Nf_GSUJZ;N) z#Fhzbk22dZgNkViiwDjeonYnIsZT_ZTMh3-lcddhs`d`0(^Vgy3Y^YL@?R%2G8o;NZyJl2t!e#;I^Uct|CG<*fH!X?6p-| zc91l+>>z7uS(!F--whHFcZEN8!l0yqIXtQw>q0|;wlr9`(|9{X`6ZnnG>dwdg;_Ei^(nDNXd}YxDx2Z3*%(pnroUxk4o;N|nM$vpDq{|cAw)sq^ zOJc{udHEHkh`Br^7-)5_ljs^L@&f}&%Zu+pSS5e1Xc?ci9rMUK`ZAgg(#7+?Msj&d diff --git a/res/gamedata/shaders/r5/bindless_tess.ds b/res/gamedata/shaders/r5/bindless_tess.ds new file mode 100644 index 00000000000..c1185babca9 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_tess.ds @@ -0,0 +1,125 @@ +// bindless_tess.ds — Domain shader (PN interpolate + heightmap displace) +// +// PN Bezier / normal coefficients are derived here from the 3 control points +// (same formulas as the classic hull patch-constant path). Avoids Slang's +// missing Patch decoration on user patch-constant varyings. +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" + +struct HS_CONTROL_POINT +{ + float3 worldPos : TEXCOORD0; + float2 texcoord : TEXCOORD1; + float3 normal : TEXCOORD2; + float3 tangent : TEXCOORD3; + float3 bitangent : TEXCOORD4; + nointerpolation uint materialID : TEXCOORD5; + float hemi : TEXCOORD6; +}; + +struct HS_CONSTANT_DATA +{ + float Edges[3] : SV_TessFactor; + float Inside : SV_InsideTessFactor; +}; + +struct DS_OUTPUT +{ + float4 position : SV_Position; + float3 worldPos : TEXCOORD0; + float2 texcoord : TEXCOORD1; + float3 normal : TEXCOORD2; + float3 tangent : TEXCOORD3; + float3 bitangent : TEXCOORD4; + nointerpolation uint materialID : TEXCOORD5; + float hemi : TEXCOORD6; +}; + +[domain("tri")] +[partitioning("integer")] +DS_OUTPUT main(HS_CONSTANT_DATA input, float3 bary : SV_DomainLocation, + const OutputPatch patch) +{ + DS_OUTPUT o; + + float u = bary.x; + float v = bary.y; + float w = bary.z; + + uint matId = patch[0].materialID; + MaterialData mat = g_Materials[matId]; + bool usePN = (mat.tessMethod == TESS_METHOD_PN || mat.tessMethod == TESS_METHOD_PN_HM); + + float3 pos; + float3 N; + if (usePN) + { + float3 b300 = patch[0].worldPos; + float3 b030 = patch[1].worldPos; + float3 b003 = patch[2].worldPos; + float3 n200 = normalize(patch[0].normal); + float3 n020 = normalize(patch[1].normal); + float3 n002 = normalize(patch[2].normal); + + float3 b210 = (2.0 * b300 + b030 - dot(b030 - b300, n200) * n200) / 3.0; + float3 b120 = (2.0 * b030 + b300 - dot(b300 - b030, n020) * n020) / 3.0; + float3 b021 = (2.0 * b030 + b003 - dot(b003 - b030, n020) * n020) / 3.0; + float3 b012 = (2.0 * b003 + b030 - dot(b030 - b003, n002) * n002) / 3.0; + float3 b102 = (2.0 * b003 + b300 - dot(b300 - b003, n002) * n002) / 3.0; + float3 b201 = (2.0 * b300 + b003 - dot(b003 - b300, n200) * n200) / 3.0; + + float3 E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0; + float3 V = (b300 + b030 + b003) / 3.0; + float3 b111 = E + (E - V) * 0.5; + + float v12 = 2.0 * dot(b030 - b300, n200 + n020) / max(dot(b030 - b300, b030 - b300), 1e-8); + float v23 = 2.0 * dot(b003 - b030, n020 + n002) / max(dot(b003 - b030, b003 - b030), 1e-8); + float v31 = 2.0 * dot(b300 - b003, n002 + n200) / max(dot(b300 - b003, b300 - b003), 1e-8); + float3 n110 = normalize(n200 + n020 - v12 * (b030 - b300)); + float3 n011 = normalize(n020 + n002 - v23 * (b003 - b030)); + float3 n101 = normalize(n002 + n200 - v31 * (b300 - b003)); + + float uu = u * u, vv = v * v, ww = w * w; + pos = b300 * (uu * u) + b030 * (vv * v) + b003 * (ww * w) + + b210 * (3.0 * uu * v) + b120 * (3.0 * u * vv) + + b201 * (3.0 * uu * w) + b102 * (3.0 * u * ww) + + b021 * (3.0 * vv * w) + b012 * (3.0 * v * ww) + + b111 * (6.0 * u * v * w); + + N = n200 * uu + n020 * vv + n002 * ww + + n110 * (u * v) + n011 * (v * w) + n101 * (w * u); + N = normalize(N); + } + else + { + pos = patch[0].worldPos * u + patch[1].worldPos * v + patch[2].worldPos * w; + N = normalize(patch[0].normal * u + patch[1].normal * v + patch[2].normal * w); + } + + float2 tc = patch[0].texcoord * u + patch[1].texcoord * v + patch[2].texcoord * w; + float3 T = normalize(patch[0].tangent * u + patch[1].tangent * v + patch[2].tangent * w); + float3 B = normalize(patch[0].bitangent * u + patch[1].bitangent * v + patch[2].bitangent * w); + float hemi = patch[0].hemi * u + patch[1].hemi * v + patch[2].hemi * w; + + bool useHM = (mat.tessMethod == TESS_METHOD_HM || mat.tessMethod == TESS_METHOD_PN_HM); + if (useHM && ((mat.flags & MAT_FLAG_HAS_NORMAL_X) != 0) && mat.normalXIndex != INVALID_TEXTURE_INDEX) + { + float height = GetBindlessTexture(mat.normalXIndex).SampleLevel(smp_linear, tc, 0).a; + // Scale from r2_parallax_h (parallax.x); fallback 0.04m so HM is visible + // even when steep-parallax is off. Classic DX11 used a similar ~cm range. + float hmScale = (parallax.x > 0.0) ? (parallax.x * 2.0) : 0.04; + float displace = (height - 0.5) * hmScale; + pos += N * displace; + } + + o.worldPos = pos; + o.position = mul(m_VP, float4(pos, 1.0)); + o.texcoord = tc; + o.normal = N; + o.tangent = T; + o.bitangent = B; + o.materialID = matId; + o.hemi = hemi; + return o; +} diff --git a/res/gamedata/shaders/r5/bindless_tess.hs b/res/gamedata/shaders/r5/bindless_tess.hs new file mode 100644 index 00000000000..74da43dbab7 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_tess.hs @@ -0,0 +1,99 @@ +// bindless_tess.hs — Hull shader (screen-space + distance LOD) +// +// IMPORTANT (Slang → SPIR-V / MoltenVK): +// Do NOT put user varyings in the patch-constant struct. Slang does not emit +// the SPIR-V Patch decoration on them, so HS/DS locations diverge and +// createGraphicsPipelines fails with VK_ERROR_INITIALIZATION_FAILED. +// PN control points are recomputed in the domain shader from the 3 CPs. +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" + +struct VS_CONTROL_POINT +{ + float4 position : SV_Position; + float3 worldPos : TEXCOORD0; + float2 texcoord : TEXCOORD1; + float3 normal : TEXCOORD2; + float3 tangent : TEXCOORD3; + float3 bitangent : TEXCOORD4; + nointerpolation uint materialID : TEXCOORD5; + float hemi : TEXCOORD6; +}; + +struct HS_CONTROL_POINT +{ + float3 worldPos : TEXCOORD0; + float2 texcoord : TEXCOORD1; + float3 normal : TEXCOORD2; + float3 tangent : TEXCOORD3; + float3 bitangent : TEXCOORD4; + nointerpolation uint materialID : TEXCOORD5; + float hemi : TEXCOORD6; +}; + +struct HS_CONSTANT_DATA +{ + float Edges[3] : SV_TessFactor; + float Inside : SV_InsideTessFactor; +}; + +// Screen-space edge length drives factor; capped for Metal tess temp buffers. +float ComputeEdgeFactor(float3 p0, float3 p1) +{ + float3 mid = 0.5 * (p0 + p1); + float dist = max(length(mid - eye_position), 0.5); + + float edgeLen = length(p0 - p1); + float pixels = (edgeLen / dist) * 720.0; + + float f = saturate((pixels - 4.0) / 20.0); + float distFade = saturate(1.0 - (dist - 8.0) / 25.0); + // integer partitioning — emit whole factors only + return max(1.0, round(lerp(1.0, 4.0, f * f * distFade))); +} + +HS_CONSTANT_DATA PatchConstant(InputPatch patch) +{ + HS_CONSTANT_DATA o; + + uint matId = patch[0].materialID; + MaterialData mat = g_Materials[matId]; + // Classic: only model mats with TessMethod; never terrain (BmmD TessMethod=0) + bool doTess = (mat.tessMethod != TESS_METHOD_OFF) && + ((mat.flags & MAT_FLAG_WATER) == 0) && + ((mat.flags & MAT_FLAG_TERRAIN) == 0); + + float f0 = 1.0, f1 = 1.0, f2 = 1.0; + if (doTess) + { + f0 = ComputeEdgeFactor(patch[1].worldPos, patch[2].worldPos); + f1 = ComputeEdgeFactor(patch[2].worldPos, patch[0].worldPos); + f2 = ComputeEdgeFactor(patch[0].worldPos, patch[1].worldPos); + } + + o.Edges[0] = f0; + o.Edges[1] = f1; + o.Edges[2] = f2; + o.Inside = max(1.0, round((f0 + f1 + f2) / 3.0)); + return o; +} + +[domain("tri")] +[partitioning("integer")] +[outputtopology("triangle_cw")] +[outputcontrolpoints(3)] +[patchconstantfunc("PatchConstant")] +[maxtessfactor(4.0)] +HS_CONTROL_POINT main(InputPatch patch, uint id : SV_OutputControlPointID) +{ + HS_CONTROL_POINT o; + o.worldPos = patch[id].worldPos; + o.texcoord = patch[id].texcoord; + o.normal = patch[id].normal; + o.tangent = patch[id].tangent; + o.bitangent = patch[id].bitangent; + o.materialID = patch[id].materialID; + o.hemi = patch[id].hemi; + return o; +} diff --git a/res/gamedata/shaders/r5/bloom/composite.ps b/res/gamedata/shaders/r5/bloom/composite.ps new file mode 100644 index 0000000000000000000000000000000000000000..788b994c20fff3af133d67f5fd461b79db0f736c GIT binary patch literal 607 zcmYk4!ET#C5QgtO#V_?xXe^*f)I*Uktt-`1+gQPkdP55gcvaX%yMxmxiqwbb6ZT0u zfSVX0q@DTapP6q-5|o{_eNx$euwFC8gU8?Bf0i&RYuM>>WNi(s=^haJiVQ(}$qkz7 z75G3#J`#~v)iu>o@T#Upi!A?IyeraRPrrC{bh$xWe3<&-Jsh>#Gr5q{C<}7c4;{Ij z6%%|v6plMFQ0`pmCuLBTW7CksY_wBO_5NH`rcv-1K%SBq;Lo7+vAVX~QTUnm5j4O8pWn#L!FGg3Fw)$9{vWVD^-mDKjZ<7K Z$J2z4WNLHb6fOmCUO=xPB_e$a{sEoB#TNho literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/bloom/downsample.ps b/res/gamedata/shaders/r5/bloom/downsample.ps new file mode 100644 index 0000000000000000000000000000000000000000..b0eeffb5a411a72c901ff325a9d095d981df50e1 GIT binary patch literal 1191 zcmcJP-%i3X6vprO6esa!6Wq!~E?l?~ydeHzanpFWlyO*=Z0pi?10^Irgiq*`*fJ2f zF)?5-nl#_(uiyEa)^T8xF_t@1HZKIt%M9&Oz}Lqo3@e(!9i3Bwuv3k*DbCUYA$W*- z5ivqs$cm#ogd`fUjB&UIj!7yca#Iq^GlDgh2a-wzk(UvdnF7#*N}(%_cv8(~$l*p? z@+qge*vCzXWh@O)W0o;0U68oIEJ)wc1IK}mBY3P=`z29Hr=GD=a3OgWOYnne^yoi_ zuxuz@D49Mbc!_)_Qkk;C+w!Yg3E}PQU_5>r5)U+HeRLOcnil2{Yh*Qqkn#lOp-Bbl zgPkdh(5WVv&M(PDzyimvLp;j)9E7j^ptl(BrE@;66MgBZQhr2!VShQ6oe@;*W9Y?1*+-l}kd;d2)chtQ1@?K_4WlgDCea2m(04<=c%9 z5A>@bZv~{a+?>ti_rDdCH*hcZ@=l0hQo(9QAn3B=&cq>$)3sJB7mj$#( zdf0ury}G`>S%)hyGS)}Mi`pHvHeRo=<1$A*b=--ND^8J78t#U*N9ud*=mJtQZG&We zaNqHWr8~{dCo4XL9+P=8A=5aCT=A2eGU?kx?nBBFZ(77;WPxLT?Bv4-oL`*by6}3% zH{ip6fF|G}?3t0Dx-u$vf)?fy(vnueC7bik!+SfKkcB=hSO~N3!g`qS-0?i?CTiq7 z2)Y)RBd;^IJnJA8wU$LG65J@O6YJVV0r1{i#BixjHSs_9(v#8I`Kj$wB AAOHXW literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/bloom/upsample.ps b/res/gamedata/shaders/r5/bloom/upsample.ps new file mode 100644 index 0000000000000000000000000000000000000000..1f8b7aef3594f74d4a2b620c582f65d9733f7994 GIT binary patch literal 1336 zcmb`H&5qMB6ovPBiX*W}rkx}$)hwvGfg({cFcm3?UF4?DkCeob?KFkaNb?XpAy2|} z-O_Z!Vn`N|qU-zJbB^r<0k)+ORnRtyRZYqLM&bAKFDzSD;v?&r;yBkZ0!xy%YD=a$ zxd!swS4YB!Nu!v5KTP zPO@#&lzhB3S*)34mFh3rhMx22fS$AzOi!WtftOM7lOJtN0wojk1(zHdKVE-v+_2r; zd7?2&%Qn$iNAc?O`s)UdmN!Eq%)IDZycVj^g{bG}N$s8roA=+}Jv@9_hI1IqQN#=t znRNfMR-Pqnn9R7omP?QY6r=s1DBP9b?#Xn|j-c|z(xvhm2~|S7Ho<6N_tLk?-t#># z-F~oR(4^5xx_c}9!x;4b;f)b`zRb2SB#j0mSxv+nNYfA`qX{>dnvl*xnQ#B4!JD`S z1%`GT4RaWU1T@M0FR+)^?2^{a0`9~Q5nyuXo9k^(`swtVzsf(4?-^;kB&R!5pKhs} Lj6qxiJ~@8?g+7^% literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/cas.ps b/res/gamedata/shaders/r5/cas.ps new file mode 100644 index 0000000000000000000000000000000000000000..df5779a6aa1921f4efc1e938bc0708a4d02dcb2b GIT binary patch literal 1302 zcmb7^+iKfD5Qgvd6a&4HYFUykx0H%r1iJ=9(wN9?b0=2P*b&vKyX!+541I_`VV|V4 zIvY|*d^3x7{%;PRS=)w)39A(F_xB&Tx>>+7i7`#&yaQFxucZrb5b?F$M=CiC!k^7QK1d)|NStPj}x(Vf` zMd5zCMXo}w8E2Uox`d9QJ!wF{ZCWrn1rc$?+)sX?*MEjO%^ZdBz_!7$tjX{;C|T@j zX9^}H-$xSGez5wo{ zCYDL6DsHmdAIw%J@r3C`@-8s=)V9m2+jETvO!cliCQ!6dgU}rl#udhTG)!;|i|@ig z)h>ju<2A68Vm-@?Uc!4;8NWo?S2n84`hO<)Iv#bZK@L0BkAMqVPQ)z9b!JvZ9YYf$ zYJ@t*CZJ)^ICi5(l9Q6KQzapzj;(}_on)w?nJ1B8v>P<`x6?~Bz#q8-UCZY*moHa1dOc5tGcn@yTg zQ>atjR<4YC7dMRWP+mR&)#P-EssE&Z658d{C)D1yw@sSWhR*bUWmlHk)_P6J-BORy GS^Ec26oY>N literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/common_functions.h b/res/gamedata/shaders/r5/common_functions.h index 81477d0fb06..c742157cf40 100644 --- a/res/gamedata/shaders/r5/common_functions.h +++ b/res/gamedata/shaders/r5/common_functions.h @@ -114,6 +114,13 @@ float3 v_hemi(float3 n) return L_hemi_color.rgb*(.5f + .5f*n.y); } +// Scalar hemi occlusion for skinned/dynamic meshes without baked vertex hemi. +// Same wrap as classic v_hemi; fed into output_forward_pbr ambient term. +float calc_model_hemi(float3 norm_w) +{ + return saturate(0.5f + 0.5f * normalize(norm_w).y); +} + float3 v_sun(float3 n) { return L_sun_color*dot(n,-L_sun_dir_w); @@ -218,6 +225,11 @@ float gbuf_unpack_mtl( float mtl_hemi ) } #include "shared/pbr_brdf.h" +#if defined(SKY_IBL) +#include "shared/sky_ibl.h" +#endif +#include "shared/foliage_sss.h" +#include "shared/shadow_sampling.h" #include "shared/clustered_lighting.h" float3 worldNormalToView(float3 N) @@ -242,29 +254,74 @@ f_forward output_forward_pbr( float metallic, float roughness, float ao, - float4 svPosition = float4(0, 0, 0, 0)) + float4 svPosition = float4(0, 0, 0, 0), + float hemi = 1.0, + float sunOcclusion = 1.0, + float sssStrength = 0.0, + float3 sssTint = float3(1, 1, 1), + float sssThickness = 1.0) { f_forward res; float3 N = normalize(worldNormal); float3 V = normalize(eye_position - worldPos); float3 L = normalize(-L_sun_dir_w); + float hemiTerm = saturate(hemi); + float sunTerm = saturate(sunOcclusion); + +#if defined(CSM_CHEAP_FORWARD) + float csm = SampleCSM_Fast(worldPos, N); +#else + float csm = SampleCSM(worldPos, N); +#endif + // Soft ambient fill in umbra — full-black CSM reads too heavy indoors / under trees. + csm = lerp(0.20, 1.0, saturate(csm)); + float contact = ContactShadow(worldPos, L); + float sunShadow = csm * contact * sunTerm; float3 sunLight = PBRDirectLighting( albedo, N, V, L, L_sun_color, metallic, roughness, (uint)pbr_diffuse_mode - ); + ) * sunShadow; - float3 ambientColor = L_ambient.rgb + L_hemi_color.rgb * L_hemi_color.w; - float3 ambient = PBRAmbient( - albedo, N, V, - metallic, roughness, ao, - ambientColor - ); + float3 ambient; +#if defined(SKY_IBL) + float iblIntensity = parallax.w; // packed: r_sky_ibl ? intensity : 0 + if (iblIntensity > 1e-4) + { + ambient = PBRAmbientIBL( + albedo, N, V, + metallic, roughness, ao, + hemiTerm, iblIntensity); + } + else +#endif + { + float3 ambientColor = (L_ambient.rgb + L_hemi_color.rgb * L_hemi_color.w) * hemiTerm; + ambient = PBRAmbient( + albedo, N, V, + metallic, roughness, ao, + ambientColor + ); + } float3 finalColor = sunLight + ambient; + if (sssStrength > 1e-4) + { + finalColor += EvaluateFoliageSSS( + albedo, N, V, L, L_sun_color, sunShadow, + sssTint, sssThickness, sssStrength); +#if defined(SKY_IBL) + if (parallax.w > 1e-4) + { + finalColor += EvaluateFoliageSkySSS( + albedo, N, sssTint, sssThickness, sssStrength * parallax.w); + } +#endif + } + #ifdef CLUSTERED_LIGHTING_FORWARD if (svPosition.w != 0) { @@ -276,6 +333,17 @@ f_forward output_forward_pbr( } #endif + { + float dist = length(worldPos - eye_position); + float fog = saturate(dist * fog_params.w + fog_params.x); + finalColor = lerp(finalColor, fog_color.rgb, fog); + } + + // Shadow diagnostics: r_shadow_debug (dev_param_3.z) overrides scene color. + int shadowDebug = (int)(dev_param_3.z + 0.5); + if (shadowDebug > 0) + finalColor = ShadowDebugColor(worldPos, N, L, shadowDebug); + res.color = float4(finalColor, 1.0); res.normal = float4(N, roughness); res.baseColor = float4(albedo, metallic); diff --git a/res/gamedata/shaders/r5/contact_shadows.ps b/res/gamedata/shaders/r5/contact_shadows.ps new file mode 100644 index 0000000000000000000000000000000000000000..462b098c34e0e184ae10fe472df9758a45862c86 GIT binary patch literal 4963 zcma)A>u%dt68`U}n6^McMG`HE($cK#NB>>c6MuyB_l>4V^liZiv z-s#patbLbfU!bli^L9b0mQT10H@Wvy?2z{Tyazy0#= z-9ML;OJXr%3SIfdb~I`KF}>=np{uP97Q zC-X-5%{_fOB7L6cdY&c84HA;5EKs?8&x9-s-=9GCrMBs=30HELZN z(OsIx6ctzduxB^LsH|6yqZw_;;ft5AXllmufmqJgThpfQ1X1AwjZB7AKnhrp5U18p zV=^bRMyMsi);r}`{vE}h=3pv(cf?xKsXyA zypzSK6(dqO=#$x?rxNV9+bDQwxo6?O);uP!y@D!uJfi8WON?y5Va=*mBR%W5 z)7Y^XT+;bD`G+(LDKUON4*Z?TeY%U&YM*HwIpQ`u9M;cm#5+#ICsoHJR;5H;ip*Px zp33-Hnv9FUk4;xf?YrcT1)zrb3&2y96eR$U(g1ak@|kLpsC#scqa@9DevBS;#@B0r z&pM3qb)}A#Y31i3;k2y?PlSCi0eG)ronx&%*Mf0GRX;sR8WjfQw#(3l&uBV<%Zzgo z0#>-kV|~`m6j~2#P0_@^#QaKzp4kCVX zvn|lpa&vk_^2iE5Iblp45HRF$K!5x}^~q~BS^~^A@yFWLoQW-{@oEXf>|k? zqk~^6kee~^q;W*Zag8Cc#9RiC#o+Ag;M@4e8u}cp@~eQ|@`TZ5!yYy2#XT43pbiv@*#{$TPEneg6Fx}lP1LSl-73LpWNCYDftG`<|yc1($3PSpDkWH}`J8b^oeu z>&1w1YVJ(Y@%j&X#Wztrqh9C!S>rzGTVY1e^73@WRW4xrEqyDTUOR#8FuLkCuSo4Q zu-J<_y?VcVK?j3Ta;>)^GviZ^arIq)LHp;loTUocmy5|uf&X_Ix5;H zQ~d=iEgxmU4buhDO&XV5J`fdlVin!ul=R zVz7Y3nEPA`XFk?evh9RCl z_`(}PP`BIvoh%CnKRIf%SYPctc2fZ*&e9aPi!Xi+wa&90$Pw7}k4Ac9UxgYOEZVrY zD`kV%EUK}~V>@54(bh2K radius) + if (dist > r) return false; } return true; @@ -112,6 +117,14 @@ bool HiZTestSphereTemporal( uint hiZHeight, uint hiZMipLevels) { + // Camera near/inside the bounding volume: large indoor wall batches have huge + // spheres; temporal Hi-Z cannot represent thin walls wrapping the eye. + // 2.5*radius (squared 6.25) — was 1.5 and still popped Skadovsk-class interiors. + float3 toCenter = center - cameraPos; + float r2 = radius * radius; + if (dot(toCenter, toCenter) <= r2 * 6.25) + return true; + // Project sphere center to PREVIOUS frame's clip space (matches Hi-Z data) float4 prevClipPos = mul(prevViewProj, float4(center, 1.0)); @@ -148,8 +161,16 @@ bool HiZTestSphereTemporal( float boxWidth = (boxUV.z - boxUV.x) * float(hiZWidth); float boxHeight = (boxUV.w - boxUV.y) * float(hiZHeight); - // Select mip where box is roughly 2x2 pixels - float mipLevel = floor(log2(max(1.0, max(boxWidth, boxHeight) * 0.5))); + // Large screen footprint (doors/walls filling the view) → never occlusion-cull. + // Sphere Hi-Z is unreliable for near-field interior geometry. + float screenArea = boxWidth * boxHeight; + float fullArea = float(hiZWidth) * float(hiZHeight); + if (screenArea > fullArea * 0.18) + return true; + + // Select mip where box is roughly 2x2 pixels, then bias one level coarser + // so MAX Hi-Z is more conservative (fewer false occlusions). + float mipLevel = floor(log2(max(1.0, max(boxWidth, boxHeight) * 0.5))) - 1.0; mipLevel = clamp(mipLevel, 0.0, float(hiZMipLevels - 1)); // Sample Hi-Z at 4 corners (previous frame's depth) @@ -173,8 +194,11 @@ bool HiZTestSphereTemporal( float frontDepth = frontClip.z / frontClip.w; - // Visible if front of sphere is in front of Hi-Z depth - return frontDepth <= hiZDepth; + // Larger slop for temporal mismatch + thin interior walls + const float depthSlop = 0.0025; + + // Visible if front of sphere is in front of Hi-Z depth (plus slop) + return frontDepth <= hiZDepth + depthSlop; } // Legacy single-viewProj version (for non-temporal Hi-Z) diff --git a/res/gamedata/shaders/r5/detail_billboard.ps b/res/gamedata/shaders/r5/detail_billboard.ps index a74ba24fdf2e7cdf32540e0d7afc45928927f898..798ed7f8afd46bf5e29097f6c0953687784cdcf6 100644 GIT binary patch delta 430 zcmZ`#!AiqG5JiiH1QZV*#Dfg<(rDPW)*u*qh^^9MrP>^d2U(YP+b$-%VRvhL5x+nX z^BJDR|M3I#C)}E<2k|uX=FPmBH}9)YYme3I0Ua_vSrf)Jj3iMC-%`dg;{!VL?W~RyGAO$+50xzJ z`SzXvs&gMpTh=fVM8lZI3~JCqiBL>4!O=9mT6w!Kl^}?cF#)1Lk6HhQQ>A>+EcwEm zQwc*ML1`)Y$bc{m?S6*H%%g!>{A5=~vxf8aUkLw-AFTUPp@|~#xc*ZKLJ{?hu#1H delta 391 zcmZvYy-ve06oo}qDq#sGW^~z-G!&$Yg@}Ql5ml(!iJaOb7EWAUJIYW+yaFO(<0TNU z!xOX{Pk${^?RfB91%y0GMa>j$RJJS(zZO!)iXscc(CMsF zp=2|K8I?YCniNTDZUuG%q`VRCDbma(E3((dAf}5aDGXeKrlvq@xWkljdf;hG>`^Xw za5g!Iar3%C&oHUTbFQ_8)XU-FgVy>gQ$^lTd%g$Tlm`h?ts@(Y7D_6dloo~D*5`jL zOk!KQB?hGW+zd85_4n{=-YsfPTlC*`W9Og|6T07ain0TWO@xobFH+C0Jn^PM|o06oPFoZqCo^VgHXcfFL z7lFg~o$n_Z4ndGZsdy%p$}}3mZHYpEMU92OpI<=LlsIr?9SNB0 visible_indices : register(t0); +StructuredBuffer detail_models : register(t1); +StructuredBuffer pulled_vertices : register(t2); +StructuredBuffer all_instances : register(t3); + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 tc : TEXCOORD0; +}; + +VS_OUTPUT main(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID) +{ + VS_OUTPUT O; + O.position = float4(0, 0, 0, 1); + O.tc = 0; + + uint src_idx = visible_indices[instance_id]; + InstanceData raw = all_instances[src_idx]; + + uint object_id = raw.packed & 0x3F; + float rotation = float((raw.packed >> 8) & 0x3FF) / 1023.0 * TWO_PI; + float scale = float((raw.packed >> 18) & 0x3FF) / 1023.0 * PACK_MAX_SCALE; + + DetailModelGPU mdl = detail_models[object_id]; + if (vertex_id >= mdl.pulledIndexCount) + { + O.position = float4(asfloat(0x7FC00000), asfloat(0x7FC00000), + asfloat(0x7FC00000), asfloat(0x7FC00000)); + return O; + } + + PulledVertex v = pulled_vertices[mdl.pulledVertexBase + vertex_id]; + float3 local_pos = float3(v.px, v.py, v.pz) * scale; + + float c = cos(rotation); + float s = sin(rotation); + float3 rotated; + rotated.x = local_pos.x * c - local_pos.z * s; + rotated.y = local_pos.y; + rotated.z = local_pos.x * s + local_pos.z * c; + + float4 world_pos = float4(rotated + raw.pos, 1.0); + O.position = mul(cb_LightVP, world_pos); + O.tc = float2(v.u, v.v); + return O; +} diff --git a/res/gamedata/shaders/r5/detail_decal.ps b/res/gamedata/shaders/r5/detail_decal.ps index 46b1f8e36b4b385c7baa669a9bfe629da7345f8b..d300161c86b1c24a86554b6ea7d56f75e21606c6 100644 GIT binary patch delta 302 zcmbQmxs-c?v0F-NT4r9Vf^)EMe6WY3i+^~$n}1NaV~`7%GE~aP-`UXzmt?SaWW1-7 z&qTv076l;K*mHu>kh3&1uS6leD6zOWzAQB}FFrFbCAGqu3nCt0keFhiqX41|A(YYP zASMAuDbBQ<{KOK4jMUsr1zQDAy@LGW%#zIfJiYSC3C#DjAhIPHnaSCCsl~-WCB=y) zrA3J)sTw7z6{$ITi3-{Z26{%C){|{mWSF@)IVVT3*t+Wg86YEd6b$tYKpcp>ih*uR c&dpF delta 61 zcmZ3=J&SXK@x visible_indices : register(t33); diff --git a/res/gamedata/shaders/r5/detail_gpu.ps b/res/gamedata/shaders/r5/detail_gpu.ps index 6a7617124a4b7ba181a5a7e3c21f7aefdc33269a..4a1367092ca0a0f150498896e8b09f29f56dd3ed 100644 GIT binary patch delta 988 zcmZ`&-)j>=5XK)^xup6aji#ubP=ut$Yg#Qx&mw7BO);gVS6T$&+{NeqO8vMZ+EQ_4a_ImB!rUyM)5N0-0Jqg+w` zbPDoEZc<*%O=y30eRaFuYOOV332JW5Q&>g4x8zjbF`ne}$FIk)k8PPUDekj<96>qb zm;=j1l8KD2=%Oj$@o2K&!{Zy#{-{v&Xavbc*41JB6mB0wBEnaFIYr2u`DK##FKQ`2tA zDL*A5M=Bl(T&6fp$~AZ4=E9JZsK5*i(T77&ZeF{6SQ)Y${bpv#1{y6iAltC$S5GmWW%->#c2Hd1n>M2?bqCeqH5x%_H>{4fLUB28 F{s670HMjr( delta 1444 zcmZ9M&u`O66vqYPM{7W6C9+zrw&DRoN~qg3?ZSa5LhcVxr9z{moc3T%>~TC|JY&s_ zn-)|>TsUz+vi7(fkT`HemAG<8;xE|0U=REUtJS_4Cp5)}Bz}JHJD>M`&w1Z|7ra^f z_~(s_Io+qc4b!XHgq2RjrKUc#IvI<7SnVW4L0wY=IaLrVGJ=S3 z-tcUZ6O8Gg7&^H~a;fx*9-v^Wqf#~1*vi+v17xihD0hDDx!6H$@{hZ zYyRh(6~{W!6l4MePGSm7gObHCZDzz6>QcYX*P7NR)ZSychLr467?FcRG=Ugw!3T}t zso{s9QzjTyuI}IYs}F2lbz$|c8WO=GKxs$kgTT=#$f&_#u*kFc!7k)P5sXn~Q5%h^ zf+N@@>KVu=;)P~X0GeEJ53mc&URc`O?m`Dz%PXf0N8yJB=Rgl~sSAZx8!#*{|L_{C zr}Q@ph215{Ba;N`T#gX112GRd+^0;03>V?x0n& zXQMr%V(>&^4eZQPqQ?m6cCtxurd5{mrsz-6N{jE_RZHpo6q=dC@=NV{0$S^Bbl62u zx|$&dLszjpnnl2HkjVo7dpgrL5%)XTol{~}p`zHaC|@pIyNapb1=`-x@wcDeo-O~m z_TYk>#qZ@}tzGgO+;SaRZo>1FxJ`-i?O^-_{rGk5%7yV{hA4_WKMwtg4@*0tE<_kJ z6^?u`vu>_I!UW*~AJAA%+n(GlT1 diff --git a/res/gamedata/shaders/r5/detail_gpu.vs b/res/gamedata/shaders/r5/detail_gpu.vs index 907fb7e5f16..d7dae88fbb2 100644 --- a/res/gamedata/shaders/r5/detail_gpu.vs +++ b/res/gamedata/shaders/r5/detail_gpu.vs @@ -42,7 +42,10 @@ cbuffer DetailGlobals : register(b3) float4 grass_sss_color; // RGB + intensity (subsurface scattering) float grass_color_variation; // Per-blade color variation amount float grass_blade_height; // Blade height multiplier (default 1.0) - float _pad0, _pad1; // Padding to 16-byte alignment + uint build_details_index; + uint build_details_pbr_index; + uint grass_vein_index; + uint _pad0, _pad1, _pad2; }; static const float M_PI = 3.1415926; diff --git a/res/gamedata/shaders/r5/detail_gpu_shadow.ps b/res/gamedata/shaders/r5/detail_gpu_shadow.ps new file mode 100644 index 0000000000000000000000000000000000000000..688201b493acdae4659af06728bee8cf5dc996e2 GIT binary patch literal 206 zcmYL>y$ZrG7)AH*DK5C!!5>5%?5GY7+G4A?g=o?msQqG-Dn$ez!YA}e?4WqJ%Q>9E z07`pl3X$7dxRX?T>syES^VI_DPdzS+26-i&L&g}OH5 YnGZvHghW?13TJ7`?k|gF(3T(c0iT^hzW@LL literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/detail_gpu_shadow.vs b/res/gamedata/shaders/r5/detail_gpu_shadow.vs new file mode 100644 index 00000000000..92d5a00e63b --- /dev/null +++ b/res/gamedata/shaders/r5/detail_gpu_shadow.vs @@ -0,0 +1,62 @@ +// detail_gpu_shadow.vs — depth-only grass caster for CSM (LOD0/1) +// Instance packing must match detail_gpu.vs +#define SM_6_0 + +cbuffer ShadowCascadeCB : register(b5) +{ + float4x4 cb_LightVP; +}; + +cbuffer GrassShadowCB : register(b6) +{ + float grass_blade_height; + float pad0, pad1, pad2; +}; + +struct v_blade_sdf +{ + float3 pos : POSITION; + float2 tc : TEXCOORD; + float t : COLOR0; + float width_scale : COLOR1; +}; + +struct InstanceData +{ + float3 pos; + uint packed; +}; + +StructuredBuffer visible_indices : register(t0); +StructuredBuffer all_instances : register(t1); + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 tc : TEXCOORD0; +}; + +static const float PACK_MAX_SCALE = 4.0; +static const float TWO_PI = 6.28318530718; + +VS_OUTPUT main(v_blade_sdf I, uint instanceID : SV_InstanceID) +{ + VS_OUTPUT O; + uint idx = visible_indices[instanceID]; + InstanceData raw = all_instances[idx]; + + float rotation = float((raw.packed >> 8) & 0x3FF) / 1023.0 * TWO_PI; + float scale = float((raw.packed >> 18) & 0x3FF) / 1023.0 * PACK_MAX_SCALE; + float blade_height = max(scale * grass_blade_height, 0.05); + + float3 facing = normalize(float3(sin(rotation), 0.0, cos(rotation))); + float3 right = normalize(float3(facing.z, 0.0, -facing.x)); + + float3 world = raw.pos; + world += right * (I.pos.x * I.width_scale * scale * 0.5); + world.y += I.t * blade_height; + + O.position = mul(cb_LightVP, float4(world, 1.0)); + O.tc = I.tc; + return O; +} diff --git a/res/gamedata/shaders/r5/dof.ps b/res/gamedata/shaders/r5/dof.ps new file mode 100644 index 0000000000000000000000000000000000000000..2e21966ac19204f8ae075dbb2622b30ed48661f1 GIT binary patch literal 3091 zcma)8>u%yk6#mas9I1cI2AuH?@Um2@!e$exZBUYpR*52OfPq+RJZQ$|l5C_tM4zxv z(lay07`s}P9|?HQ_nrIaIX{OvU)XC2|NQkg%n~YPJcA29>B?-MFc{xlg0<$clss+2 zf(y75bj7|1x_*JH@onpuIa|ajgS$VXUPM~0d$!AR!Q3${qYIuO;3EjOjAh1zm67)F z#A3|W*~^KAgBEvmwN9A0%V@?xuGW#r67)e&xTNB0SYoJYRwa~*ur-_Jiv<%f=8Fl% z1Z34@O4_Zr7T{-*aGLq5BGFo#4dMJ8b|dAx1N+f}&vMy;*U>`#-j3w=eKg0^>frB8 zq%1MuJ&b_Z!MR`xi9gIC$oQ2Z2jpok*}THA^I;WTr<;e#u>SO6*m}ooWG3=i29vw! z`p?PNdw4qpzUC^iyN77PWt_!49hwst@(mu`fBtrHb8|Z;Lr^q_)_tDbjXzKBuT-yz zO$c#XElm3%X4|e@(-{MmBTexOO?bM58BTzpY8tjw%)$EBy`_6Ndu-eATe78l2^a|a z-!QwSORBjZF9l!ehFkif3aVq`13|a?)9GwzqAdVAzB30Cm0H9DQMcTy}vtM~IKyjF#D zS0ioGVT3wZ&3aY_eo{4MfQ?uGj%CB;p>b-JyK&t%8*xv?l4Zr7<8)uotEz4ty%3~ri{-QeFUM3<=rw6jXxS_AJ6!H^5sIN0)pt-WU%7V1 zUAzFxjL@eMR8`@}kJ1!=RdN=FVPNuBehdc3uhdO@dlG`~)JVNdiiy^UVsVsLZSYC0 zlYgWwmcxj$g*7zGQ!rx>FU;%eBgh%W_Kmb`&;(30c5j~gmon8asR}reWJ=gP5T`>=dcN-l4MbQ&48or8V&edva30@6iz(sV2aCR0O@;7Z(x^Y!JXo^>gGvH5gu@L3C zr_RmQ@Be5X8h4;7dL@zKxusilwTyl$dj|^CJq1=Dy_SkV1Cc`AW-2!j4Ke(M3@Mb7 zq7u{8Kx6-33aG#(ur7q^P<_gnF3OG^74d+sM5DTSHab@GgZZkvdCc_C-h*Z3_+6Ld TR7Mr4dC3>R;)QG{Vm|?zIffJ1e zqYT{H`YA2K84Nb9B{Uv|e!{ryhYk%VNeH$d{!(^MF-vBA5tKYv!$|DI4)0D&is4#n z3$B&lYo}UjwMsTK0)bOFo8t0Fi=xWH-HIXAoi@%&i(648?CTg?V=r6fZ6L_~wOdHj d;KDWNep1^z!8+gDho2x1W~dKGrxu3mWG{Vm|?zIffJ1e zqYT{H`YA2K84Nb9B{Uv|e!{ryhYk%VNeH$d{!(^MF-vBA5tKYv!$|DI4)0D&is4#n z3$B&lYo}UjwMsTK0)bOFo8t0Fi=xWH-HIXAoi@%&i(648?CTg?V=r6fZ6L_~wOdHj d;KDWNep1^z!8+gDho2x1W~dKGrxu3mW<_9&&X{7NqlnnNBfil(p;2T5)QO|4|26seLFVmAoTQ$L{S7v@X4 z6lGg-bPyzQcXoDmR)YaxmLYd``K*mhmsV#kDJ{JH{s;aUQY_sXH0QFiLMvc76UsXH za#>NLfjVujEkxh7bf_M_^)1UV?%m_YRR+h`V3#Xq5vE`;7uKPP+%P_G1?0;GM}?1` zfrn~Gjwo`$ic+FkIOY&+QNF{o9{@tZ0UQ+>oO#(3x%0u2mmB&Z*duE0SBy(nu&biTr&W*Zn5DAtamaDGcL=xX2)C zkdkM(fMN16uB*J%_kC{@z%5d(Fml596&l|M1gh6$TZ)pXa<|?CQ#ABFKvVRPlx?~h zf$Dx$j+02f%dS3^$rwj?`|}rm2q__Y7PN)7VDyZo6_Q++k+ZliB~#3V<~#x4c~kzP zMg9XxFlsdLVg5>m<; g_histogram : register(t0); // 64 bins from histogram pass -RWTexture2D g_exposure : register(u0); // 1x1 output (also read for adaptation) - -// ═══════════════════════════════════════════════════════ -// EXPOSURE CALCULATION -// ═══════════════════════════════════════════════════════ +StructuredBuffer g_histogram : register(t0); +RWTexture2D g_exposure : register(u0); +// Recover approximate linear average luminance from 64 log2 bins. +// Classic CoP used a linear downsample of the bloom/high buffer; this is the +// FG stand-in (same MiddleGray formula on top). float ComputeAverageLuminance() { - // Count total pixels in histogram uint totalPixels = 0; for (uint i = 0; i < 64; i++) - { totalPixels += g_histogram[i]; - } if (totalPixels == 0) - return 0.5; // Default mid-gray + return 1.0; - // Find percentile boundaries - uint lowThreshold = (uint)(totalPixels * g_low_percentile); - uint highThreshold = (uint)(totalPixels * g_high_percentile); + // Match histogram binning in luminance_histogram.cs: log2 in [-10, +4] + const float minLog = -10.0; + const float logRange = 14.0; - // Accumulate weighted luminance, skipping extremes float weightedSum = 0.0; - uint validPixels = 0; - uint runningCount = 0; - for (uint bin = 0; bin < 64; bin++) { - uint binCount = g_histogram[bin]; - uint prevCount = runningCount; - runningCount += binCount; - - // Skip if entirely below low percentile - if (runningCount <= lowThreshold) + uint c = g_histogram[bin]; + if (c == 0) continue; - - // Stop if we've passed high percentile - if (prevCount >= highThreshold) - break; - - // Calculate how many pixels in this bin are within our range - uint startInBin = max(prevCount, lowThreshold) - prevCount; - uint endInBin = min(runningCount, highThreshold) - prevCount; - uint countInRange = endInBin - startInBin; - - if (countInRange > 0) - { - // Convert bin index to log luminance (use bin center) - float t = (float(bin) + 0.5) / 64.0; - float logLum = g_min_log_luminance + t * g_log_luminance_range; - float luminance = exp2(logLum); - - weightedSum += luminance * float(countInRange); - validPixels += countInRange; - } + float t = (float(bin) + 0.5) / 64.0; + float lum = exp2(minLog + t * logRange); + weightedSum += lum * float(c); } - - if (validPixels == 0) - return 0.5; - - return weightedSum / float(validPixels); + return weightedSum / float(totalPixels); } -float ComputeTargetExposure(float avgLuminance) -{ - // Standard exposure equation: - // EV100 = log2(L * S / K) - // where L = luminance, S = ISO 100 sensitivity, K = calibration constant - // - // Exposure = 1 / (2^EV100) for proper exposure - // Simplified: exposure = K / (L * 100) for ISO 100 reference - - // Avoid division by zero - float lum = max(avgLuminance, 0.0001); - - // Calculate exposure to achieve middle gray (18% reflectance) - // The formula K / (luminance * 100) gives exposure for ISO 100 - float exposure = g_calibration_constant / (lum * 100.0); - - // Apply exposure compensation (in EV stops) - exposure *= exp2(g_exposure_compensation); - - // Clamp to valid range - return clamp(exposure, g_min_exposure, g_max_exposure); -} - -float AdaptExposure(float currentExposure, float targetExposure) -{ - // Asymmetric adaptation: faster when brightening, slower when darkening - // This mimics human eye adaptation behavior - float adaptSpeed = (targetExposure > currentExposure) - ? g_adapt_speed_up - : g_adapt_speed_down; - - // Exponential approach to target - // adaptFactor = 1 - e^(-dt * speed) - float adaptFactor = 1.0 - exp(-g_delta_time * adaptSpeed); - - // Lerp toward target - return lerp(currentExposure, targetExposure, adaptFactor); -} - -// ═══════════════════════════════════════════════════════ -// MAIN COMPUTE SHADER -// ═══════════════════════════════════════════════════════ - [numthreads(1, 1, 1)] void main(uint3 dispatch_id : SV_DispatchThreadID) { - // Read current exposure (for temporal adaptation) - float currentExposure = g_exposure[uint2(0, 0)]; - - // Compute average luminance from histogram - float avgLuminance = ComputeAverageLuminance(); + float Lw = ComputeAverageLuminance(); + float scale = g_middle_gray_x / max(Lw * g_middle_gray_y + g_middle_gray_z, 1e-6); - // Compute target exposure - float targetExposure = ComputeTargetExposure(avgLuminance); + float scalePrev = g_exposure[uint2(0, 0)]; + // First frame / seed: treat 0 as "no history" + if (scalePrev <= 1e-6) + scalePrev = 1.0; - // Apply temporal adaptation - float newExposure = AdaptExposure(currentExposure, targetExposure); + float rvalue = lerp(scalePrev, scale, saturate(g_middle_gray_w)); + rvalue = clamp(rvalue, 1.0 / 128.0, 20.0); - // Write output - g_exposure[uint2(0, 0)] = newExposure; + g_exposure[uint2(0, 0)] = rvalue; } diff --git a/res/gamedata/shaders/r5/hud/cursor.ps b/res/gamedata/shaders/r5/hud/cursor.ps new file mode 100644 index 0000000000000000000000000000000000000000..d05fdb9c3164353010760c348043477bb8406032 GIT binary patch literal 217 zcmYL?O$&lR6h+t0uXyOjiI5hp3Kx|U1L7hlaW{uwKc)hCEk*it*I3bgIG(_C@aT3XxSRS6FdP%;Q(1Kxa=P|k~eYM27DH9oLD_U z7y?W4Q^F#_uwKc)hCEk*it*I3bgIG(_C@aT3XxSRS6FdP%;Q(1Kxa=P|k~eYM27DH9oLD_U z7y?W4Q^F#_`=d@&Mw{&9LT29NRrks`WJ32kz@y!&` W#TX)A{sQ@Pao0VS!wxU}8~q24W#QET literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/hud/movie.vs b/res/gamedata/shaders/r5/hud/movie.vs new file mode 100644 index 00000000000..95cbc6a6168 --- /dev/null +++ b/res/gamedata/shaders/r5/hud/movie.vs @@ -0,0 +1,21 @@ +#include "common_iostructs.h" + +////////////////////////////////////////////////////////////////////////////////////////// +// UI / hud\movie — fullscreen/UI video (YUV sampled in PS) +v2p_TL main(v_TL_positiont I) +{ + v2p_TL O; + + { + I.P.xy += 0.5f; + O.HPos.x = I.P.x * screen_res.z * 2 - 1; + O.HPos.y = (I.P.y * screen_res.w * 2 - 1) * -1; + O.HPos.zw = I.P.zw; + } + + O.Tex0 = I.Tex0; + O.Color = I.Color.bgra; + O.TexIdx = I.TexIdx; + + return O; +} diff --git a/res/gamedata/shaders/r5/luminance_histogram.cs b/res/gamedata/shaders/r5/luminance_histogram.cs index 971bd74ee28..5282ada2eeb 100644 --- a/res/gamedata/shaders/r5/luminance_histogram.cs +++ b/res/gamedata/shaders/r5/luminance_histogram.cs @@ -32,8 +32,8 @@ float ComputeLuminance(float3 color) { - // ITU BT.709 luminance coefficients - return dot(color, float3(0.2126, 0.7152, 0.0722)); + // Classic CoP LUMINANCE_VECTOR (common_defines.h) — not BT.709 + return dot(color, float3(0.3, 0.38, 0.22)); } uint ComputeBinIndex(float luminance) @@ -77,13 +77,11 @@ void main(uint3 dispatch_id : SV_DispatchThreadID, uint group_index : SV_GroupIn // Compute luminance float luminance = ComputeLuminance(color.rgb); - // Skip very dark pixels (effectively transparent/sky) - if (luminance > 0.0001) + // Include dark pixels too (classic averages the whole bloom buffer; + // skipping them made night meter sky-only and blew exposure). + if (luminance >= 0.0) { - // Get bin index - uint binIndex = ComputeBinIndex(luminance); - - // Atomically increment local histogram + uint binIndex = ComputeBinIndex(max(luminance, 0.00001)); InterlockedAdd(gs_histogram[binIndex], 1); } } diff --git a/res/gamedata/shaders/r5/object_cull.cs b/res/gamedata/shaders/r5/object_cull.cs index c5e62248bc6..87f6ed9099b 100644 --- a/res/gamedata/shaders/r5/object_cull.cs +++ b/res/gamedata/shaders/r5/object_cull.cs @@ -53,7 +53,8 @@ struct IndirectDrawArgs uint g_HiZHeight; // Hi-Z pyramid base height uint g_HiZMipLevels; // Number of Hi-Z mip levels uint g_FrameId; // Frame stamp for visibility - uint3 g_Padding; + uint g_HiZEnable; // 0=skip Hi-Z occlusion (frustum+distance only) + uint2 g_Padding; }; // ═══════════════════════════════════════════════════════ @@ -113,9 +114,12 @@ void main(uint3 dtID : SV_DispatchThreadID) // Note: This test is conservative - may mark occluded objects as visible // but will never mark visible objects as occluded // TEMPORAL HI-Z: Use prevViewProj for Hi-Z lookup since pyramid was built from previous frame's depth - if (!HiZTestSphereTemporal(obj.position, obj.radius, g_CameraPos, g_ViewProj, g_PrevViewProj, - g_HiZPyramid, smp_nofilter, g_HiZWidth, g_HiZHeight, g_HiZMipLevels)) - return; + if (g_HiZEnable != 0) + { + if (!HiZTestSphereTemporal(obj.position, obj.radius, g_CameraPos, g_ViewProj, g_PrevViewProj, + g_HiZPyramid, smp_nofilter, g_HiZWidth, g_HiZHeight, g_HiZMipLevels)) + return; + } // ───────────────────────────────────────────────────── // OBJECT IS VISIBLE - Add to output diff --git a/res/gamedata/shaders/r5/rain/rain_billboard.cs b/res/gamedata/shaders/r5/rain/rain_billboard.cs new file mode 100644 index 00000000000..29919fb1f82 --- /dev/null +++ b/res/gamedata/shaders/r5/rain/rain_billboard.cs @@ -0,0 +1,96 @@ +// rain_billboard.cs — build rain streak quads from GPU-visible particles +#define THREAD_GROUP_SIZE 64 + +struct RainParticle { + float4 headSpeed; + float4 dirPad; + uint2 uvFlags; + uint2 pad; +}; + +// float4-only cbuffer (32 bytes) — rain color u32 passed via g_Params.w bitcast. +cbuffer RainBillboardParams : register(b0) { + float4 g_CameraPos; + float4 g_Params; // x=factorVisual, y=dropLength, z=dropWidth, w=rainColorU32 bits +}; + +StructuredBuffer g_Particles : register(t0); +StructuredBuffer g_VisibleIndices : register(t1); +ByteAddressBuffer g_VisibleCountBuf : register(t2); + +RWByteAddressBuffer g_Vertices : register(u0); +RWByteAddressBuffer g_DrawArgs : register(u1); + +static float2 UV0[4] = { float2(0,1), float2(0,0), float2(1,1), float2(1,0) }; +static float2 UV1[4] = { float2(1,0), float2(1,1), float2(0,0), float2(0,1) }; + +void WriteRainVertex(uint vIdx, float3 pos, uint col, float2 uv) +{ + uint b = vIdx * 24u; + g_Vertices.Store3(b, asuint(float4(pos, 0.0))); + g_Vertices.Store(b + 12u, col); + g_Vertices.Store2(b + 16u, asuint(uv)); +} + +void WriteDrawArgs(uint actualCount) +{ + uint vertexCount = actualCount * 6; + g_DrawArgs.Store(0, vertexCount); + g_DrawArgs.Store(4, 1u); + g_DrawArgs.Store(8, 0u); + g_DrawArgs.Store(12, 0u); +} + +[numthreads(THREAD_GROUP_SIZE, 1, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + uint visIdx = dtID.x; + uint actualCount = g_VisibleCountBuf.Load(0); + + if (visIdx == 0) + WriteDrawArgs(actualCount); + + if (visIdx >= actualCount) + return; + + uint pIdx = g_VisibleIndices[visIdx]; + RainParticle p = g_Particles[pIdx]; + + const float factorVisual = g_Params.x; + const float dropLen = g_Params.y * factorVisual; + const float dropW = g_Params.z; + const uint col = asuint(g_Params.w); + + float3 posHead = p.headSpeed.xyz; + float3 dir = normalize(p.dirPad.xyz); + float3 posTrail = posHead - dir * dropLen; + + float3 sC = (posHead + posTrail) * 0.5; + float3 lineD = normalize(posHead - posTrail); + + float3 camDir = normalize(sC - g_CameraPos.xyz); + float3 lineTop = cross(camDir, lineD); + float ltLen = length(lineTop); + if (ltLen < 1e-4) + lineTop = float3(0, 0, 1); + else + lineTop /= ltLen; + + float2 uv[4]; + if (p.uvFlags.x == 0u) { [unroll] for (int i = 0; i < 4; ++i) uv[i] = UV0[i]; } + else { [unroll] for (int j = 0; j < 4; ++j) uv[j] = UV1[j]; } + + float3 corners[4]; + corners[0] = posTrail - lineTop * dropW; + corners[1] = posTrail + lineTop * dropW; + corners[2] = posHead - lineTop * dropW; + corners[3] = posHead + lineTop * dropW; + + uint baseV = visIdx * 6; + WriteRainVertex(baseV + 0, corners[0], col, uv[0]); + WriteRainVertex(baseV + 1, corners[1], col, uv[1]); + WriteRainVertex(baseV + 2, corners[2], col, uv[2]); + WriteRainVertex(baseV + 3, corners[1], col, uv[1]); + WriteRainVertex(baseV + 4, corners[3], col, uv[3]); + WriteRainVertex(baseV + 5, corners[2], col, uv[2]); +} diff --git a/res/gamedata/shaders/r5/rain/rain_update.cs b/res/gamedata/shaders/r5/rain/rain_update.cs new file mode 100644 index 00000000000..ae34c2ae293 --- /dev/null +++ b/res/gamedata/shaders/r5/rain/rain_update.cs @@ -0,0 +1,170 @@ +// rain_update.cs — GPU rain streak simulation + rain-SM cover cull +#define THREAD_GROUP_SIZE 64 + +// Match fgRainRender.cpp / Rain.cpp constants (never read from CB — avoids layout bugs). +static const float kSourceRadius = 12.5; +static const float kSourceOffset = 40.0; +static const float kMaxDistance = 50.0; // source_offset * 1.25 +static const float kSinkOffset = -10.0; // -(max_distance - source_offset) +static const float kDropAngleRad = 0.0523598776; // deg2rad(3.f) +static const float kDropSpeedMin = 40.0; +static const float kDropSpeedMax = 80.0; + +struct RainParticle { + float4 headSpeed; // xyz=head, w=speed + float4 dirPad; // xyz=dir (normalized), w=unused + uint2 uvFlags; // x=uvSet, y=alive (1=valid) + uint2 pad; +}; + +cbuffer RainSimParams : register(b0) { + float4 g_CameraPos; + float4 g_WindAxis; + float4 g_Params; // x=dt, y=factorVisual, z=unused, w=rainDensity + float4x4 g_RainSampleVP; + float4 g_RainSM; // x=enabled, y=res, zw=unused + uint g_ParticleCount; + uint g_FrameSeed; + uint2 g_Pad; +}; + +#include "common_samplers.h" + +RWStructuredBuffer g_Particles : register(u0); +RWStructuredBuffer g_VisibleIndices : register(u1); +RWByteAddressBuffer g_VisibleCount : register(u2); + +Texture2D g_RainShadow : register(t0); + +static const float PI = 3.14159265; + +float Hash11(uint n) +{ + n = (n << 13u) ^ n; + return frac(float(n) * (1.0 / 4294967296.0)); +} + +float2 Hash22(uint n) +{ + return float2(Hash11(n), Hash11(n * 1664525u + 1013904223u)); +} + +float3 RandomUnitSphere(uint seed) +{ + float z = cos(Hash11(seed) * PI); + float a = Hash11(seed + 1u) * PI * 2.0; + float r = sqrt(max(0.0, 1.0 - z * z)); + return float3(r * cos(a), r * sin(a), z); +} + +float3 RandomConeDir(float3 axis, float coneAngle, uint seed) +{ + float3 rnd = RandomUnitSphere(seed); + float scale = Hash11(seed + 2u) * tan(coneAngle); + return normalize(axis + rnd * scale); +} + +bool NeedsRespawn(RainParticle p) +{ + if (p.uvFlags.y != 1u) + return true; + float3 dir = p.dirPad.xyz; + if (any(isnan(dir)) || dot(dir, dir) < 0.25) + return true; + return dir.y > -0.5; +} + +void RespawnParticle(uint idx, inout RainParticle p) +{ + uint seed = idx * 747796405u + g_FrameSeed * 2891336453u; + + float2 h = Hash22(seed); + float angle = h.x * PI * 2.0; + float dist = sqrt(h.y) * kSourceRadius; + float3 cam = g_CameraPos.xyz; + + float3 axis = g_WindAxis.xyz; + if (dot(axis, axis) < 1e-4) + axis = float3(0.0, -1.0, 0.0); + else + axis = normalize(axis); + + float3 randDir = RandomConeDir(axis, kDropAngleRad, seed + 17u); + float3 head = float3( + dist * cos(angle) + cam.x - randDir.x * kSourceOffset, + kSourceOffset + cam.y, + dist * sin(angle) + cam.z - randDir.z * kSourceOffset); + + p.headSpeed = float4(head, lerp(kDropSpeedMin, kDropSpeedMax, Hash11(seed + 31u))); + p.dirPad = float4(randDir, 0.0); + p.uvFlags = uint2(uint(Hash11(seed + 47u) * 2.0) & 1u, 1u); + p.pad = uint2(0, 0); +} + +bool UnderRoof(float3 headPos) +{ + if (g_RainSM.x < 0.5) + return false; + + float4 tc = mul(g_RainSampleVP, float4(headPos, 1.0)); + float3 uvz = tc.xyz / max(tc.w, 1e-5); + if (any(uvz.xy < 0.0) || any(uvz.xy > 1.0) || uvz.z < 0.0 || uvz.z > 1.0) + return false; + + float depth = saturate(uvz.z); + float shadowDepth = g_RainShadow.SampleLevel(smp_nofilter, uvz.xy, 0).x; + const float bias = 0.0015; + float cover = saturate((shadowDepth - (depth - bias)) * 800.0); + float edge = saturate(min(uvz.x, 1.0 - uvz.x) * 8.0) * saturate(min(uvz.y, 1.0 - uvz.y) * 8.0); + cover = lerp(1.0, cover, edge); + return cover < 0.5; +} + +[numthreads(THREAD_GROUP_SIZE, 1, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + uint idx = dtID.x; + if (idx >= g_ParticleCount) + return; + + RainParticle p = g_Particles[idx]; + if (NeedsRespawn(p)) + RespawnParticle(idx, p); + + const float dt = g_Params.x; + const float3 cam = g_CameraPos.xyz; + const float3 dir = normalize(p.dirPad.xyz); + const float speed = p.headSpeed.w; + + p.headSpeed.xyz += dir * speed * dt; + + const float radiusWrap = kSourceRadius + 0.5; + const float radiusWrapSqr = radiusWrap * radiusWrap; + + float3 wdir = float3(p.headSpeed.x - cam.x, 0.0, p.headSpeed.z - cam.z); + float wlenSqr = dot(wdir, wdir); + if (wlenSqr > radiusWrapSqr) + { + float wlen = sqrt(wlenSqr); + if ((p.headSpeed.y - cam.y) < kSinkOffset) + RespawnParticle(idx, p); + else + { + wdir /= wlen; + p.headSpeed.xyz -= wdir * (wlen + kSourceRadius); + } + } + + p.dirPad.xyz = dir; + g_Particles[idx] = p; + + if (UnderRoof(p.headSpeed.xyz)) + return; + + if ((p.headSpeed.y - cam.y) < kSinkOffset) + return; + + uint outIdx; + g_VisibleCount.InterlockedAdd(0, 1, outIdx); + g_VisibleIndices[outIdx] = idx; +} diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade.ps b/res/gamedata/shaders/r5/shadow/shadow_cascade.ps new file mode 100644 index 0000000000000000000000000000000000000000..157f640326af8b31665d2381927ad3ca68aaf748 GIT binary patch literal 621 zcmZWn-D=w~6uyq0;@AiUI-+8Kl;u&ieqpa6!sS{&=n|TR6F?l`wyV5H+WHKRYPeTT3X>+LJSqUA5s9( zy=)viQ>M_ZkZ}5@jL_0KUf8O#Cfb}Hfx{hlSk;6N)SPr;1GiH)~ye_EQfkh^w_z; z+86?-HI*mw5;$6Sxf7h@R*L9bc%j zY@N)EkXx8cAbWUDW?4#tRsOV?QiIaziZr89BYoQ#dTZ#_FkOmvEB*C;Y`NSLNA^LE ujl*c!4=r$2&9yQToAaUG=+MwG!p^$;EJ6Qo;G!!%>h}v-hJ^0ej{O7CfzB8J literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade.vs b/res/gamedata/shaders/r5/shadow/shadow_cascade.vs new file mode 100644 index 00000000000..854f6636b4e --- /dev/null +++ b/res/gamedata/shaders/r5/shadow/shadow_cascade.vs @@ -0,0 +1,56 @@ +// shadow_cascade.vs — depth-only CSM caster (bindless mega-buffer path) +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" + +struct VS_INPUT +{ + float4 position : POSITION; + float4 normal : NORMAL; + float4 tangent : TANGENT; + float4 binormal : BINORMAL; + float2 texcoord : TEXCOORD0; + float2 texcoord1: TEXCOORD1; + float4 color : COLOR0; + uint drawIndex : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; + nointerpolation uint materialID : TEXCOORD1; +}; + +struct InstanceData +{ + float4x4 world; + uint materialID; + uint flags; + float pad0, pad1; +}; + +cbuffer ShadowCascadeCB : register(b5) +{ + float4x4 cb_LightVP; +}; + +StructuredBuffer g_InstanceData : register(t14); +StructuredBuffer g_CompactBatchIndices : register(t15); +StructuredBuffer g_CompactMaterialIDs : register(t16); + +VS_OUTPUT main(VS_INPUT input) +{ + VS_OUTPUT output; + + uint drawID = input.drawIndex; + uint batchIndex = g_CompactBatchIndices[drawID]; + InstanceData instanceData = g_InstanceData[batchIndex]; + uint materialID = g_CompactMaterialIDs[drawID]; + + float4 worldPos = mul(instanceData.world, float4(input.position.xyz, 1.0)); + output.position = mul(cb_LightVP, worldPos); + output.texcoord = input.texcoord; + output.materialID = materialID; + return output; +} diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade_foliage.ps b/res/gamedata/shaders/r5/shadow/shadow_cascade_foliage.ps new file mode 100644 index 0000000000000000000000000000000000000000..cd7a0693cd45d8a27d291fb71d213f002ba42cae GIT binary patch literal 1482 zcmZ`(+m72d5Pi13VwwVl1?DhTM zsc5}v{ov=QGM0SJK3m1Q03lE+` zWq?N25`G5w0l?SRbR=L_vZSqf9O}-J@1-tOTQWV{N?j^;E-%b}Z**2aJ3H)cxfA9^U8o!{60#{v&yQER3-w&Ns`C zukY^Giz!aEks75mMv3U%;+OWJIw{m@ad!LpRr2X7Ne%|p?1j|hQxcFt)A}^z@@8;c z2rmK(&1jdOjPAF1G5do4JMc#6-0T0SKjZyO!zT>{DL&;rqcy9e)H~3mj(9}8Z%Exs z=Rjpoepegl8COc|qJpKygo%KgPQnyI1W}00(-Sddx1?%?8pGiG^p73j;wr8 0.001f) { + float shadow = 1.0; + // spotParamsAndType.w = asfloat(tile+1) when this light owns an atlas slice + uint localTile = asuint(light.spotParamsAndType.w); + if (localTile != 0) + shadow = SampleLocalShadow(worldPos, light.spotVP, localTile); + + // Contact history is sun-marched — do not apply to clustered local lights. float3 litColor = PBRDirectLighting( albedo, N, V, L, - lightColor * atten, + lightColor * atten * shadow, metallic, roughness, diffuseMode); totalLight += litColor; } diff --git a/res/gamedata/shaders/r5/shared/common.h b/res/gamedata/shaders/r5/shared/common.h index d8853bb04fd..2a36a68aa5e 100644 --- a/res/gamedata/shaders/r5/shared/common.h +++ b/res/gamedata/shaders/r5/shared/common.h @@ -34,7 +34,7 @@ cbuffer static_globals : register(b2) float4 fog_plane; float4 fog_params; // x=near*(1/(far-near)), ?,?, w = -1/(far-near) - float4 fog_color; + float4 fog_color; // .w = rain_density (terrain puddles / wet) float4 L_ambient; // L_ambient.w = skynbox-lerp-factor float3 L_sun_color; @@ -43,8 +43,8 @@ cbuffer static_globals : register(b2) float hud_fov; float4 L_hemi_color; - float3 eye_position; - float _pad_eye; + float3 eye_position; + float contact_shadow_length; // 0 = off; meters of SS contact ray float4 pos_decompression_params; float4 pos_decompression_params2; diff --git a/res/gamedata/shaders/r5/shared/contact_shadows.h b/res/gamedata/shaders/r5/shared/contact_shadows.h new file mode 100644 index 00000000000..ae7fe06f482 --- /dev/null +++ b/res/gamedata/shaders/r5/shared/contact_shadows.h @@ -0,0 +1,37 @@ +// shared/contact_shadows.h — screen-space contact from temporal history RT +#ifndef CONTACT_SHADOWS_H +#define CONTACT_SHADOWS_H + +#ifdef CSM_SHADOW_FORWARD +// History only (t28). Do not declare unused g_ContactDepth — FilterReflection +// drops it from the layout while TextureSlot(27) still binds → NVRHI fails. +Texture2D g_ContactHistory : register(t28); // 1 = lit (fullscreen contact pass) + +// Sample only the precomputed contact history. Inline march against prev-frame +// depth + current InvVP caused a sticky dark disc (false hits + 0.82 temporal). +float ContactShadow(float3 worldPos, float3 lightDir) +{ + float len = contact_shadow_length; + if (len <= 1e-4) + return 1.0; + + float4 clip0 = mul(m_VP, float4(worldPos, 1.0)); + if (clip0.w <= 1e-4) + return 1.0; + + float2 uv0 = float2(clip0.x / clip0.w * 0.5 + 0.5, 0.5 - clip0.y / clip0.w * 0.5); + if (any(uv0 < 0.0) || any(uv0 > 1.0)) + return 1.0; + + // lightDir kept for call-site API; history pass is view-aligned, not light-marched. + return saturate(g_ContactHistory.SampleLevel(smp_nofilter, uv0, 0).r) + + 0.0 * dot(lightDir, lightDir); +} +#else +float ContactShadow(float3 worldPos, float3 lightDir) +{ + return 1.0 + 0.0 * dot(lightDir, lightDir); +} +#endif + +#endif diff --git a/res/gamedata/shaders/r5/shared/foliage_sss.h b/res/gamedata/shaders/r5/shared/foliage_sss.h new file mode 100644 index 00000000000..bb670703b2c --- /dev/null +++ b/res/gamedata/shaders/r5/shared/foliage_sss.h @@ -0,0 +1,66 @@ +// foliage_sss.h — basic subsurface / transmission for grass, leaves, bushes +// Thickness defaults to 1; pass a texture sample later (MAT_FLAG_HAS_SSS_MAP). +#ifndef FOLIAGE_SSS_H +#define FOLIAGE_SSS_H + +// Wrap + backlit transmission (Jimenez-style cheap foliage SSS) +// Extra view↔light term so CoP density billboards still glow with sun behind them +// (flat cards often face the camera → classic -N·L transmission dies). +float3 EvaluateFoliageSSS( + float3 albedo, + float3 N, + float3 V, + float3 L, // direction toward sun + float3 lightColor, + float shadow, // 0..1 (can be softened for transmission) + float3 sssTint, + float thickness, // 1 = opaque leaf, lower = thinner + float intensity) +{ + if (intensity <= 1e-5) + return float3(0, 0, 0); + + N = normalize(N); + L = normalize(L); + V = normalize(V); + + const float wrap = 0.45; + float NdotL = dot(N, L); + float wrapDiffuse = saturate((NdotL + wrap) / (1.0 + wrap)); + + // Transmission through the blade (either face) + float backLit = saturate(-NdotL); + float3 H = normalize(L + N * 0.35); + float VdotH = saturate(dot(V, -H)); + float scatter = pow(VdotH, 1.6) * backLit; + + // Looking into the sun through foliage (billboard-friendly) + float through = pow(saturate(dot(V, -L)), 3.0); + through *= lerp(0.35, 1.0, saturate(1.0 - abs(NdotL))); + + float thin = saturate(thickness); + float term = (wrapDiffuse * 0.35 + scatter * 1.1 + through * 0.85) * thin * intensity; + + float sssShadow = lerp(shadow, 1.0, 0.65); + + return albedo * sssTint * lightColor * term * sssShadow; +} + +#if defined(SKY_IBL) +// Soft sky bleed through foliage (cheap multi-scatter ambient) +float3 EvaluateFoliageSkySSS( + float3 albedo, + float3 N, + float3 sssTint, + float thickness, + float intensity) +{ + if (intensity <= 1e-5) + return float3(0, 0, 0); + float3 sky = SampleSkyRGB(-normalize(N)); + float weather = length(L_ambient.rgb + L_hemi_color.rgb * L_hemi_color.w); + return albedo * sssTint * sky * weather * thickness * intensity * 0.35; +} +#endif + +#endif // FOLIAGE_SSS_H diff --git a/res/gamedata/shaders/r5/shared/shadow_sampling.h b/res/gamedata/shaders/r5/shared/shadow_sampling.h index 44d3108b34c..7b55cc73870 100644 --- a/res/gamedata/shaders/r5/shared/shadow_sampling.h +++ b/res/gamedata/shaders/r5/shared/shadow_sampling.h @@ -1,76 +1,568 @@ +// shared/shadow_sampling.h — CSM + soft PCSS (FrameGraph r5) #ifndef SHADOW_SAMPLING_H #define SHADOW_SAMPLING_H #ifdef CSM_SHADOW_FORWARD -Texture2DArray g_ShadowMapArray : register(t23); +// Per-cascade maps (resolution ladder). Texture2D — not Array: one SampleCmp +// target per cascade avoids layout/slot mismatches with a shared array. +Texture2D g_ShadowMap0 : register(t23); +Texture2D g_ShadowMap1 : register(t29); +Texture2D g_ShadowMap2 : register(t30); SamplerComparisonState smp_shadowcmp : register(s4); +SamplerState smp_shadow_point : register(s5); #endif #ifdef HUD_SHADOW_FORWARD -Texture2D g_HUDShadowMap : register(t24); +Texture2DArray g_HUDShadowMap : register(t24); #endif +#ifdef LOCAL_SHADOW_FORWARD +// Spot / OMNIPART local shadow atlas (t27 — free after g_ContactDepth removal) +Texture2DArray g_LocalShadowAtlas : register(t27); +#ifndef CSM_SHADOW_FORWARD +SamplerComparisonState smp_shadowcmp : register(s4); +#endif +#endif + +#include "shared/contact_shadows.h" + +#ifdef CSM_SHADOW_FORWARD + +float ShadowCmpCascade(uint cascade, float2 uv, float refDepth) +{ + if (cascade == 0) + return g_ShadowMap0.SampleCmp(smp_shadowcmp, uv, refDepth); + if (cascade == 1) + return g_ShadowMap1.SampleCmp(smp_shadowcmp, uv, refDepth); + return g_ShadowMap2.SampleCmp(smp_shadowcmp, uv, refDepth); +} + +// NVRHI Vulkan comparison: CompareOp::Less → 1 = lit, 0 = shadowed. +// uvw.xy = shadow UV, uvw.z = cascade index (matches previous Array API). +float ShadowCmpLit(float3 uvw, float refDepth) +{ + return ShadowCmpCascade((uint)uvw.z, uvw.xy, refDepth); +} + +float ReceiverBias(uint cascade) +{ + // Small depth bias; the bulk of acne removal is done by world normal-offset + // (see NormalOffsetWorld), so this stays low to avoid peter-panning. + return 0.0004 + 0.0003 * float(cascade); +} + +// World-space normal offset before projecting into the shadow map. Pushing the +// sample off the surface along N removes self-shadow acne without detaching the +// contact (peter-panning) the way a large depth bias does. dev_param_3.w = +// r2_sun_normal_bias (meters); coarser cascades cover more world/texel so scale up. +float3 NormalOffsetWorld(float3 N, uint cascade) +{ + float baseMeters = dev_param_3.w > 0.0 ? dev_param_3.w : 0.04; + return N * (baseMeters * (1.0 + float(cascade))); +} + +float ShadowDepth(uint cascade, float2 uv) +{ + if (cascade == 0) + return g_ShadowMap0.SampleLevel(smp_shadow_point, uv, 0).x; + if (cascade == 1) + return g_ShadowMap1.SampleLevel(smp_shadow_point, uv, 0).x; + return g_ShadowMap2.SampleLevel(smp_shadow_point, uv, 0).x; +} + +float CascadeSmapSize(uint cascade) +{ + float base = dev_param_3.x > 0 ? dev_param_3.x : 2048.0; + return max(base / float(1u << cascade), 512.0); +} + +bool CascadeCoordValid(float4 sc) +{ + return all(sc.xy >= 0.0) && all(sc.xy <= 1.0) && sc.z >= 0.0 && sc.z <= 1.0; +} + +float CascadeUVRim(float2 uv) +{ + float2 d = min(uv, 1.0 - uv); + return min(d.x, d.y); +} + +float Gaussian11(int i) +{ + static const float k[11] = { + 0.0093, 0.0280, 0.0650, 0.1210, 0.1760, 0.2000, + 0.1760, 0.1210, 0.0650, 0.0280, 0.0093 + }; + return k[i + 5]; +} + +int BlockerKernelRadius(float sunQuality) +{ + return (sunQuality >= 2.5) ? 5 : (sunQuality >= 1.5) ? 3 : 2; +} + +int FilterKernelRadius(float sunQuality) +{ + // Default tier (quality 1) uses radius 3: radius 2 (5 taps) left penumbrae too + // crisp — with more taps the variable-width PCSS filter reads as genuinely soft. + return (sunQuality >= 2.5) ? 5 : (sunQuality >= 1.5) ? 4 : 3; +} + +float SampleCascadePCF(uint cascade, float2 shadowUV, float refDepth, float softScale) +{ + float smapSize = CascadeSmapSize(cascade); + float ts = (0.6 * max(softScale, 1.0)) / smapSize; + float rd = saturate(refDepth - ReceiverBias(cascade)); + float lit = 0.0; + lit += ShadowCmpLit(float3(shadowUV + float2(-ts, -ts), (float)cascade), rd); + lit += ShadowCmpLit(float3(shadowUV + float2(+ts, -ts), (float)cascade), rd); + lit += ShadowCmpLit(float3(shadowUV + float2(-ts, +ts), (float)cascade), rd); + lit += ShadowCmpLit(float3(shadowUV + float2(+ts, +ts), (float)cascade), rd); + return lit * 0.25; +} + +float FindAverageBlockerDepth( + uint cascade, + float2 shadowUV, + float refDepth, + float searchRadiusUV, + int kernelRadius, + out float blockerCount) +{ + float avgBlocker = 0.0; + blockerCount = 0.0; + int R = clamp(kernelRadius, 1, 5); + + [unroll] + for (int y = -5; y <= 5; ++y) + { + if (abs(y) > R) + continue; + [unroll] + for (int x = -5; x <= 5; ++x) + { + if (abs(x) > R) + continue; + + float2 uv = shadowUV + float2((float)x, (float)y) * searchRadiusUV; + float d = ShadowDepth(cascade, uv); + if (d < refDepth - ReceiverBias(cascade) - 1e-5) + { + float w = Gaussian11(x) * Gaussian11(y); + avgBlocker += d * w; + blockerCount += w; + } + } + } + + if (blockerCount > 1e-5) + return avgBlocker / blockerCount; + return 0.0; +} + +float FilterPCSS( + uint cascade, + float2 shadowUV, + float refDepth, + float filterRadiusUV, + int kernelRadius) +{ + float sum = 0.0; + float wsum = 0.0; + float rd = saturate(refDepth - ReceiverBias(cascade)); + int R = clamp(kernelRadius, 1, 5); + + [unroll] + for (int y = -5; y <= 5; ++y) + { + if (abs(y) > R) + continue; + [unroll] + for (int x = -5; x <= 5; ++x) + { + if (abs(x) > R) + continue; + + float w = Gaussian11(x) * Gaussian11(y); + float2 uv = shadowUV + float2((float)x, (float)y) * filterRadiusUV; + sum += w * ShadowCmpLit(float3(uv, (float)cascade), rd); + wsum += w; + } + } + + return (wsum > 1e-5) ? (sum / wsum) : 1.0; +} + +// Full PCSS: blocker search + penumbra estimate + variable-size weighted filter. +// sunQuality in dev_param_3.y (r2_sun_quality 0..3). Tunables in dev_param_4: +// .x = r2_sun_soft (max penumbra, texels) — how soft shadows get far from the occluder +// .y = r2_sun_blocker (blocker-search spacing, texels) +// .z = r2_sun_contact (min penumbra, texels) — contact sharpness +float SampleCascadePCSS(uint cascade, float2 shadowUV, float refDepth, float viewZ, float midSplit) +{ + float smapSize = CascadeSmapSize(cascade); + float sunQuality = dev_param_3.y; + + float softTexels = dev_param_4.x > 0.0 ? dev_param_4.x : 6.0; + float blockerTexels = dev_param_4.y > 0.0 ? dev_param_4.y : 2.5; + float contactTexels = dev_param_4.z > 0.0 ? dev_param_4.z : 1.0; + + // Lowest tier: a single crisp contact-sized tap. + if (sunQuality < 0.5) + return SampleCascadePCF(cascade, shadowUV, refDepth, contactTexels / 0.6); + + int blockerRadius = BlockerKernelRadius(sunQuality); + int filterRadius = FilterKernelRadius(sunQuality); + + float blockerSearchUV = max(blockerTexels, 0.5) / smapSize; + float blockerCount = 0.0; + float avgBlocker = FindAverageBlockerDepth( + cascade, shadowUV, refDepth, blockerSearchUV, blockerRadius, blockerCount); + + // No occluder standing above this receiver → sharp contact tap, no false blur. + if (blockerCount <= 0.12) + return SampleCascadePCF(cascade, shadowUV, refDepth, contactTexels / 0.6); + + // PCSS penumbra grows with the receiver→blocker gap. The gap is in normalized + // cascade depth (~0.1-0.4 for real occluders), so keep the gain low: a high gain + // saturates penumbra to max for almost everything, widening the filter so much that + // thin shadows (tree trunks/branches) only cover the center tap and wash out to lit. + float gap = max(refDepth - avgBlocker, 0.0); + float penumbra = saturate(gap / max(avgBlocker, 1e-3) * 3.0); + + float cascadeScale = 1.0 + float(cascade) * 0.3; + float maxTexels = softTexels * cascadeScale; + float widthTexels = clamp(lerp(contactTexels, maxTexels, penumbra), contactTexels, maxTexels); + + // widthTexels is the half-extent of the kernel; convert to per-tap spacing so the + // filter always spans ±widthTexels regardless of tap count (smooth, no aliasing). + float filterRadiusUV = (widthTexels / float(max(filterRadius, 1))) / smapSize; + return FilterPCSS(cascade, shadowUV, refDepth, filterRadiusUV, filterRadius); +} + +// Try cascade c with full PCSS; returns lit in [0,1] or -1 if UV/depth invalid. +float TrySampleCascadePCSS(uint c, float3 worldPos, float3 N, float viewZ, float midSplit) +{ + float3 wp = worldPos + NormalOffsetWorld(N, c); + float4 sc = mul(shadow_matrices[c], float4(wp, 1.0)); + if (!CascadeCoordValid(sc)) + return -1.0; + return SampleCascadePCSS(c, sc.xy, sc.z, viewZ, midSplit); +} + +// Cheap 4-tap PCF for cascade blend / rim / grass — avoids a second full PCSS. +float TrySampleCascadePCF(uint c, float3 worldPos, float3 N) +{ + float3 wp = worldPos + NormalOffsetWorld(N, c); + float4 sc = mul(shadow_matrices[c], float4(wp, 1.0)); + if (!CascadeCoordValid(sc)) + return -1.0; + return SampleCascadePCF(c, sc.xy, sc.z, 1.0); +} + +// Depth-selected CSM with smooth split blend + UV rim handoff. +// Full PCSS only on the primary cascade; blends use cheap PCF (was 2–3× PCSS/pixel). +float SampleCSM(float3 worldPos, float3 N) +{ + const uint cascadeCount = 3; + float3 splits = cascade_splits.xyz; + float rimWidth = cascade_splits.w > 1e-4 ? cascade_splits.w : 0.12; + float midSplit = splits.y; + float farSplit = max(splits.z, midSplit + 1.0); + + float viewZ = abs(mul(m_V, float4(worldPos, 1.0)).z); + + // Soft end of shadow distance — keep most of r2_sun_far usable (was 0.72→too early). + float rangeFade = 1.0 - smoothstep(farSplit * 0.88, farSplit, viewZ); + if (rangeFade <= 1e-3) + return 1.0; + + uint primary = 2; + if (viewZ < splits.x) + primary = 0; + else if (viewZ < splits.y) + primary = 1; + + float blendStart = 0.0; + float blendEnd = 0.0; + uint nextC = primary; + if (primary == 0) + { + blendEnd = splits.x; + blendStart = splits.x * 0.65; // was 0.82 — hard pop a few metres ahead + nextC = 1; + } + else if (primary == 1) + { + blendEnd = splits.y; + blendStart = lerp(splits.x, splits.y, 0.65); + nextC = 2; + } + + float litPrimary = -1.0; + uint used = primary; + [unroll] + for (uint c = 0; c < cascadeCount; ++c) + { + if (c < primary) + continue; + float lit = TrySampleCascadePCSS(c, worldPos, N, viewZ, midSplit); + if (lit >= 0.0) + { + litPrimary = lit; + used = c; + break; + } + } + + // Outside all cascade UVs: fully lit (never clamp-sample edge texels — that + // stretched a hard dark disc matching the far-cascade footprint). + if (litPrimary < 0.0) + return 1.0; + + float result = litPrimary; + + // Split-distance blend toward next coarser cascade (cheap PCF) + if (primary < 2 && used == primary) + { + float splitBlend = saturate((viewZ - blendStart) / max(blendEnd - blendStart, 1e-3)); + splitBlend = smoothstep(0.0, 1.0, splitBlend); + if (splitBlend > 1e-3) + { + float litNext = TrySampleCascadePCF(nextC, worldPos, N); + if (litNext >= 0.0) + result = lerp(result, litNext, splitBlend); + } + } + + // UV rim blend into next coarser map (or to lit on far cascade edge) + float4 scUsed = mul(shadow_matrices[used], float4(worldPos + NormalOffsetWorld(N, used), 1.0)); + float rim = saturate(CascadeUVRim(scUsed.xy) / max(rimWidth, 1e-3)); + if (rim < 0.999) + { + if (used + 1 < cascadeCount) + { + float litNext = TrySampleCascadePCF(used + 1, worldPos, N); + if (litNext >= 0.0) + result = lerp(litNext, result, rim); + else + result = lerp(1.0, result, rim); + } + else + { + result = lerp(1.0, result, smoothstep(0.0, 1.0, rim)); + } + } + + return lerp(1.0, result, rangeFade); +} + +// Fast CSM for dense detail/grass: one cascade, 4-tap PCF (no PCSS). +float SampleCSM_Fast(float3 worldPos, float3 N) +{ + const uint cascadeCount = 3; + float3 splits = cascade_splits.xyz; + float farSplit = max(splits.z, splits.y + 1.0); + float viewZ = abs(mul(m_V, float4(worldPos, 1.0)).z); + + float rangeFade = 1.0 - smoothstep(farSplit * 0.88, farSplit, viewZ); + if (rangeFade <= 1e-3) + return 1.0; + + uint primary = 2; + if (viewZ < splits.x) + primary = 0; + else if (viewZ < splits.y) + primary = 1; + + float lit = -1.0; + [unroll] + for (uint c = 0; c < cascadeCount; ++c) + { + if (c < primary) + continue; + lit = TrySampleCascadePCF(c, worldPos, N); + if (lit >= 0.0) + break; + } + + if (lit < 0.0) + return 1.0; + return lerp(1.0, lit, rangeFade); +} + +// Cheap single-tap CSM for volumetrics. Sunshafts ray-march this 20-40× per pixel at +// full res; the PCSS path (blocker search + PCF kernel) there is the dominant cost and +// its soft penumbra is invisible in scattered light. One hardware comparison per probe. +float SampleCSM_Volumetric(float3 worldPos) +{ + const uint cascadeCount = 3; + float3 splits = cascade_splits.xyz; + float farSplit = max(splits.z, splits.y + 1.0); + float viewZ = abs(mul(m_V, float4(worldPos, 1.0)).z); + + float rangeFade = 1.0 - smoothstep(farSplit * 0.88, farSplit, viewZ); + if (rangeFade <= 1e-3) + return 1.0; + + uint primary = 2; + if (viewZ < splits.x) + primary = 0; + else if (viewZ < splits.y) + primary = 1; + + [unroll] + for (uint c = 0; c < cascadeCount; ++c) + { + if (c < primary) + continue; + float4 sc = mul(shadow_matrices[c], float4(worldPos, 1.0)); + if (!CascadeCoordValid(sc)) + continue; + float rd = saturate(sc.z - ReceiverBias(c)); + float lit = ShadowCmpLit(float3(sc.xy, (float)c), rd); + return lerp(1.0, lit, rangeFade); + } + return 1.0; +} + +// Overload without a normal (volumetric march, debug): no normal-offset bias. float SampleCSM(float3 worldPos) { + return SampleCSM(worldPos, float3(0.0, 0.0, 0.0)); +} + +// Debug overlay (r_shadow_debug -> dev_param_3.z). Returns a diagnostic color: +// 1 = selected cascade index (R=0, G=1, B=2, black = outside all cascades) +// 2 = raw SampleCSM occlusion term (grayscale; black = shadowed, white = lit) +// 3 = cascade UV.xy in RG, projected depth in B (should be a smooth ramp) +// 4 = N.L against the sun (grayscale; verifies sun direction independent of maps) +float3 ShadowDebugColor(float3 worldPos, float3 N, float3 L, int mode) +{ + int used = -1; + float4 scUsed = float4(0, 0, 0, 0); + [unroll] + for (uint c = 0; c < 3; ++c) + { + float4 sc = mul(shadow_matrices[c], float4(worldPos, 1.0)); + if (CascadeCoordValid(sc)) { used = int(c); scUsed = sc; break; } + } + + if (mode == 1) + { + if (used == 0) return float3(1, 0, 0); + if (used == 1) return float3(0, 1, 0); + if (used == 2) return float3(0, 0, 1); + return float3(0, 0, 0); + } + if (mode == 2) + { + float s = SampleCSM(worldPos, N); + return float3(s, s, s); + } + if (mode == 3) + { + if (used < 0) return float3(0, 0, 0); + return float3(saturate(scUsed.xy), saturate(scUsed.z)); + } + // mode == 4 + float ndl = saturate(dot(normalize(N), normalize(L))); + return float3(ndl, ndl, ndl); +} + +#endif // CSM_SHADOW_FORWARD + #ifndef CSM_SHADOW_FORWARD - return 1.0; -#else - float smapSize = dev_param_3.x > 0 ? dev_param_3.x : 4096.0; - float2 texelSize = 1.0 / float2(smapSize, smapSize); - - [unroll] - for (uint c = 0; c < 3; c++) - { - float4 shadowCoord = mul(shadow_matrices[c], float4(worldPos, 1.0)); - float2 shadowUV = shadowCoord.xy; - float depth = shadowCoord.z; - - if (all(shadowUV > 0.0) && all(shadowUV < 1.0) && depth >= 0.0 && depth <= 1.0) - { - float shadow = 0.0; - [unroll] - for (int y = -1; y <= 1; y += 2) { - [unroll] - for (int x = -1; x <= 1; x += 2) { - float2 offset = float2(x, y) * texelSize * 0.5; - shadow += g_ShadowMapArray.SampleCmp( - smp_shadowcmp, float3(shadowUV + offset, c), depth); - } - } - return shadow * 0.25; - } - } - - return 1.0; -#endif +float SampleCSM(float3 worldPos, float3 N) +{ + return 1.0; +} + +float SampleCSM(float3 worldPos) +{ + return 1.0; +} + +float SampleCSM_Fast(float3 worldPos, float3 N) +{ + return 1.0; +} + +float SampleCSM_Volumetric(float3 worldPos) +{ + return 1.0; } +float3 ShadowDebugColor(float3 worldPos, float3 N, float3 L, int mode) +{ + float ndl = saturate(dot(normalize(N), normalize(L))); + return float3(ndl, ndl, ndl); +} +#endif + float SampleHUDShadow(float3 worldPos) { #ifndef HUD_SHADOW_FORWARD - return 1.0; + return 1.0; +#else + // shadow_matrices[3] = dedicated HUD light-VP (clip→UV) + float4 shadowCoord = mul(shadow_matrices[3], float4(worldPos, 1.0)); + float2 shadowUV = shadowCoord.xy; + float depth = shadowCoord.z; + + if (any(shadowUV < 0.0) || any(shadowUV > 1.0)) + return 1.0; + + float hudSmapSize = 1024.0; + float ts = 0.6 / hudSmapSize; + float lit = 0.0; + lit += g_HUDShadowMap.SampleCmp(smp_shadowcmp, float3(shadowUV + float2(-ts, -ts), 0), depth); + lit += g_HUDShadowMap.SampleCmp(smp_shadowcmp, float3(shadowUV + float2(+ts, -ts), 0), depth); + lit += g_HUDShadowMap.SampleCmp(smp_shadowcmp, float3(shadowUV + float2(-ts, +ts), 0), depth); + lit += g_HUDShadowMap.SampleCmp(smp_shadowcmp, float3(shadowUV + float2(+ts, +ts), 0), depth); + return lit * 0.25; +#endif +} + +// Spot / OMNIPART local shadows. tilePlusOne = asuint(spotParamsAndType.w): +// 0 = no atlas tile, else slice = tilePlusOne - 1. spotVP is clip-space (same as +// projector sampling); UV uses Y-flip to match D3D→Vulkan viewport. +float SampleLocalShadow(float3 worldPos, float4x4 spotVP, uint tilePlusOne) +{ +#ifndef LOCAL_SHADOW_FORWARD + return 1.0; #else - float4 shadowCoord = mul(shadow_matrices[3], float4(worldPos, 1.0)); - float2 shadowUV = shadowCoord.xy; - float depth = shadowCoord.z; - - if (any(shadowUV < 0.0) || any(shadowUV > 1.0)) - return 1.0; - - float hudSmapSize = 2048.0; - float2 texelSize = 1.0 / float2(hudSmapSize, hudSmapSize); - - float shadow = 0.0; - [unroll] - for (int y = -1; y <= 1; y += 2) { - [unroll] - for (int x = -1; x <= 1; x += 2) { - float2 offset = float2(x, y) * texelSize * 0.5; - shadow += g_HUDShadowMap.SampleCmp( - smp_shadowcmp, shadowUV + offset, depth); - } - } - return shadow * 0.25; + if (tilePlusOne == 0) + return 1.0; + + float4 sc = mul(spotVP, float4(worldPos, 1.0)); + if (sc.w <= 1e-4) + return 1.0; + + float3 ndc = sc.xyz / sc.w; + float2 uv = ndc.xy * 0.5 + 0.5; + uv.y = 1.0 - uv.y; + float depth = ndc.z; + + if (any(uv < 0.0) || any(uv > 1.0) || depth < 0.0 || depth > 1.0) + return 1.0; + + // Classic KERNEL 0.6 4-tap PCF. Soft UV rim only (like accum_sun_far edge fade) — + // hard [0,1] reject made shadows pop on/off right in front of the camera as the + // light frustum swept the floor. Keep rim narrow so long mid-frustum shadows stay. + uint slice = tilePlusOne - 1; + float ts = 0.6 / 1024.0; + float rd = saturate(depth - 0.0003); + float lit = 0.0; + lit += g_LocalShadowAtlas.SampleCmp(smp_shadowcmp, float3(uv + float2(-ts, -ts), slice), rd); + lit += g_LocalShadowAtlas.SampleCmp(smp_shadowcmp, float3(uv + float2(+ts, -ts), slice), rd); + lit += g_LocalShadowAtlas.SampleCmp(smp_shadowcmp, float3(uv + float2(-ts, +ts), slice), rd); + lit += g_LocalShadowAtlas.SampleCmp(smp_shadowcmp, float3(uv + float2(+ts, +ts), slice), rd); + lit *= 0.25; + + float2 edge = min(uv, 1.0 - uv); + float rim = smoothstep(0.0, 0.05, min(edge.x, edge.y)); + return lerp(1.0, lit, rim); #endif } diff --git a/res/gamedata/shaders/r5/shared/sky_ibl.h b/res/gamedata/shaders/r5/shared/sky_ibl.h new file mode 100644 index 00000000000..80f0252196c --- /dev/null +++ b/res/gamedata/shaders/r5/shared/sky_ibl.h @@ -0,0 +1,83 @@ +// sky_ibl.h — IX-Ray-style sky cubemap IBL (fake irradiance + rough specular) +#ifndef SKY_IBL_H +#define SKY_IBL_H + +#ifndef SKY_IBL_CUBES_DECLARED +TextureCube s_env0 : register(t25); +TextureCube s_env1 : register(t26); +#define SKY_IBL_CUBES_DECLARED +#endif + +// L_ambient.w = skybox lerp; parallax.w = IBL intensity (0 = classic ambient) +float3 SampleSkyRGB(float3 dir) +{ + float3 d = normalize(dir); + float3 s0 = s_env0.SampleLevel(smp_rtlinear, d, 0.0).rgb; + float3 s1 = s_env1.SampleLevel(smp_rtlinear, d, 0.0).rgb; + return lerp(s0, s1, saturate(L_ambient.w)); +} + +// Fake irradiance: N + hemisphere taps (cubes often lack useful mips) +float3 SampleSkyIrradiance(float3 N) +{ + N = normalize(N); + // Orthonormal basis around N + float3 up = abs(N.y) < 0.999 ? float3(0, 1, 0) : float3(1, 0, 0); + float3 T = normalize(cross(up, N)); + float3 B = cross(N, T); + + float3 irr = SampleSkyRGB(N) * 0.30; + irr += SampleSkyRGB(normalize(N + T * 0.55)) * 0.14; + irr += SampleSkyRGB(normalize(N - T * 0.55)) * 0.14; + irr += SampleSkyRGB(normalize(N + B * 0.55)) * 0.14; + irr += SampleSkyRGB(normalize(N - B * 0.55)) * 0.14; + irr += SampleSkyRGB(normalize(N + T * 0.35 + B * 0.35)) * 0.07; + irr += SampleSkyRGB(normalize(N - T * 0.35 + B * 0.35)) * 0.07; + return irr; +} + +float3 SampleSkySpecular(float3 R, float roughness) +{ + // Fake LOD: bias sample direction toward N for rough materials + float lodMix = saturate(roughness * roughness); + // Remap: keep some sky color even when rough (IBL_FAKE_IRRADANCE style) + float3 dir = normalize(lerp(R, float3(R.x, abs(R.y) + lodMix * 0.35, R.z), lodMix)); + float3 spec = SampleSkyRGB(dir); + // Soften with irradiance for rough + if (lodMix > 0.05) + spec = lerp(spec, SampleSkyIrradiance(dir), lodMix); + return spec; +} + +float3 PBRAmbientIBL( + float3 albedo, + float3 N, + float3 V, + float metallic, + float roughness, + float ao, + float hemiOcclusion, + float intensity) +{ + float3 F0 = CalculateF0(albedo, metallic); + float NdotV = max(dot(N, V), 0.0); + float3 F = F_SchlickRoughness(NdotV, F0, roughness); + + float3 irradiance = SampleSkyIrradiance(N); + // Weather tint / night scale from classic constants + float3 weatherScale = L_ambient.rgb + L_hemi_color.rgb * L_hemi_color.w; + irradiance *= weatherScale * intensity; + + float3 kD = (1.0 - F) * (1.0 - metallic); + float3 diffuseAmbient = kD * albedo * irradiance; + + float3 R = reflect(-V, N); + float3 specEnv = SampleSkySpecular(R, roughness); + specEnv *= weatherScale * intensity; + // Specular ambient strength scales with gloss + float3 specularAmbient = F * specEnv * (0.25 + 0.75 * (1.0 - roughness)); + + return (diffuseAmbient + specularAmbient) * ao * saturate(hemiOcclusion); +} + +#endif // SKY_IBL_H diff --git a/res/gamedata/shaders/r5/shared/ssr.h b/res/gamedata/shaders/r5/shared/ssr.h new file mode 100644 index 00000000000..9be6c104d18 --- /dev/null +++ b/res/gamedata/shaders/r5/shared/ssr.h @@ -0,0 +1,306 @@ +// r5/shared/ssr.h — Screen-space reflections (X-Ray +Z view depth) +// Hard frustum miss made ALL wet pixels snap together (parallel R) — sky or geo. +// On border exit: clamp UV + fade conf, never binary zero. +#ifndef SSR_H_R5 +#define SSR_H_R5 + +#define SSR_EDGE_ATTENUATION 0.10 +#define SSR_VERTICAL_FADE 4.0 + +#if !defined(SSR_QUALITY) + #define SSR_QUALITY 1 +#endif +#if (SSR_QUALITY <= 1) || (SSR_QUALITY > 4) + #define SSR_SAMPLES 16 + #define SSR_DISTANCE 80.0 + #define SSR_THICKNESS 1.5 + #define SSR_REFINE 2 +#elif SSR_QUALITY == 2 + #define SSR_SAMPLES 24 + #define SSR_DISTANCE 120.0 + #define SSR_THICKNESS 1.25 + #define SSR_REFINE 4 +#elif SSR_QUALITY == 3 + #define SSR_SAMPLES 32 + #define SSR_DISTANCE 160.0 + #define SSR_THICKNESS 1.0 + #define SSR_REFINE 6 +#else + #define SSR_SAMPLES 48 + #define SSR_DISTANCE 220.0 + #define SSR_THICKNESS 0.85 + #define SSR_REFINE 8 +#endif + +Texture2D g_SceneColor : register(t20); +Texture2D g_SceneDepth : register(t21); + +float RayAttenBorder(float2 pos, float value) +{ + float borderDist = min(1.0 - max(pos.x, pos.y), min(pos.x, pos.y)); + return saturate(borderDist > value ? 1.0 : borderDist / value); +} + +bool WorldToUv(float3 worldPos, out float2 uv, out float w) +{ + uv = 0.0; + float4 clip = mul(m_VP, float4(worldPos, 1.0)); + w = clip.w; + if (w <= 1e-4) + return false; + float2 ndc = clip.xy / w; + uv = float2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5); + return true; +} + +float3 ReconstructWorld(float2 uv, float rawDepth) +{ + float2 ndc = float2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0); + float4 worldH = mul(m_InvVP, float4(ndc, rawDepth, 1.0)); + return worldH.xyz / max(worldH.w, 1e-5); +} + +float SampleSceneViewZ(float2 uv, out float rawDepth) +{ + rawDepth = g_SceneDepth.SampleLevel(smp_nofilter, uv, 0).x; + float3 worldPos = ReconstructWorld(uv, rawDepth); + return abs(mul(m_V, float4(worldPos, 1.0)).z); +} + +float4 compute_ssr_ex( + float3 position, + float3 normal, + float3 skybox, + int steps, + float maxDist, + float thickness, + int refineCount, + float roughnessBlur) +{ + float3 N = normalize(normal); + float3 V = normalize(position - eye_position); + float3 R = reflect(V, N); +#ifdef SSR_WATER_DOWNWARD_BIAS + if (R.y < -0.2) + return float4(skybox, 0.0); +#endif + +#ifdef SSR_OPAQUE_REFLECTOR + if (N.y > 0.5) + { + N = normalize(lerp(N, float3(0.0, 1.0, 0.0), 0.35)); + R = reflect(V, N); + R.y = max(R.y, 0.02); + } +#endif + + R = normalize(R); + steps = clamp(steps, 8, 64); + refineCount = clamp(refineCount, 1, 8); + maxDist = max(maxDist, 8.0); + thickness = max(thickness, 0.35); + + float3 origin = position; + float2 startUV = 0.0; + float startW = 1.0; +#ifdef SSR_OPAQUE_REFLECTOR + origin = position + N * 0.45; + thickness = max(thickness, 2.0); + steps = max(steps, 40); + maxDist = max(maxDist, 180.0); +#endif + WorldToUv(origin, startUV, startW); + + float tMin = max(0.5, maxDist / float(steps) * 0.2); +#ifdef SSR_OPAQUE_REFLECTOR + tMin = max(tMin, 1.25); +#endif + + float2 hitUV = 0.0; + bool hit = false; + float hitConf = 0.0; + bool borderFade = false; + + float tPrev = tMin; + float2 prevUV = startUV; + { + float3 p0 = origin + R * tMin; + float pw; + WorldToUv(p0, prevUV, pw); + } + + [loop] + for (int i = 1; i <= steps; ++i) + { + float u = float(i) / float(steps); + float t = max(tMin, maxDist * u * u); + float3 marchPos = origin + R * t; + + float2 uv; + float w; + if (!WorldToUv(marchPos, uv, w)) + { + // Behind camera: keep last on-screen sample with soft conf (no hard cut) + if (tPrev > tMin && all(prevUV >= 0.0) && all(prevUV <= 1.0)) + { + hitUV = prevUV; + hitConf = 0.35; + hit = true; + borderFade = true; + } + break; + } + if (any(uv < 0.0) || any(uv > 1.0)) + { + // Left frustum: clamp to edge — all wet pixels share ~same R, so a hard + // miss here snaps the WHOLE screen (sky or geo). Keep edge sample. + if (tPrev > tMin) + hitUV = saturate(prevUV); + else + hitUV = saturate(uv); + hitConf = 0.3 * RayAttenBorder(hitUV, 0.08); + hit = true; + borderFade = true; + break; + } + + float rawDepth; + float sceneVZ = SampleSceneViewZ(uv, rawDepth); + float marchVZ = abs(mul(m_V, float4(marchPos, 1.0)).z); + + if (rawDepth >= 0.9995) + { +#ifdef SSR_ACCEPT_SKY + if (t > tMin * 1.25) + { + hitUV = uv; + // Low conf — sky sample stays dim via fill lerp (avoid plate washout) + hitConf = 0.22; + hit = true; + break; + } +#endif + prevUV = uv; + tPrev = t; + continue; + } + + float delta = sceneVZ - marchVZ; + if (delta <= thickness && delta >= -thickness * 5.0) + { +#ifdef SSR_OPAQUE_REFLECTOR + if (length(uv - startUV) < 0.028) + { + prevUV = uv; + tPrev = t; + continue; + } +#endif + float2 uvA = prevUV; + float2 uvB = uv; + float distA = tPrev; + float distB = t; + [loop] + for (int r = 0; r < refineCount; ++r) + { + float midT = 0.5 * (distA + distB); + float2 uvM = 0.5 * (uvA + uvB); + float dM; + float sVZ = SampleSceneViewZ(uvM, dM); + float mVZ = abs(mul(m_V, float4(origin + R * midT, 1.0)).z); + if ((sVZ - mVZ) <= thickness) + { + uvB = uvM; + distB = midT; + } + else + { + uvA = uvM; + distA = midT; + } + } + hitUV = 0.5 * (uvA + uvB); +#ifdef SSR_OPAQUE_REFLECTOR + if (length(hitUV - startUV) < 0.028) + { + prevUV = uv; + tPrev = t; + continue; + } +#endif + float dHit; + float sHit = SampleSceneViewZ(hitUV, dHit); + float mHit = abs(mul(m_V, float4(origin + R * (0.5 * (distA + distB)), 1.0)).z); + float err = abs(sHit - mHit); + hitConf = saturate(1.0 - err / max(thickness * 2.5, 1e-3)); + hit = hitConf > 0.04; + break; + } + else if (delta < -thickness * 5.0) + { + // Occluded: soft keep last UV instead of hard kill (reduces yaw snap) + if (tPrev > tMin * 1.5 && all(prevUV >= 0.0) && all(prevUV <= 1.0)) + { + hitUV = prevUV; + hitConf = 0.25; + hit = true; + borderFade = true; + } + break; + } + + prevUV = uv; + tPrev = t; + } + + if (!hit) + return float4(skybox, 0.0); + + // Always soft-fade near screen border (even if SSR_NO_EDGE_ATTEN for water) + float edge = RayAttenBorder(hitUV, borderFade ? 0.14 : SSR_EDGE_ATTENUATION); +#ifdef SSR_NO_EDGE_ATTEN + if (!borderFade) + edge = max(edge, 0.85); +#endif +#ifdef SSR_NO_VERTICAL_FADE + float vertFade = 1.0; +#else + float vertFade = saturate(hitUV.y * SSR_VERTICAL_FADE); +#endif + float conf = edge * vertFade * hitConf; + + float2 texel = screen_res.zw; + float br = saturate(roughnessBlur) * 6.0; + float3 img = g_SceneColor.SampleLevel(smp_rtlinear, hitUV, 0).xyz; + if (br > 0.01) + { + img += g_SceneColor.SampleLevel(smp_rtlinear, hitUV + float2( br, 0) * texel, 0).xyz; + img += g_SceneColor.SampleLevel(smp_rtlinear, hitUV + float2(-br, 0) * texel, 0).xyz; + img += g_SceneColor.SampleLevel(smp_rtlinear, hitUV + float2(0, br) * texel, 0).xyz; + img += g_SceneColor.SampleLevel(smp_rtlinear, hitUV + float2(0, -br) * texel, 0).xyz; + img *= 0.2; + } + float hitLum = dot(max(img, 0.0), float3(0.2126, 0.7152, 0.0722)); +#ifdef SSR_KEEP_DIM_HITS + float keepDim = step(1e-5, hitLum); + conf *= keepDim; + img *= keepDim; +#else + if (hitLum < 1e-4) + return float4(skybox, 0.0); + conf *= saturate(hitLum * 6.0 + 0.2); +#endif + // On miss-like border: blend toward skybox so content doesn't pop to black + if (borderFade) + img = lerp(skybox, img, saturate(conf * 1.5)); + return float4(img, conf); +} + +float4 compute_ssr(float3 position, float3 normal, float3 skybox) +{ + return compute_ssr_ex( + position, normal, skybox, + SSR_SAMPLES, SSR_DISTANCE, SSR_THICKNESS, SSR_REFINE, 0.0); +} + +#endif diff --git a/res/gamedata/shaders/r5/shared/wmark.h b/res/gamedata/shaders/r5/shared/wmark.h index c997718f46e..7159efa7896 100644 --- a/res/gamedata/shaders/r5/shared/wmark.h +++ b/res/gamedata/shaders/r5/shared/wmark.h @@ -21,4 +21,4 @@ float4 wmark_shift (float3 pos, float3 norm) P -= normalize(eye_direction + normalize(P-eye_position)) * s; return float4 (P,1.f); } -#endif \ No newline at end of file +#endif diff --git a/res/gamedata/shaders/r5/sky_forward.ps b/res/gamedata/shaders/r5/sky_forward.ps index b7a9ec834a04f7d5ede0a636ddfe8b5cb575905b..9c1dbffeb6bee8408152dc8dd5e0febeccc6a8c0 100644 GIT binary patch literal 1443 zcmZ`(&2HL25Wf2ZuP<^$GJN zon4#8p(+>bou8fgejEgFlxxOQCWB2*@J|IhAs>lM1GR7B?%A)E69ll?H;{^o!OM@| zKue-jL18XdP!pYliv^dM=}kZvT*C#>L&7Rj<5TF5Mg!AA=ZwRF7rTO`par*aQrBg{ z6evnc2JWfzL1B(PySRjxpTB@Jjh)70&v%^n1*hd9W$;erM6xuXqN)V<^LM>adloBF z)g`WGf{xPiKsBs3@#22=FJJ=l!b4wqBsdFnV@cGL&S=12ml7^ezxE6K7#X(nAAdeeVXqBkiKrWn%h zge;x}hn>hQ^%c=HN4`>QML1Xy5cq!XSx) z|a`z@CFsdh(5WVv&M(G8&78s?cSUot-N0F*vffMzH)-X0Gu)Eq_YDZQ6duA8>nU)K% zJ8$OAdoy7OJC!0YQH5E>=%vH8P+O+TP`|$A!*?LH7lx3%zCkHk1X=H}0qp=lG4Rl1 zzJYSNYQCX(2Vt#jP=nbDD7qE4OoG9k=@cC@Uc#y&goYKGEg}a~VQXsSO&>`%?3w=}LqvBBZbk3(>ZM2i4WdRezRiM@MOU>1m_7!oVWS z*68?HX&$%$z5%5~k`E>)X|*Jvu5RsN)p$2w%4{SEi{sGBt)cAPGl-+N@4Yy0#f3YlTHd z|3^jx(iJ6Aw{|QKQ%P9Rh8db!pEa^#2P3ywZz=<8C0aoKr&fZu$j#_TzIXh=8BVY` zaAOm&OO|xkppw3Ab?j)5PoY*|J5z8=dudG>exlDQo_X7h1J uqfh$@jOx@_LdR>`@s?`Vw4j2!C6-LC@13gT)y7)*o80(54vS>qT38N4nMYL$D1kra|nRs#N^G zI7rvGuN3`WQDKF!qW4|Pd&w)i2hTJL%NMaFfT0hG-4+blEbr#m*Efq+_?9D)T&jkn3m7-4+kk>F;dc3fiLx@ICgFytoEpDh zQ+;x?Cd#X+JdGOr;Fyl$VHCRR>5w1Z9Nv)6paoKUkARX=aZQo xcqjQ&vvdibpxgM*z=}PFbGVpJJ%7~lDrzbv+L~PCMNMlBPjD=gDMmdx{{U|xr9}V$ delta 124 zcmaFP^@7M(vA99YcUrN>M7cp)^P>MInv)X07cptg_y~|V)cS?RGw00AnX~S6z?cP(Sy_s0R2umG^B1J5D3#$BVLtyn0WPhA>(LFE6eUtr zDBXIUVb1B_{A>I+?zUcYnTjeyctzks$!Phi@y;xn#;j8&I!<(!yI|`UH@4E)8^I#J za+L56G%mQYr~}(=505f@BS#r$&oZV;7fc+b^q!@BWCr0VqfZ%~4%h*31D4&Rgk%dQ zrcNQ(s&G=!G_hetDNvhe3Bii9-Ro%Cj^I0SGD=1SBa{stpR!BTYfcf3xxmzNCFxiy zZK=!vnAz1}G6Twmgv*S}1sJrh6M;GXO1B4r7Na96Mp)YA(G=%2MJr9JJV)(U_Q7p) z#xyHTGch@Dw;o$SPc9U56)Z~H^LF_6Wgp^_Wy97JQ5vhO)WU2YPo}f)xA0iw20-p0 z-uYcTb2g#jhIOO|)rtbQpMG3@`*t(x4#Cm*Vgo1`m%)K+(yke9nO>l6w$-m42A6wd zM02nH7hItTOi{*yjK_2clc-vS&}~P0ku*jC&DOO@j>4Z_P)L{WLm=4MA2sZ03Gt?CfL1L6An+^l{!@I!_-aWKQR0OgZ*rTJx;qjO2aM2{yXY! z|LLJNL7LR#E`hF?NGriCj=MpE| z(zfq!_In9DJcKa`eOOkLzYq5#LI$)7>Ui9y4eZX1ZhZIN`0$2I$|;FZCwuFiJpZoz J*i3pp~PbUNKcfk&vXD-j-w&b~nA!Qt(Ik3;iX| zW|KA%QJ2D&oq2C&-n(#)|A^$;o&Qn?Qj8o&zTvDv;2WuH|(y=A@7hos1D? z7;#i43DH$wVGGNUhnZ;SMYoo5GyqSM=Aj&eBzuzj^gz7!Yfyx1&p08aki3i~Ed5~d ze)(|)M-7}nkt<^VQ?Sehl{9BwLs{-HuzK_P_5J;ax$A+Vsj5vN3n??tTnkGxtU|sa zvfUTNgyG;{V1{y3!#aML6)Aa3b|f`LRs<|hXo@*~SUBab53c3x^$autv%$Zdf$L1C zQyg0jtqvzBG^@ujT}0}i5>UnKRy9M_fZE4@)Nd*H1Vou3R+Bh<6SacysDmTNtxJ9_ z8PC)7o34+xF)5Qkkzg?P@P07^yltE6DURn^I>4&=2E47o+cS83k>En0FiJ11g-XHA z4ImKzU2;{1I&sbi{Pp5T_0Q7lHM9wSpZy z94h#~4AQknm*G)SkbPke!bq407&O*Xy+3mu_2o4Pbyx1d4K^s(xy8il6-DgfvTT&F zhqM0~h&U|SLxwJ|vV=VYp}eBP-Du?)47Z+7Ra+NjQ# Iv9Xi!3$0dC6#xJL literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/ssgi_temporal.ps b/res/gamedata/shaders/r5/ssgi_temporal.ps new file mode 100644 index 0000000000000000000000000000000000000000..0128540c20c98d8a92316d32aed83e4151fc0e5d GIT binary patch literal 1367 zcma)6%Wm5+5WMRv7Rg0Q9QgsKfa@kek@R68X^hG?auaB2EwhkBfuvl!Nq~Mtzp!7@ zrK!kLdPsaSsM*=wnIXGfP%7b}#;g#MrGBB{@9#fwy8wxW6hAQ1a0)S3B8t*dagoC^ z#XJT#2tMD#T$93i+3--SKrnCDaxBOFB{%S7(4j~r))TU|;VTtgCG zl~Q7^;XSoVxFQl)_q=fn^Oeqp+_fh|PBasi5w4NRju@yiX#P;WMbh-@K5?T;U@tVr<&H<-FAE5cl?cuH<{ zpxku8I969fy!XOX?_GJd*5BG{z5>_QUS2@YAB{%m@c7t}6F$%}nP6I(pF~<9-w#V7 zn3Kww%sOpGu%^FZhFxQY0jw!=^94yjN}Yn`YXGW~>6Q?56B504tDYx~R8*>%y;#0; zD^9aS&oi4f4lki!9aS- zfj*v{d#%7xD5d{zRUvbrVfN8&EUDK`qS;Comx0t=m<^>;-dEl!a^V XhA)$4XD__{XKB68K8rr1;wR@Hb6K*J literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/ssgi_trace.ps b/res/gamedata/shaders/r5/ssgi_trace.ps new file mode 100644 index 0000000000000000000000000000000000000000..8048905d99757fb6d17c89ae8aa71fcefaec1ff7 GIT binary patch literal 4000 zcmbVP|8Cnh5dPm!achA=P8`LyoVHn-4$TiV0X8?Noo>J|7_>~=LS;!Kr8sU^U=Oh; z+>`7(lKPco*f1lAE#C2Xci-Ld)8Qd$y^3bJ5~1`nO@ICIC+Sd0nH=a0YxM4#Rw7?Z zMP8WdD3P?5n@DF^k+cyiTz5Oq7jnsx$&a%avwr7!l!S4ykn}7}H=8u^*UuVjdM%V( z9BQq2y)%{fd7yKhG)@)KcckPh(z#S#-tV5YRK}^=h`6OPc%X77w4}C`Ayf_z zal#~&iL}R5j~6n_*R)jW2DyM!Bb3F8V)VNN%b+4cD5c1mBY>WH$GJ)C@;-2kjSN|O^S8B%cv!SwRy^Y`yR zT=Y+f%~-dugkBGZo_QEj)`}^hETB_lM}$@(@G}2?ci*Kw>idI!z}e>vKyCk&7HRHf zK@U&4~( z8B7jd`h)h$@xXuCwbqf>b5q6#TN$m^xm+~sS&K~;OA-noENE==6VSvgUsZMQGC|{B zncVxRl{L!N5wiBeAHE6(uZ{u)cpv%qO{mdK)3wY6)bwvE_aNW7JXuuGYb}L=-=_mM z(^3nl$Y4UuwKwKWz6L7C$H(-&^(^rEJsR|=-v#)_Zs74FuQa>~XbK~V>8its$%;UmC9vIy+mM-8qV;L5nl8qrN69e02HH>J= z^NE|VO#L=h@d7pO5C|Gr6n7;SVVY>um4wF9?0F0%Jr8}_EnBTe!ydhIfz%r?eJ~AA zX)v_mTZuykvI)`zaih8E&?eY3TVb%7HW7x!2Ks%!-$*Cf8l2Q3v*(vMO|xI^cC_?TmWGE+$Pdf3{QZL=}{hS zq=F6>&_ZbEa%IaZ=&3IDTkrWaE@&KNCWHCyEL)R(|Ha@{KM0O%Gdq|;{N=!?JPv3P zad?lq3>AjHa*1rHn_CCiV^3KIIE8Bm=8y1`?QxNnIN^8SAF_no$LJlEb6NfCj{Rn9VUnOG^kM~kofzbfbD3?I8*8oxjNK+_`q_#-1UX5mT#E^7x zSLeN2r)_&NT9Wt8Puv&N^!Cmxv()45J=%8bL)Zj==1PiBkBx|A=V6bTB^VsP|4!w~ z8^RR&XG2x+Y}`&8R3C0!fYm<6Hj)zCi>KDj9+y>H73SV9uTgdmQ8~jsG{pA8H83B{ zAI=3Y(;Eg20}(g1mDvAZT2H)Irpw%7rhdxuX?w`J7zTNi6pcFJRZJ`XDnB+vW7dTd z%Vd?WD_}6qsPHh~ZToR2x-SfY7jn@3h7Ld|OyQ z$U7N(dXvp!Fj%2_Hcq}DeYKjmgQzVUj>!i3$Nw<}9`a)pFG$35xkxc-G2^#UzD|pr zmPH%`yG^<%V!<4aJaLys3-CcH7jw`+n#bw308`_%mLKQc9NY!G{geZ-f$vc)RpxF^ zzW(_S3`q;{F_BA7q6r9t-#>z&?*~UumZQi)aA8`7na6x&zZ!pL_Ku$1G*8t66#A4t zh}~N@aAp?x)Z33eQ0ot~%ZqoHv$xaf<@omP^!okyNjqDZunz2DVzbRQI`)nQ33VCR zdpIEL+ZIZV;c1J|6&4p&t@xS-@?ccwxye6A%&pv?bGR+G4jtCFJkpNw+2l1F0U}hf z_-kfq;O@8C}T%E%aLfTJTVsAzPCa% zWoF82M2YPiV6mFnm$Cogg9XYJ=oouzs_5*2S}_0TS~hA!N){m$?Ssd`7-ljwEEs7t zxx@%H628+RX&M+=Zg2$~nBM;W?#lh*YxHNYMHM4Le%4RrA%7p@2$TOx!W6i02FKoj!bPbU3Ucu9Lq3IlEyV literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/ssr_apply.ps b/res/gamedata/shaders/r5/ssr_apply.ps new file mode 100644 index 0000000000000000000000000000000000000000..d4c40c9de0a5ac039ad837c640f9872cf6f9456c GIT binary patch literal 769 zcmaiy(QeZ)7=`ci6i4DFfsmwI$HfX)8%ULaRdWZsi`>jftU7UII}Ot`iHG0`c@lmn z?P$dXcb0w5(f1$!X$osi&bv-MCY{CapTEG^r+XB-*~nI0LJ8A_g;v_cUK*oP+S>0z zR<(;peXB8WTkGBhuO386F0e28yF3d5#+!};nBz{sHeKH8QYt45E@4C^EgeIfMT>ux zigLICPLwlJ25a%d^+udsqsp&5*s;Xss!e*@$)hA1YrpX53C12s4EhX4Qo literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/ssr_blur.ps b/res/gamedata/shaders/r5/ssr_blur.ps new file mode 100644 index 0000000000000000000000000000000000000000..27645eb6f90aeb5346ef631c245fcf2654a2c67d GIT binary patch literal 2188 zcma)7?P}Xd6#eg~xUf(pTUO*)*{#=2p_}ZFz%~t4T1v_y#`f5mAdl3UQS2rJ`Vf7> zK1uJL(PtXk8e<9RoO3_sp6i1H&|0O7tW;s4;oFz5pz?BaExFcXwquIJLS)Qv#WL`2 zr!)9evP_uI{@{-lUkk}0d7b{5MuS1ZwnfHOVh{~_Tck2yiwv3YHz>Xl8gD%v`STx< z8R{?v&_hlTIk@C|Q!0LV3Y#=Z-Xf%nF{IXoobtk4_aP+Hg`DSVi?wueGKUmtxhU6b zOcta3gWb4bifwflwut<}{eUu9XE`&6VECSAiMZqQ_9+0h@=O{oHFl}@CN0=%K6t=b z+Ng4A;37%S&M)3w!o8D>p~xv@@;<%DkuGvMuY;Ah_~P>44<~QlygiNPKr%Lu7`9AE zuhBa5ox&wk8*aK8$FSs>n1W;1%+Q_KfBcqbUiZKPI19_$0I0i7y%qV&vTdJ#gCFjl zf>?v+f`<%VKomyNbKh!{zT$Ysvg&~sVPN;I!nforczKNE*=%<71KY`CSDK7tkS8`< zcjUj3Zl~NSoG+0q?X=(-d-5dQf4+keO!)Yy*OQ-nCdl&}W%TBV<{v1NW?17USYxX!Z!+yCZY`=qbcN2Wgk! zPX@W`+L}(u>!)m21AdY>+}WVNvbzggo4wYxWulKL@zzJ1^p|$5 zY>UQW8RAfma8!nP#!nNGhb0VQC+SDyPZO4oYY1qkM@+KD2$xLg?6d19WSxVrJC7f% z4<0(3Woq58_zJ_5+yVZ5>^-8Q*ZDZEBHA)QUo%y_7JI_hi_Pt^p|dz<88un<*SOs| Wv}rI_P&*y-4jNUzdCV~C!{9r75qc;9 literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/ssr_resolve.ps b/res/gamedata/shaders/r5/ssr_resolve.ps new file mode 100644 index 0000000000000000000000000000000000000000..ba57750babcfeb8e16d8694bf505b162796c9fde GIT binary patch literal 4713 zcmbtY+j84R5`E`abR{pKO+h36zsc01quav8*uX7}!?+poXQrtXiQ zukLQ=KaHYY`s2jCn$IT> zt(~j!c=9xNXPb0v)S}XXb~k%ixwx{ z&MzHyWf>}wl2+Mj9m^ycXQ^5)FB)4xoXR)}(tUGrEt0~|=IRpH$lKVLFU{3RXm34B zZJ9DLu{H z+lQymbNVHZ$cQ$IBR2cuJ}G#QbY0{-L&~<;m`{EhKR!;c`xnG&Oc))St61Iysa1T{ zt3@l$4taD*k$ADN)1$u4Aml7)39QE0Jtk{l_vtM@zM+B5nf%u293RssDL1W3UuqT6 zB2>E|UXjXDO4pJ$p-3fJ$@3nX)K=<+4p!@w5>Lh`^}?*N*i6A0|JlmY751SSyD*F3 zgRfG{>*Wz~tiC-Q3{Tmiv%$&GICeH1b`PCWr6c&KZO!vwPdIyTtEiL}^V%T2mA z5twnZ&DU4A|6UaGBQ$vd* zZK6B`q>lh+$S}H(Ob+Zl9KlW#T_f5l9r`!W5oMtjxwrB5%Ts}~kp=C&%2%VS#F)9! z$)!=Skk1v_FZ-`31x8!3u!EdDVwBb4+Xiy?cgToEV&(UKsqQplP+44Oe{Fd@qButy zq*rd8 zi1)d5>;gI|7+CmX5rSiPB0-Tjjv2x7sVY?JS^DJbeO@=_A}$6kZqP=X0W{XEPzj98 z+_auC*j{wGz^hEuk*@M)ICFHu$A&gf4KT{BO>z)K3HHf+m$drP^0!h;*lRF)t$NZyj56*x4^`8hqKOY~OK6~g#MWbs^ z7P&1gnJjT>)^ko!|E0!O?Xzjd<_1cU;Kb$_e1RFZEz1*u{JdrJX7iyvkzVV3YkrV19siCykaeMF+@1f=>(# zxSoTVt*afHVzq&B55w`D@^OUQ3J_c30g*r9zR2YON3MeixLL?ZY-laTHh^E|xden0 zPe@$B__t=(;o)hwR;U}(XzS}Rvhgr}N#)RFs>dF!0XB_dv!e-&a%(!gS}ZWfZuEo2#7)&d9Y$9m0$hLY46m zT5{VtHK3aktRONjOztdRh$+jmkne6i<)cwz6dauwxip$b{BC6d6>YLqy8McjFIE+d z60<-ecnU`Jsx5OD99@RG;>U$p zh99-~%5`yxchJtNgz|0#O$eJ)jc)M%Z|IL+2+lqZr9d(1qHivRCyx-0WG&J26UDzq zpu`k@iy#n5CPGx@3NlgcJh2cf#1L1IdZQs8k}ETG!MX>%F!uyn7&~ph0J7o^L(na( z(yke%oU~!G2@A@Jl~r$IMjBv}bN;XkQ+SrCC(n&Q!^gwZjhxieP z2V5fnyG_hC)J+g?By_%kecmH8tF}_MyB9AwZi*IN8K}npC_!i`&ap=k9#WWn`EZGg O_Ds+10Q=bd*7-NzZIoRA literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/sun_forward.vs b/res/gamedata/shaders/r5/sun_forward.vs index 59b4a804086..1624c6c089d 100644 --- a/res/gamedata/shaders/r5/sun_forward.vs +++ b/res/gamedata/shaders/r5/sun_forward.vs @@ -24,8 +24,8 @@ VS_OUTPUT main(VS_INPUT v) { // Vertices are already in world space (billboard computed on CPU) o.hpos = mul(m_WVP, float4(v.position.xyz, 1.0)); - // Place sun at far plane (reverse-Z: far = 0) - o.hpos.z = o.hpos.w * 0.0001; + // Place sun at far plane (standard-Z: clear=1, LessOrEqual — far = z/w = 1) + o.hpos.z = o.hpos.w; // Pass through color and UV o.color = v.color; diff --git a/res/gamedata/shaders/r5/taa.ps b/res/gamedata/shaders/r5/taa.ps new file mode 100644 index 0000000000000000000000000000000000000000..c933ab2eff359e68be46c34930ed571efdb6fe52 GIT binary patch literal 7528 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 literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/tonemap.ps b/res/gamedata/shaders/r5/tonemap.ps index 36570c194958eef7faad58912c281a2d31b9e4ba..c9d80ee1c00baa3398ae3e2f75cbbcb2757bfd36 100644 GIT binary patch literal 1412 zcmaJ>+iuf95PkPojKs^jO??aLl^~V0sv@;biOoeqDB9Yac!ljXyPJ}LD)A9~Az#Am zu9MP8;0McI&zv(e=ZwP;v}jmO+n`nO`{ysnE2%jaGi(y(rk+l$74g4rh_)Wo)IFk|h_QXJsMb6eL@4 zrI{o;^3R^WznWKq>em>?R;^H&^DH*T*p$|7#bm0fW}xbJCUu2e^cku$sGibtnQOR9 zXXD$ucNzTf0541N5)hu7R*@imKf9Zx<7_;+J$njCK(kdY1kUXgvdfR7$z*yFoq3Ol zY%%&Uax!F29MYZ2#eL7QI4OM35#u@R4c!@Hm|BboyDIg-ssy~ztic-BfO-KLRi?)j zCmkBueVd=7RJ!Aue=y>Xw^>7z=lc0iSo(GfL)gvT+dXkOlA(+Z5ZeimIEc_@<|W*X zM_Av-V;j1Ul^8)DJ>$AJlF=^;r4vGz2OHbKJ#7l!EC3aGG8LVpN8tKm3SXDB(fm7e zqX*Nb#ECqx;;J*=t>B6|4H9U#Lw6<5CC$ko2~Liallb@qut9TD2u<2ftb{~Cba)iU z(crKL_#6$6`$zG>$II5<8Kdx(hD`2mi-Gm&NLw%(*~RwuX?v_MMTClRKY ztd0>?=&Wo0By|flmr_XF=aS*dQB;S{nO>g(EoiHOR;XFAcEJih#bKNr_aKQr+XmbZ zXXri?`_?m(qy88dAajD_MDEVBuFsw2vF-P|jd|8t!8MkJlliCUZmgS)4pwU)4p70t i#{GtHe+V5=2+3d&;3nI0I!`-s$yO;_j&Iwr>i+>oNWpyo literal 1346 zcmZWp?{C^L5dEIN;wcC*HAgGAea0ywh6cvU)JHQl5 z*6RXlVHk&R;{-Tc7lw&7Fb%A%I4zM)bIfFu{Vb?(G+b4b=Rm58Kld%| z5=OO=mvmc6T`LMJB}=$#zGf(-A>H21Va0gKGH8@nSbDekK+2LpCQ)u?m=xf%6$nam zng__Rq=~3%qg_P+)9J%Jqk>`#mkRvk;p-^6S=>LyqZAOZ-yauG(>VEV699%p{evnn zbZe|uKiWo+^tw&p8|eY=t`@bXIjk_NkegjHjrAXyphSTt);Q&P=`Lq?3}#W}3i@c; znrTCoZ#u!?xc+&?B{5%+cJS1=u3y{6+hIEN1b1l1}33;7I9OYceTSj{4KP zU$=3bj5`DGW#HMJk8Bg~Uo{c@DN(=WhGl=D+lj8NW!9^sVnF|Fu+z|UIcy5Q`Q8So z(Y2_N2?U1F4X>lYYj}xpC+zgj;LNeF&f(m#uRoo!se_}d1FR@IE3l3DAcp$>7B0Xi z__u`*unYod!w$*?B;g3o0V|x zhRD}twcu{7THq3d`)*({*Cg8md-C8gI<;VEcZraZfXA2NSZjMU_IxMr$hRf;!QrDM zxzB{@`JEQnpWT(67p$`Bt$by~BhcjHrthf04>j{>3y(CmZHIl4FBieSbgg?LWos6R&sI}Av zNylBiCkrF0N-O_7WycM*LJ>AM<-4+A$3oR|Xb4BO?mDm8A05Qz4CV7Bv(5~)!+KL* z->yGz*#m?BTB*4^$G+EgJmM0YFXdWW;e=MpXUPH{uh!Z$Ub6l0_5I!5r&VzvIG@;q z$1FID0;Pp=WO^<6Xp6xg;cFx#xV&B+I4+j#amm<3)p4Pc85lzKgG(dfmYW^9;$;zr z>>yoCDOx1qL3WVm%@v9?)btSkW5G8Q#)>;WGT{olat_*WuRZRg{rEDb^ZHE~>naX& zoZU%e&E}S+csoVHe51ulFWg7oMAsLbv8z*C0!e0e)eIX0Je}zRRV_uI#6mNUQzS)JoFT{J;vy^3No&Xr LMosyUEF4Y1?jZ{vLB&1qg>)dH>?(vV@_gK2+AKnc+IzI0nyKjzATFn3e diff --git a/res/gamedata/shaders/r5/trail.vs b/res/gamedata/shaders/r5/trail.vs index 1c9e157091f..18e0da47771 100644 --- a/res/gamedata/shaders/r5/trail.vs +++ b/res/gamedata/shaders/r5/trail.vs @@ -68,6 +68,7 @@ struct VS_OUTPUT float4 hpos : SV_Position; float2 texcoord : TEXCOORD0; float4 color : TEXCOORD1; + nointerpolation uint materialID : TEXCOORD2; }; // ═══════════════════════════════════════════════════════ @@ -299,7 +300,9 @@ VS_OUTPUT main(uint vertexID : SV_VertexID, uint instanceID : SV_InstanceID) // --- Output --- output.hpos = mul(m_VP, float4(finalPos, 1.0)); output.texcoord = uv; - output.color = float4(1.0, 1.0, 1.0, smoothAge); + // smoothAge fades birth→death; tailFade fades along trail length (classic ribbon) + output.color = float4(1.0, 1.0, 1.0, smoothAge * tailFade); + output.materialID = g_MaterialID; return output; } diff --git a/res/gamedata/shaders/r5/volumetric/apply_lighting.cs b/res/gamedata/shaders/r5/volumetric/apply_lighting.cs new file mode 100644 index 00000000000..78228142ef7 --- /dev/null +++ b/res/gamedata/shaders/r5/volumetric/apply_lighting.cs @@ -0,0 +1,25 @@ +// apply_lighting.cs — sun lighting of froxel density (unshadowed MVP) +#include "volumetric/shared_froxel.h" + +RWTexture3D u_FroxelVolume : register(u0); + +[numthreads(8, 8, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + if (any(dtID >= cb_Dims)) + return; + + float4 v = u_FroxelVolume[dtID]; + float density = v.a; + if (density <= 1e-6) + { + u_FroxelVolume[dtID] = float4(0, 0, 0, 0); + return; + } + + float3 albedo = v.rgb / density; + // Simple isotropic phase; sun intensity scales inscattering + float3 inscattered = albedo * density * cb_SunColor * cb_SunIntensity; + // Store inscattered RGB + extinction (density) in A for ray march + u_FroxelVolume[dtID] = float4(inscattered, density); +} diff --git a/res/gamedata/shaders/r5/volumetric/clear_volume.cs b/res/gamedata/shaders/r5/volumetric/clear_volume.cs new file mode 100644 index 00000000000..3c6313c9b43 --- /dev/null +++ b/res/gamedata/shaders/r5/volumetric/clear_volume.cs @@ -0,0 +1,12 @@ +// clear_volume.cs — zero froxel volume +#include "volumetric/shared_froxel.h" + +RWTexture3D u_FroxelVolume : register(u0); + +[numthreads(8, 8, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + if (any(dtID >= cb_Dims)) + return; + u_FroxelVolume[dtID] = float4(0, 0, 0, 0); +} diff --git a/res/gamedata/shaders/r5/volumetric/particle_inject.cs b/res/gamedata/shaders/r5/volumetric/particle_inject.cs new file mode 100644 index 00000000000..3a8680e1874 --- /dev/null +++ b/res/gamedata/shaders/r5/volumetric/particle_inject.cs @@ -0,0 +1,45 @@ +// particle_inject.cs — splat particle batch centers into froxel density (smoke MVP) +#include "volumetric/shared_froxel.h" + +RWTexture3D u_FroxelVolume : register(u0); + +struct ParticleInjectPoint +{ + float3 position; + float radius; + float3 albedo; + float density; +}; + +StructuredBuffer g_Particles : register(t0); + +cbuffer ParticleInjectParams : register(b6) +{ + uint cb_ParticleCount; + uint3 cb_PadP; +}; + +[numthreads(8, 8, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + if (any(dtID >= cb_Dims) || cb_ParticleCount == 0) + return; + + float3 worldPos = FroxelToWorld(dtID); + float4 accum = u_FroxelVolume[dtID]; + + uint count = min(cb_ParticleCount, 256u); + for (uint i = 0; i < count; ++i) + { + ParticleInjectPoint p = g_Particles[i]; + float r = max(p.radius, 0.25); + float d = length(worldPos - p.position); + float w = saturate(1.0 - d / r); + w = w * w; + float dens = p.density * w; + accum.rgb += p.albedo * dens; + accum.a += dens; + } + + u_FroxelVolume[dtID] = accum; +} diff --git a/res/gamedata/shaders/r5/volumetric/ray_march.ps b/res/gamedata/shaders/r5/volumetric/ray_march.ps new file mode 100644 index 0000000000000000000000000000000000000000..6acf60aecd5b994b0d5bbd2f0bd744ef451734fc GIT binary patch literal 2403 zcmai0+in{-5PjBu#UOoI+44%!1*ogIMPW6yfi^ZG)hJvP0<}v?gt^NF$<@Vyfj;#E zihg0fq=%$d7Y9M}5JcRWGc#w-%%t5W&9>8=>2#TthCcuGFNwnPg=Uu1T&oS2v{JIl zIk6(6ROO{I0^ih%Yq}YIB$M)jN8gAdl~u;+O|Z6Fq;0cgnrG7hlPupv(S&borFnNm z_+zJI=XI|b7V7A+I!*sR=DU|toKL7m>tps0Rn3AQO9w`p!D;R(a zR^X9IRV=&}v5hdz$mc4gt5ve0GwLQCx}dj7r{(^_Kk>c|@8GZ|ER~|HUHdU` zT9kQ*BO9OQ4XpA?#_4Q&TdW@LTjX4wRb2%Pf@QMVK2w`=wuyHSYe4dg!5*{cDZ87` z4Y#!$ctDbXzh*ht3_pRbITTrt}&+Qs{U+%sDE9&>w!c-;XS-|ZwF*N^ailxc-!Mp!CXZCBLo*9`O! zHm!2JVKjGqTg?iS3mc~H$|-vP%^{4%Gl#=IBgikzT+`#%wr&DpcFLlo?hmO{`6r@b z*~LCD;p3B_cOk_gou7+l)c6!N>YQK^g2bXp=O}m3MAdaVh}jTxiYX~#8UK{x+*G;_);oZ4w-|R3^yE@cCrO_6|CJ?l0Im8M zK0z;!6R>y4zB4!$NdtS%ol7u0R&#$kpi%c<0k%>CM_*TF>8Qpqx9b7M=*}cdpQ6TT z23X-($|rA!+3h5~0mc*weCz7n-6+jXCEYIa5>6cVXRjDI4+HB;LY6;sm2<1NXt`_c zmX_#kDw#1NJseKge{34e8!m1(Bvr^Q)G@agGc5beucjr#P%pXW9ybp4W~zdP6U)?z kMdLPTY_27 world + float3 cb_CameraPos; + float cb_Pad1; + + // Fog / lighting (filled by WorldFog + sun) + float cb_FogDensity; + float cb_FogHeightFalloff; + float cb_FogBaseHeight; + float cb_Pad2; + float3 cb_FogAlbedo; + float cb_Pad3; + float3 cb_SunDir; // world-space direction toward sun + float cb_Pad4; + float3 cb_SunColor; + float cb_SunIntensity; + // Classic distance fog (combine_1): fog = saturate(d * w + x) + float4 cb_ClassicFogParams; // x=-n/(f-n), yzw=1/(f-n) +}; + +float FroxelSliceToViewZ(uint slice, uint slices) +{ + float t = (float)(slice + 0.5) / (float)slices; + return cb_Near * exp2(t / max(cb_InvLogFarNear, 1e-5)); +} + +float ViewZToFroxelZ(float viewZ, uint slices) +{ + float z = max(viewZ, cb_Near); + float t = log2(z / cb_Near) * cb_InvLogFarNear; + return saturate(t) * (float)(slices - 1); +} + +float3 FroxelToWorld(uint3 coord) +{ + float2 uv = (float2(coord.xy) + 0.5) / float2(cb_Dims.xy); + float viewZ = FroxelSliceToViewZ(coord.z, cb_Dims.z); + + // Reconstruct world pos: Vulkan/D3D clip.z in [0,1] (same as restir_motion_vectors) + float2 ndc = float2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0); + float depth01 = (viewZ - cb_Near) / max(cb_Far - cb_Near, 1e-3); + float4 clipPos = float4(ndc, saturate(depth01), 1.0); + float4 worldH = mul(cb_InvVP, clipPos); + return worldH.xyz / max(worldH.w, 1e-5); +} + +#endif diff --git a/res/gamedata/shaders/r5/volumetric/sunshafts.ps b/res/gamedata/shaders/r5/volumetric/sunshafts.ps new file mode 100644 index 0000000000000000000000000000000000000000..05d8fcb88e4418337ebc6f3c8fbd84859936f83a GIT binary patch literal 2260 zcmb7FTW{Mo6n@vQIA~v_#Fk~}(j<;MWOj!RNE;*0l3g$ejC5qPr9_RQ;!7J~k9{uo z3;RoUC|b6h1{lT%U&!xVzweNp4h#h=3Le2wj-e>CV#X3(fXkw&Ov48$$`rNYQAlTg zUU=~B?|(qCC8SJ6GuVgW}#)M}GlXu~97;OI1Q8;;fF}k`7f4aK9ySN@XFL@S; zGDdh2$uyOjKYP*Gqu>hT4t>RP9u_Rk1<$4i<3#smlAwY~nN6w`hd;s@D4g;_qjK*L zJm-@Gv=SkiK7^`{FlUOTMISmHSPXckF)O%UwqQBf?xYg&SQa~=9)X-ft1{9so`gTYAOCW* zn{F;CF)lMQo7{$EU;XQEE9!y9MI@z)33Bu1*WuOG^(g398B%p`nMm3S3roux$WrGe z@u+7AREnk^VcZscI@9DxrEvBB@;7h=|A-YvDn8{I6At-$j(Tdzj1%7qvlvwY)Jq8r z3N#(o2$mhshYJw&Ujfm3Pm(bHauOJnxovRKxfdll&@)~D%VJ=0OztQvxlR@{^qn<} zDdU-2cZ`BkO`dJh4O3IpL9>exSL_UGp8Sas5MJT~3b#n}Fp~)vlw8Yt^6QSaAn^P} zquGZrU(9h*;yNz1s zV%-`^4otFjhE%7e>9yHDr3IfqZ`hMOU5ljRd6d>~MwSmYk{{L_=(Y4_w5_Uuk9H8q;F z>DL}8RLgdzXXrY9)$ImH#}#YA(aFK9?vY39X^2Q-l2918O264tXu}5&zHc^Vu+w={ zm|5%k>1xdH$e}HAkA{*0teI%QqEZp)`;s3x&VQ#9f!S2g+_V&K8s4r8J9NkM7IaBQ zG&{>xO}laXhmbP=bHiOCRV75==d+Uew z3{c<$t1kCM$rRR?uwryALCmQ)-@bgkjF%Zp>6xVse`V{eNi4G)3m!lwRZ8#H3f=al zd65k~oUesc;USqYx-4wG0kjEZt^K7fbN8G^&8WCxvv0=ldFV;-47Nw3ZOS)H?9qs9 xUF}?e)q6_(?_5N4Fti5n!_)0EBdpqQ^X{vnfUP}}f|Rvq-Zz1k6M7Gwe*vMNvzY(@ literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/volumetric/sunshafts_combine.ps b/res/gamedata/shaders/r5/volumetric/sunshafts_combine.ps new file mode 100644 index 0000000000000000000000000000000000000000..92fbfd2765fa85d59324c70703fbe25ce901729e GIT binary patch literal 1258 zcmcIj%Wl&^6y48PT(z5oI8K@Zi$>XyvOuCV!U3_fCib;G(s;(2N1GH?;v@J%zJxor zQ)1B-qTcv*=e*|3YzEdVTk*o#G}HA?D2y8mzkd7#-&kHZ62Vmn6_>@-paq+8=g?q2 zm7=U1?0iw6fqUU9;9zSmB{)=$&1SHZ-T+rQ@H_|M!08sN@KW|zNOW)`jM2un|FjXM zP+USUCcV?r*XWGMz&dQM0&Wm=`rRuK7HV#?3W>}C$@3XKl3-fygjNw)U63x!qp(~B z{6<^hFpt@Jjs+Qqh8g4CJO z?sCk&GoY)Gn!DFeHX3etZI?jm_Xk*=G*F!Z+k<75#O zW?Gw^Mz-(2t#5BXU(c7VzTRrhg&G~1feFWil7za;J@E42!LNI-4g5E*kc~J+?6Z92mThvB3S@pZz QZM;D`GvS!id7+xJ-+&C6%>V!Z literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/volumetric/temporal_reproject.cs b/res/gamedata/shaders/r5/volumetric/temporal_reproject.cs new file mode 100644 index 00000000000..5c64f85340b --- /dev/null +++ b/res/gamedata/shaders/r5/volumetric/temporal_reproject.cs @@ -0,0 +1,39 @@ +// temporal_reproject.cs — temporal accumulate froxel density/lighting +#include "volumetric/shared_froxel.h" + +Texture3D t_PrevFroxel : register(t0); +RWTexture3D u_FroxelVolume : register(u0); +SamplerState smp_linear : register(s0); + +cbuffer TemporalParams : register(b6) +{ + float4x4 cb_PrevVP; // previous frame view-projection + float cb_TemporalAlpha; + float3 cb_PadT; +}; + +[numthreads(8, 8, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + if (any(dtID >= cb_Dims)) + return; + + float4 cur = u_FroxelVolume[dtID]; + float3 worldPos = FroxelToWorld(dtID); + + float4 clip = mul(cb_PrevVP, float4(worldPos, 1.0)); + float3 ndc = clip.xyz / max(clip.w, 1e-5); + float2 uv = float2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5); + + float3 viewVec = worldPos - cb_CameraPos; + float viewZ = max(length(viewVec), cb_Near); + float zNorm = ViewZToFroxelZ(viewZ, cb_Dims.z) / max((float)(cb_Dims.z - 1), 1.0); + float3 prevUVW = float3(uv, zNorm); + + float4 hist = cur; + if (all(uv >= 0.0) && all(uv <= 1.0) && zNorm >= 0.0 && zNorm <= 1.0) + hist = t_PrevFroxel.SampleLevel(smp_linear, prevUVW, 0); + + float a = saturate(cb_TemporalAlpha); + u_FroxelVolume[dtID] = lerp(cur, hist, a); +} diff --git a/res/gamedata/shaders/r5/volumetric/world_fog.cs b/res/gamedata/shaders/r5/volumetric/world_fog.cs new file mode 100644 index 00000000000..ef577c4efbf --- /dev/null +++ b/res/gamedata/shaders/r5/volumetric/world_fog.cs @@ -0,0 +1,20 @@ +// world_fog.cs — height fog density injection into froxel volume +#include "volumetric/shared_froxel.h" + +RWTexture3D u_FroxelVolume : register(u0); + +[numthreads(8, 8, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + if (any(dtID >= cb_Dims)) + return; + + float3 worldPos = FroxelToWorld(dtID); + float height = worldPos.y - cb_FogBaseHeight; + float heightFactor = exp(-max(height, 0.0) * cb_FogHeightFalloff); + float density = cb_FogDensity * heightFactor; + + float4 prev = u_FroxelVolume[dtID]; + // rgb = albedo * density (accum), a = density + u_FroxelVolume[dtID] = float4(prev.rgb + cb_FogAlbedo * density, prev.a + density); +} diff --git a/res/gamedata/shaders/r5/water.ps b/res/gamedata/shaders/r5/water.ps new file mode 100644 index 0000000000000000000000000000000000000000..c646ff73c53f253fdf7c89da6c0a0b059209dcfa GIT binary patch literal 4684 zcmbVP+iv4V5`EWKR5Cz7ITUsE%z9?^;Ee1U0lvkO?QAv+qD8eNVknX!DNA+`AW!*# zAYYg-*;Cz3v1x1buzDb=?mE?Vuj+Q2wxW=km*(`>Z-3HU&cgr;bG=MAlWTJFWKm!z zEmrg`EfTTDYcjdT^E{JzETd-Q&0H?RSkmOuf9rRuzmr@4`@7SN@$_HR->=?H&iu*s z`P6?nou1w9m#@ywMu*mMl&17Y*AG_@r?(^jyYcCy@g|IeXfv1eb&#yrN$jn@w$^50 zJdb3a`-e93mB{40o#z>Y8hMdzf&$9=Ddn-udi=MojO6BWb2ONQ4(}t6Lf0?J&T}Em%Pcp_f&G|*9m%h z-y#^)3Hq-Hj%x&uy)+5q!am}yMKGX%HQ?WS`gnbDeQWCob0y1ix0LXmhR4iSW@!=$ zHXUvFlr@K4D5CMmlK3CB^NOI5yQPdRf|C!_Zc`A2X)8}fAaQC&Dn3;hQ(Fb*-F@TN z54Kyc@20Q(8YB_0%BfS4EoEV=Ga&Yui0pmaV*_HBe|*Wp_St~8Nfyl!Fg6o7jYXTv zU9rjJ$IVPq?#uYG1ENeW!yHy|ir#Va!)uyuo#t&t(;ISU;1{%wbjFACPify)>(I(#Ha_*9s(UA7r6op?T z{3P-`43x%5F6{+)K8&3**JxaxQxPPaBHa|Q635e}iIb5i1P2XHS^DK-{_n9@1=4?X z^n?p?9ISeaNGx;uhAvO1{`tk}CtXFFG#pZ=shINt986BD3u9I(t=)^GCSsjNauhBW z(2|+DH1<@w*Xa8ccR;NwM&Idp$1bTy*Xw|ePtjWxcmJM@WoOZK^o`@W+|gG<8eiR` zh>m?PZ@R<`n{`u{<;-R?_RQ)F(o8NQi4HJC?x-)v(1G(7v`Tgqo@E3A z*eDI-CHi6^W|1r*sBF1*iShNL;U#7{7~|=bEB5JBF0RE6WO(k$0P%M2tEDG*k70`E zQty%xwTF8Gu&a`yB=DZ-pNP+slane7ti1{i!XxT=okI>%HEU>!mmM=%5n&P#StVuD zL!qdp0l*wQs`7RJ4=}J18j8b0xWwFCy70w%7RtErY>$1q(31@6JSLV*rV+YE&Jg(T z%M+m;t;0M=2u(kqU6IHNl;AL+{O1iu1ezsDUf7YlujxaQPtwP1pvD2j@jkXJO$US# z1HxhkK}C~3{y8NF4lO0Si3S#_Qp;r|+YKKAtpZih;S~XobiMv@>2yXNK^A_otL!YI zg&xn?(j||1Qtaak9?~tpWTpa_(qxOiFyyymIJk6DuVf_F$p&2&S6)cWx~q5o1K)-n?`M~aF# zKxc}Rg7R<~Kem*_h*fxl4UL%K_E`g%xVF zR~bsLOUilRT7?FVVVf(yMpDg1_u*>O)k;^#mA=!NhR9N*H+MAsY4^1K5XB$n z7(=Bh6;gy}Tw`!Jwyt_0q5zjzo*`OXy^Z~N2=bow7-Zgk%mSPfd~xBS>0otN>~zrl zuZSp5w4Fzb3#H?^2nt-A09_|ZvBGsF=%)zH=+V%T8@@+Se73CklAOoNS0V?8?4sIs-2q=%og* XuXFoOQm)wDA(I$OV$vAgY8(Fr<%*>8 literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/water.vs b/res/gamedata/shaders/r5/water.vs new file mode 100644 index 00000000000..248d9d3a142 --- /dev/null +++ b/res/gamedata/shaders/r5/water.vs @@ -0,0 +1,107 @@ +// water.vs — MDI port of r3/water.vs (watermove + dual nmap TC + TBN + vertex lighting) +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" +#include "shared\waterconfig.h" +#include "shared\watermove.h" + +struct VS_INPUT +{ + float4 position : POSITION; + float4 normal : NORMAL; + float4 tangent : TANGENT; + float4 binormal : BINORMAL; + float2 texcoord : TEXCOORD0; + float2 texcoord1 : TEXCOORD1; + float4 color : COLOR0; + uint drawIndex : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 hpos : SV_Position; + float2 tbase : TEXCOORD0; + float2 tnorm0 : TEXCOORD1; + float2 tnorm1 : TEXCOORD2; + float3 M1 : TEXCOORD3; + float3 M2 : TEXCOORD4; + float3 M3 : TEXCOORD5; + float3 v2point : TEXCOORD6; + float4 c0 : COLOR0; + float fog : TEXCOORD7; + nointerpolation uint materialID : TEXCOORD8; + float4 tctexgen : TEXCOORD9; +}; + +struct InstanceData +{ + float4x4 world; + uint materialID; + uint flags; + float pad0, pad1; +}; + +StructuredBuffer g_InstanceData : register(t14); +StructuredBuffer g_CompactBatchIndices : register(t15); +StructuredBuffer g_CompactMaterialIDs : register(t16); + +float3 UnpackNormal(float4 packed) +{ + return packed.rgb * 2.0 - 1.0; +} + +VS_OUTPUT main(VS_INPUT input) +{ + VS_OUTPUT o; + + uint drawID = input.drawIndex; + uint batchIndex = g_CompactBatchIndices[drawID]; + InstanceData instanceData = g_InstanceData[batchIndex]; + float4x4 worldMatrix = instanceData.world; + uint materialID = g_CompactMaterialIDs[drawID]; + + // Match r3 unpack: normals/color arrive as UNORM [0,1] (same as unpack_D3DCOLOR path) + float3 N_unpacked = UnpackNormal(input.normal); + float3 T_unpacked = UnpackNormal(input.tangent); + float3 B_unpacked = UnpackNormal(input.binormal); + + // UnifiedVertex UVs are already unpacked (float); r3 used unpack_tc_base on int2 + float2 tbase = input.texcoord; + + float4 P = mul(worldMatrix, float4(input.position.xyz, 1.0)); + P = watermove(P); + + o.v2point = P.xyz - eye_position; + o.tbase = tbase; + o.tnorm0 = watermove_tc(tbase * W_DISTORT_BASE_TILE_0, P.xz, W_DISTORT_AMP_0); + o.tnorm1 = watermove_tc(tbase * W_DISTORT_BASE_TILE_1, P.xz, W_DISTORT_AMP_1); + + // r3: xform = m_W * float3x3(columns T,B,N) + float3 N = N_unpacked; + float3 T = T_unpacked; + float3 B = B_unpacked; + float3x3 world3 = (float3x3)worldMatrix; + float3x3 tbn = float3x3( + T.x, B.x, N.x, + T.y, B.y, N.y, + T.z, B.z, N.z); + float3x3 xform = mul(world3, tbn); + o.M1 = xform[0]; + o.M2 = xform[1]; + o.M3 = xform[2]; + + // Vertex lighting (r3/water.vs) + float hemi = input.normal.a; // packed hemi in alpha + float sunOcclusion = input.color.a; + float3 L_rgb = input.color.rgb; + float3 L_hemi = v_hemi(N) * hemi; + float3 L_sun = v_sun(N) * sunOcclusion; + float3 L_final = L_rgb + L_hemi + L_sun + L_ambient.rgb; + o.c0 = float4(L_final, 1.0); + + o.hpos = mul(m_VP, P); + o.fog = saturate(calc_fogging(P)); + o.materialID = materialID; + o.tctexgen = o.hpos; + return o; +} diff --git a/res/gamedata/shaders/r5/wet/rain_apply.ps b/res/gamedata/shaders/r5/wet/rain_apply.ps new file mode 100644 index 0000000000000000000000000000000000000000..2a03c80b01749f9758accb87d3c31fe9322e21aa GIT binary patch literal 4130 zcmbVP+iu%N5PjEI43ZbhRw!8)H)x=T!iv)%i6U5zZTLl4kt>-9MY7ygbSVn-Bl?B? zlAhTmDX!(Tar2;9)a;ovXD&Nzw`n7FI~P$h6ImAT{7li`fBr?&>E+!krFXHUP~=}_ zLSB;6Jdc(SS|&|8r7({&)p!}oMU+UIewv-nI*rE5C<)?qDCq^`Dz(_oHbTpMA>!D7 zc=6QlO6B}ZV=7izEc2;`J*ibT%XJKBk<-tV%Vnehy$Q%{_L-Aa9tCQ6G$JAUV#X5;q;;ol-oh3?Z8Qa+N2 ziuCSH{rTIS1ZC&Zbe$MMoQK!ZO6HE;woj|s&7^K+9__=c#$LS+TNcSL-M5IVqSBPOi)ctIvGr=|`rB4rY=3uj<{cOO zkd#3E;ZM)toKU~xGycIrpR%Irm}^mi|Ceo#Hf4D-T3M^lg7de#eNBKKjS)tY<|`3L zd+8Ny*$VT*q^9Q~DCkyvbUK~0rW0e&Xf#Q+j7CYwDM*MUzfUN;y5XC@Gu87>7f`=TbkIR8I;n z7VHFQt2EUQm{^(D9hhjl9rK?#Tt*D)BRgC%EJ?(38K!3Xev4Ez$LK|;k0%%HWiFIz z|N3Rrq6Af$S97=)hw0Gw35GMbH0YiA9ePFTw_JNwWflBr;18M{UdR8bYePFa&Q>j@?#o^m@hL@nGDu3t_^P?0*T7YqgB($3M~~%Ly8q`BK?rAVf=MRSBa+%7WJVVA~ZtQlwcn<(~7H20!}tt)b+bh9qo}?mt&)xw5(&R z)-mU%Ws1)-XxmVv9;7KpMme!D8z!9H^MMV&deGwsWfh||xbca~G>9sjIrillW&mK8wEl{d!ZGeuWucW~Py9h?EmG`p$FL0#I# zF9~vXDFTf)9+FoSb~!B#Zej0Qf)CIF8O5H1aYAPZ-PS)m9f)*k{p}7ejIv!1to(h8 z$@x3TW^X)y3(PuPT@(hmcL6Z{p9Sz~HH{Fy%g)lyi?3Q^*`cks&aQ zR4a#tY7eb_O;V;gI?&{XVvbw5Dsdij2IN5}bZ?|2D`o10x0lN}-CT){hnob=WEL7! z0hTzsnzdM2Ck6w-T0IBi{uqMecn-wq7y<~aWD(SfZ35)^)W&&_gqyzbcbLB%`N$Tj znFQPhjy7SX%P3E=(CVqqrTFSWfR?#WT2&=ERKiM($LH?<{T%P%2*HEnk|=WrUio#$ zwf>lBkCKY%JE=|D#-Bv?!zlznMfpbooCNkTVa*Il?)=?$U(q$n-~0RRu6k78Q7MC* z9C4NQ3G2&PPsNF$2JFJRHq!3y-GPQVe540g_V@KL^UB|&DtKdiI;}uG66r@!xBrF8 zARJXC-F9zER#n2`fg>Ak=wsCsZVkTRpkZ)kJunIU=8SR~8l8^U5Y{(Ma$40+{f$B{194u07V y1a_#<*+A^Ae#)!)Bjo38=yoB)txyPgSks2p@^)%>2pe=R@fMQUbq?=Tjeh`CBoG<^ literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/wet/rain_patch_normal.ps b/res/gamedata/shaders/r5/wet/rain_patch_normal.ps new file mode 100644 index 0000000000000000000000000000000000000000..d43fefdb70f6498d8444bb0bfb436c844229e655 GIT binary patch literal 3406 zcmbVO?QYvv4E^t?Alg5&qssC}(yUnq3`rLh*j%H|>Y&3QDE752M3w|yxwbPD*hB0I z_ar;yl`LJy`>_Rq6Upb2JmeuSyItCzkT%*nWT#-#@80I zcM4frBn#OGvURU^#U@w%TUrJqA>PCk?6(P-rg=Ocr`iIs%LC|p0}ko z)2)SOw+A#}Q%;A47@R`)`XaiIP{6 zD3Jkw`3=G9BHzpk8c%``qw%lzc3@eGm?Eh09Zfz5h((?nR{K<_-3&^d19JcV;pXGV zPq)6YEaZgSP5KqelIHUln$NvL4hYL(Snqk2x2KZ=rU zQ?yOO?!$bk3Ol7Ejr0ps(*rM9R!!*|Q(l!|=Ge&mfJU%l21xX*9I@-|_RomWWE$!5 zIX&jX_x)bmHXt3wRz{ZHDg_dxU{Lt~6efoOX>kFJ2y&zd=GNiPX=vTsxlQSN`(}8= zXYP1XU6T%xe*Ei*oem8_%EF4k0A%AvY!wN>>No}h5J~`75Q{upK-;lX9-#aP_D+R$ z$@C;Mv?9oYSTRL>E{m101k!>O=u#j|vwFLRTalFUTA4TcL@l|Eo07$=Y6jFAttkq_ z4@m(>vlvRNQf96LRp<@wgPGDn&)d7ltx~lN{+_@;BPK2Y7saLiQ3){Y5{0YlWwi{l z4iO`O=kO$pL%jn2lv>NgHGs%C+vcKh5J4VgxXiS3;82ctS8g_Q3)KXV zn{3KHtJ$+P(4#5EyO{;!JK_z6kC+*U1MMs7V)p|J?Gn~Fv(bSxwmEIhCn*ZNHj2Y? zU8iZW!g`irroWXt@6zS?nz`!J7TPXbWC~wXhEgTyK%ul*m)tB@=He8u<>Dn1dFYH>lVsTY G*7^qm5h5P| literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/wet/rain_write_normal.ps b/res/gamedata/shaders/r5/wet/rain_write_normal.ps new file mode 100644 index 0000000000000000000000000000000000000000..9999ee8c6b30419f7dd7b904ec426619060052c6 GIT binary patch literal 1313 zcmb7E!ET#C5WVviBlQ9;g|$;$t5yzC+@_UUVi~8(4Q=c-rUDl2F0q{|RgUes{X&09 zXBRNI>Lor{;CcJz&3m)1s6HZyi&s9>5nc#{fqPkuaZr{q=YpW zK(h?ENLit%N_ioz7b~_-3kK1bcoY-MifCSDOhi~`AoDU7D#JPz@D2n^QmL4*C2=N} zwO~6{2{xQU5{ITg^g&94`j{D2%MDwdU=dthhIM-iay-JRmM@Q{eyUez_Bc; zM72~fi{ek=>}Fwow*Wuuj8pXnN-k492XaPV<5}}I=^#T;Y`5fG=(daZx0lz~^C_8F zkEp-l$k9c+rH8aAE9KOD3n~((PCKG`37j7%_G-UAvwN1=eY@P6$!puYpzY{OZ`{ya zjWf?AGi+$ym7r*tR8%MlQ~^2{1c#^J#2W|D#jh75ucU&qk{h^V8Q-GebGg|^6?bG5-8DQ)7BAr9@00$11d!W^eRgODS- zUeWY{+1gcaw|{_Fb+TvrxISHf5kdw|zatWqCj_m$=W(1+;MSLR$X%p1iuFV@o+G9QF+t-Ur_t59UqR|dImjv z5S+|L%QD;7N#l{)praESjrpNH^+|j-Ui^p0%-Sk6*>7FQ@fx%yJQ literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/wet/ssfx_ripples.h b/res/gamedata/shaders/r5/wet/ssfx_ripples.h new file mode 100644 index 00000000000..41d200c8e83 --- /dev/null +++ b/res/gamedata/shaders/r5/wet/ssfx_ripples.h @@ -0,0 +1,46 @@ +// wet/ssfx_ripples.h — SSFX ripples, Slang-safe (no early returns) +#ifndef WET_SSFX_RIPPLES_H +#define WET_SSFX_RIPPLES_H + +static const float3 SSFX_ripples_speed = float3(1.05f, 1.31f, 1.58f); +static const float4 SSFX_ripples_offset = float4(0.5f, 0.25f, 0.31f, 0.5f); +static const float SSFX_ripples_PI = 3.141592f; + +float hash22(float2 p) +{ + float3 p3 = frac(float3(p.xyx) * 0.1031); + p3 += dot(p3, p3.yzx + 33.33); + return frac((p3.x + p3.y) * p3.z); +} + +float2 ssfx_process_ripples(float4 ripples, float3 setup, float time) +{ + float2 ripples_N = ripples.yz * 2.0 - 1.0; + float RFrac = frac(ripples.w + time * setup.x); + float TimeFrac = RFrac - 1.0 + ripples.x; + float RFreq = clamp(TimeFrac * setup.z, 0.0, 4.0); + float FinalFactor = saturate(0.7 - RFrac) * ripples.x * sin(RFreq * SSFX_ripples_PI); + FinalFactor *= saturate(1.0 - RFreq * 0.25); + ripples_N *= FinalFactor * setup.y; + return ripples_N; +} + +// depth = distance to eye. Fade instead of early-return (Slang SPIR-V crash). +float2 ssfx_rain_ripples(Texture2D ripples_tex, SamplerState smp, float2 uvs, float3 setup, float depth, float time) +{ + float fade = saturate((15.0 - depth) * 0.0666); + + float4 Layer0 = ripples_tex.SampleLevel(smp, uvs, 0); + float4 Layer1 = ripples_tex.SampleLevel(smp, uvs * 0.61 + SSFX_ripples_offset.xy, 0); + float4 Layer2 = ripples_tex.SampleLevel(smp, uvs * 0.87 + SSFX_ripples_offset.zw, 0); + + float2 result = + ssfx_process_ripples(Layer0, float3(SSFX_ripples_speed.x * setup.x, setup.yz), time) + + ssfx_process_ripples(Layer1, float3(SSFX_ripples_speed.y * setup.x, setup.yz), time) + + ssfx_process_ripples(Layer2, float3(SSFX_ripples_speed.z * setup.x, setup.yz), time); + + result *= fade; + return clamp(result, float2(-1.0, -1.0), float2(1.0, 1.0)); +} + +#endif diff --git a/res/gamedata/shaders/r5/wet/ssfx_ssr.h b/res/gamedata/shaders/r5/wet/ssfx_ssr.h new file mode 100644 index 00000000000..2cbaf9c485b --- /dev/null +++ b/res/gamedata/shaders/r5/wet/ssfx_ssr.h @@ -0,0 +1,104 @@ +// wet/ssfx_ssr.h — conservative wet SSR (world march + SceneReflection) +#ifndef WET_SSFX_SSR_H +#define WET_SSFX_SSR_H + +#ifndef WET_SSR_STEPS +#define WET_SSR_STEPS 16 +#endif +#ifndef WET_SSR_MAXDIST +#define WET_SSR_MAXDIST 48.0 +#endif +#ifndef WET_SSR_THICK +#define WET_SSR_THICK 0.45 +#endif + +float2 WetWorldToUv(float3 worldPos, float4x4 vp, out float clipW) +{ + float4 clip = mul(vp, float4(worldPos, 1.0)); + clipW = clip.w; + float2 ndc = clip.xy / max(clip.w, 1e-4); + // Vulkan/Metal: Y flip like rest of r5 + return float2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5); +} + +float WetBorderAtten(float2 uv) +{ + float b = min(min(uv.x, uv.y), min(1.0 - uv.x, 1.0 - uv.y)); + return saturate(b / 0.08); +} + +// Returns rgb=reflection, a=confidence (0 = miss — do not darken/blacken) +float4 WetSSR( + Texture2D sceneColor, + Texture2D worldPosTex, + SamplerState smpColor, + SamplerState smpPos, + float2 uv0, + float3 worldPos, + float3 N, + float3 eyePos, + float4x4 vp) +{ + float3 V = normalize(worldPos - eyePos); + float3 R = reflect(V, normalize(N)); + // Skip downward rays into ground (common black/noise source) + if (R.y < -0.35) + return float4(0, 0, 0, 0); + + R = normalize(R); + float2 hitUV = uv0; + float conf = 0.0; + bool hit = false; + + float tPrev = 0.15; + [loop] + for (int i = 1; i <= WET_SSR_STEPS; ++i) + { + float u = float(i) / float(WET_SSR_STEPS); + float t = WET_SSR_MAXDIST * u * u; + float3 march = worldPos + R * t; + + float w; + float2 uv = WetWorldToUv(march, vp, w); + if (w <= 1e-3) + break; + if (uv.x < 0.0 || uv.y < 0.0 || uv.x > 1.0 || uv.y > 1.0) + break; + + float3 samplePos = worldPosTex.SampleLevel(smpPos, uv, 0).xyz; + float sampleLen = length(samplePos); + // Empty / sky + if (sampleLen < 0.05) + { + tPrev = t; + continue; + } + + float dist = length(samplePos - march); + // Behind surface along view? Prefer closer geometry hit + float toward = dot(normalize(samplePos - worldPos), R); + if (toward > 0.15 && dist < WET_SSR_THICK * (1.0 + t * 0.02)) + { + hitUV = uv; + conf = saturate(1.0 - dist / max(WET_SSR_THICK, 0.001)); + conf *= WetBorderAtten(uv); + // Avoid self-hit at start + conf *= saturate((t - 0.35) * 2.0); + hit = conf > 0.05; + if (hit) + break; + } + tPrev = t; + } + + if (!hit) + return float4(0, 0, 0, 0); + + float3 refl = sceneColor.SampleLevel(smpColor, hitUV, 0).rgb; + // Reject near-black "hits" (former fullscreen SSR failure mode) + float lum = dot(refl, float3(0.299, 0.587, 0.114)); + conf *= saturate(lum * 8.0); + return float4(refl, conf); +} + +#endif diff --git a/res/gamedata/shaders/r5/wet/ssfx_waterfall.h b/res/gamedata/shaders/r5/wet/ssfx_waterfall.h new file mode 100644 index 00000000000..b7b41f1700b --- /dev/null +++ b/res/gamedata/shaders/r5/wet/ssfx_waterfall.h @@ -0,0 +1,104 @@ +// wet/ssfx_waterfall.h — SSFX/CoP waterfall + sparse droplet streaks +#ifndef WET_SSFX_WATERFALL_H +#define WET_SSFX_WATERFALL_H + +#include "wet/ssfx_ripples.h" // hash22 + +static const float4 ssfx_wetsurfaces_1 = float4(0.90f, 1.50f, 0.20f, 1.00f); +static const float4 ssfx_wetsurfaces_2 = float4(0.75f, 1.50f, 0.20f, 0.55f); + +float3 ssfx_RainPos(float3 worldPos, float3 eyePos) +{ + return worldPos - eyePos; +} + +float3 ssfx_GetWaterFall(Texture2D s_texture, SamplerState smp, float2 tc, float rainInt, float time) +{ + float col_num = 50.0; + float col_scale = 1.0 / col_num; + float2 tc_ori = tc * ssfx_wetsurfaces_2.x; + float2 col_tc = float2(frac(tc_ori.x * col_num), tc_ori.y); + float col_id = ceil(tc_ori.x * col_num); + + float col_offset = hash22(float2(col_id, 1.0)); + col_offset = (col_offset < ssfx_wetsurfaces_2.z) ? (col_offset * 3.0) : col_offset; + col_offset += time * col_offset * max(rainInt, 0.001) * ssfx_wetsurfaces_2.y; + + float3 water = s_texture.SampleLevel( + smp, + col_tc * float2(col_scale, 1.0) + float2(col_scale * col_id, col_offset), + 0).xyz; + + water = water.xzy * 2.0 - 1.0; + water *= ssfx_wetsurfaces_2.w; + return water; +} + +float3 ssfx_GetWaterNMap(Texture2D s_texture, SamplerState smp, float2 tc) +{ + float3 water = s_texture.SampleLevel(smp, tc, 0).xyz; + water = (water.xzy - 0.5) * 2.0; + water *= 0.3; + water.y = 0.0; + return water; +} + +// Raw water_normal grain — use NM xy variance (luma of DXT normals is often flat) +float ssfx_FallFlowRaw(Texture2D s_texture, SamplerState smp, float2 tc) +{ + float3 s = s_texture.SampleLevel(smp, tc, 0).xyz; + float3 n = s.xzy * 2.0 - 1.0; + // Horizontal/vertical lobes of the normal map = visible micro-flow grain + float flow = abs(n.x) * 1.6 + abs(n.z) * 1.2; + flow += abs(s.x - 0.5) * 0.8; + return saturate(flow); +} + +float2 ssfx_WallWeightsSoft(float3 worldN) +{ + return abs(worldN.xz); +} + +float2 ssfx_WallWeightsXZ(float3 worldN) +{ + float2 w = saturate(abs(worldN.xz) - 0.15); + w *= float2(w.x > w.y, w.x < w.y); + return w; +} + +// Sparse tear-shaped drips (not noise, not infinite bars). No lateral wobble. +float ssfx_DropletStreak(float2 tc, float rainInt, float time) +{ + float col_num = 40.0; + float2 t = tc * 0.72; + float col_id = floor(t.x * col_num); + float x = frac(t.x * col_num) - 0.5; + + float seed = hash22(float2(col_id, 7.0)); + float active = step(0.22, seed); // denser — fewer empty columns + float speed = lerp(0.45, 1.35, seed) * max(rainInt, 0.2); + float y = t.y + time * speed + seed * 3.0; + + float body = exp(-x * x * 70.0); // slightly wider + + float cell = frac(y * 0.62 + seed * 1.7); + float head = saturate(1.0 - abs(cell - 0.22) * 6.5); + head = head * head; + float tail = saturate(1.0 - abs(cell - 0.55) * 3.2) * 0.4; + float tear = saturate(head + tail); + + return body * tear * active; +} + +// Streak from waterfall NM peaks only (suppresses noisy midtones) +float ssfx_FallPeak(float3 fallNM) +{ + float ax = abs(fallNM.x); + float az = abs(fallNM.z); + // Keep only strong lobes → droplet-like, not grain + float peak = saturate(ax * 5.0 - 0.35); + peak = max(peak, saturate(az * 4.0 - 0.4)); + return peak * peak; +} + +#endif diff --git a/res/gamedata/textures/fx/puddles_perlin.dds b/res/gamedata/textures/fx/puddles_perlin.dds new file mode 100644 index 0000000000000000000000000000000000000000..07331f63efec2b7a51d7af2f3af031b9888a0f1a GIT binary patch literal 699192 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

{#Xb;k!Q8o$e)7(~eiLxv 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 literal 0 HcmV?d00001 diff --git a/res/gamedata/textures/fx/rain_drop.dds b/res/gamedata/textures/fx/rain_drop.dds new file mode 100644 index 0000000000000000000000000000000000000000..20276e969dd9f49a2a4142c67726ad3e5274a3b7 GIT binary patch literal 5588 zcmeHLZ%|ZK6u-O63Lg@fX52rtQwyvM2`##t1fsSy5SR-B+6Jbw6z*&|VipCY6_%|) z3z#7TtJ@9+QCR%K*}s$P2a!F^k-@%F;{6-}8nDYc8=R&SMWp1)q6FBA}g01*javS&w$ zgc-h^PkXPlz=7dTsJP;!IYTzoUV0i7{ozR5yyEYD*PK8{zG%YU&LOaO{fMu3PGZ*S z^RVbtHAHm8psGvA*V_Gd8+YfWz)0Pv&|5cwcH1nJ*=BIHZIs5^93X4ZqN1Kx{Dz&& z6X+##BYgSd5F9W~!)vBV$a>*C#%4RK|=VI}FLQyvBb^npFJT=T9uaqmLyr`&FxGWmG!duuwCb78iy~bRs@u@3$>r z6AW8)0z9G-;r{4(KbJ>|al1l00#mb2yh_4f&VlI zs@TsD_K1TQ%mSP&3&QJFA$Yqr7$>`f5HIk0uKROeh=8`~#6Gt{<{KGX2hZ7=i&xjaW0~)R#s_`}4P#r! literal 0 HcmV?d00001 diff --git a/res/gamedata/textures/fx/rain_splash.dds b/res/gamedata/textures/fx/rain_splash.dds new file mode 100644 index 0000000000000000000000000000000000000000..5f5effc274e1c62bd60b8e2d25ca75105306a1ec GIT binary patch literal 240 zcmZ>930A0KU|?Vu;9}qa(je>r#Ed}90>s1szpxNvka`6m2I+B$2r&jy7=S~-0Yi*o zYQz68|9}3UQyAl{ufO$bB*TBs|MCCl&pG{P;@7Qfxqtru`hUWIhN(4Wdt9z=)z|+& zA1KQi$*^ZnO&=5Y+O?asfA9ajf0MS%5g8j@AXo(jd*;?;-Br4)1mnj{sjdOB0q>GT AwEzGB literal 0 HcmV?d00001 diff --git a/res/gamedata/textures/fx/water_normal.dds b/res/gamedata/textures/fx/water_normal.dds new file mode 100644 index 0000000000000000000000000000000000000000..38cf76f1c239bcd2e240015b3e5e22c66924ba3a GIT binary patch literal 699192 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 z

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�Sms;l!SF6%~4&>w!kb=vgMmx@4!&>3>Jf|vNi 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

    tB-h33`RxtD^Bg&zJvDDoZ`Z@V@CNu|K={ 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

    gG*3(vqGMp#h!dp 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%0DK9BzWP7{@`M{2$&o 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 z z7s!uM*e#QbPi*+ajQO^^QP^Y##WaDKnY`bkqa zgMCo`9{QPIDflvCU)6)oxqP7(vkqy8u?L>3*^6Y1_g)Im4v}Z{VgA(`^Q+d3SEles z-zcEhS&xrajxG9Qy$OEsTbv=|BR6|%3;CsPZAS2NHqN8ZQ+MlO`U~darcZtJ1^INl zt+xE$JB+_gTiy*8=QPSo6pU2x@2Br|-jKhy`?rQ@J@>dDHi5gc8Yu8?Rjy3$u`aMH zlhHDAOuU#KP-*Wa^VJwTl}zWt$Cc=XZ7{FO#ai)M06vyJX=wRAN17DxS8py&w!lwg z7CGQqbFNJ6WQv|CV0W~;dG`hHqZcz;+kDsdK_q_*fRS9C;&OzQ^C6O21Kb@PO}JPYtrV9Jud2&i$dxhik4)X&vr22G8IAL3i!MLVmZ@ zpl&x({E;YL;DNkVPx{S8MGrc>d2S&8apQ{fI_~`CP&b)RIX}4QGat_7_R(vccOySr zzFTqTZJY0mN59!enwW&}9{OAQFQnld{8sXvrxyW#|L!;5+YjImHSD)N z_@)<6g@4_9|9A{Ms?s@G#BZxRPjY@h-UaS_h?nw5=JK25_u-G;m`1!;qU;R)2c=9$ zBgfFod~_NEak`UR7rr8k5IpJfXL=R;MI4L75b#z#8W;VT-~aITzxq({*IFwaiCpHw z7ddJD9Q&8>F=f&g{zBMU>eG)~%&cn#p6^||>PdgjlVMl-V;ka{^n(|R-LOl)oZd&? z@MoVNvyNb&;dwrU{~9;_KQpZg^~2YqzeHY22PfWBq3>wEUti#-Uo1<1Pez5A2mI%q z@mM#!Mt<`BIEO1d5=XD*qZ2$JJMGs6P&Sx`Ej^$#9i2D_Dv=;ju^C$(L z4K*L9=nsxplSdo4v!1!g-_^x%W`Vz`6DnKiLBhGLx43PIR6x~;nhLr0+}c==ERm~B zrbtVDd(MqOSBHb@Hu{upF8GD`6na7vt;ygcZ=!Ms=pgwac*;SK|k!Sz)|M9LHHzS?BBd8@N$p99jJl- zludd9SMhu|D{zQ>dBAn7qY3a;*6+0VJN6+f@nQITxZ}{DYaTkYfO-CSa`_4Ul)bvF z$E?a%*3^Y9zt8bI!AFuV)}_?%a+YPhy{+4G^h86M zTT~t3L4LVF#^b$FcO?-x`+I|og?_6i+E>DV_N}4li6rx?Ab;1!c~-lzEl>rT$UOe(RtXLI>!*>F^vo81C#TzHgoWbMRi(v6{Bvi{GqUl@6ghe1|`1)9*|lJrlk4 zhJLoFXXDJN4+vg|N%OHqylS4^9&49+MBDcicp`5v0zc4+x(zZO{xEb=m2D&QN`5Ed zvqO)+FF8)eWB%9X&MO)3ytT6M%Xh|;gMi;$pN)KbtZO8X%EtbzC1NMM!4Z5MaG!VW zUcmRonlnkEf02WLG2g*P+5?VxB>A7%+4_+4zbWhSPG9gko1Gg0@>+O;f5-1g-*yBJ z-?Wg!lz4IRg#~=Qc1NYicfY^0Zhhc2yj&*&4)SZUgUHwad?$RD{u^yC-)DUCd!oFO z`I2xW{5=daWB7&qho(la0H5KDe$H~fg5JPO!gJ)-=-D3l&DRe*f>&c)G?{;OlAE;y z;OJbv*9e$LZauFQ@O^jcKak(O(Yy`!5SN+)2lTp#OppHdvEMd)K3d*-k9m{}BlaH_ zaI}5Re4s}n{W_PN&t%*JdAiJ}#JO4YncB`Rt>)zK*>T~O#?JT6HopS$^Ep`$-ZDoT zy`kLoX#+hKmv^48=E2)L$-~n-VlN5zWcf|J;`9#lDSddW)>4x9XRaLz9L2fh6?u*J z&G+78K4o<@tSjJrcoepT9}BleXh+_N^6%0Q_Z5fK^$c9XzoFRM{M>2U%pX5}!1&x> zEBh-k^p*T4&-E{e&-0$+{eXN2^cehTGtP_vr?i9{_UO0V+Z+^a#y#QukmkGN!t<=q zAvTIHz-Q^&NhbB^cOGUt^4)&XM=!a9k%dW(i;I$qPOui3X??CDe#n3ZcwZIE;#1MQ{cNO;*0|({&@fTUY zan2Yx$?G=^>Cf?^t7!}VpLH^6T}xaj)Qffm-GMummb~(DCrf{o)u#q}g80+5qZI|8 zuq`}S^zW!}ChupCy~R4xFki0|eSbG${tfj_^UIjFKO8LlAhqBtEpaXMgN6S*g>Rq( zb={;Lw)c86-iAG|YrZQyn=#pDAug37}v*r8`>A#YW#Q(5~%aAwCcf%;Y zP~h)zFWa|;u8X+0E-Ut9=%4y9oa+X!%WQaxT~0dLE&SvL+lRy#;hX9CNI?&8v`fFl zm*o5|?W{Ww;e&E9iq0*5FBeBz@ENKm`Yhm_z=e?_Z-3K{R*+-vd3x`!0`S7I>c10y z9Y+T1^O*Q3`MtmqaiI+@e&Bd&^IZ;eW3xXHO5I@eKj?|&yMzru=0iS9VJQ01Nm@&> z=!X+C22Svp1<#5+_r*GGo4gnNIz=9Aav0O^y3;)HXrmVn`v|PB{viQw{6>ceJ^DPn z-(UE*UULq8^*hTLOqrKa*Gu5Ddh_1s967Dv?gEEZHIE%_15b1`JN6nu^y5du#|3?DD6O|KX({+()u0{&>*#Fu z;ZE984R1bwY~xoCeD3G2$DAXObHMCxpkSCsx!7c13cPH^qlG;BS@L;UuMcicl*M}O zMfSP_-Wvbx@zw3_$oSfc;Kk;LYw<4SQCKT2bcwRa4g`9miQNHk{ zGrH2Yf$!MM3>NrxeCCt7KYgoyDD#Wf75a;Hug^gsp5Hw?#b04vPJNMB^gD`zSxP}yj{Z=2mxX&hqMc$RZ)!ne*%L4Y>W*)MiB6uM$ zwk`5Ka@dIG`y2IIU6D7{aW*JbB^cQ+S&viz|Nu#)CGfvKIP`@j1+rGr7 zB)=oZ-(np;dVLQ?blH4mvmV|&879(Fc2(?JDGFOf4>`9lsGtBGPg%wZpO|rLEd3iz zkNjQ>e@V`LAAGx6( z_i!%0<**~fd5ieD@O7^Bgn8hHW^(%;@wM#NzV;z)6?Y#z za4zu1qTa;z(7yk+Nlsuly{4@YA!qLD<9418Y zfTKNBbM#z!hx}f_myw;JmylET@wIC=9_N?tn+p4+qWjAp{GB@V}AIX;ey`(alr@L{ez1R z`oLm8N~1^Oa_A5LrDEJiKYEXSInV9veUtqzk9!CwcRbg2+i~Mu*W-73hbOhP6;Nm0 zM{abT?B%S;f8o)=Gm%s6b#?`xT2Ut*{!{YPx%T$b=EIr=-lFL)d`=uKU*Mdw$WyOiLkAT;8}o0Y{m4OH73&4D2SF); zZUO5j(xK{#^@Mq_jNYfNAbQJ&uVWH~1iwY21AT*1VM4w#aP>(HhX1KczE%j(@7pg8 zGc9}WyWeLY8Tpu+;}>xY`_BO!;HpMNZ2~X$Pb2>+@xR8U4SoGlw`f;AcYM*i79j6! zbN4vFuGIY}Bf-P6mi092^*!o`Fs~i0R~75^y@$=AwE4*neh2${#N{mBQ|D1E<2c?( z{K^XXqVOAe-9P&FGsSZ~`i8TRzr;U=Kgw%4@-gqTj+n^2gT}U(`O=1Ovg0pb@*BDK z?(@5Rwvv7!^kYB0pVkBJV?4os@`xK7I&E>zB1?V5mfaW`;XQeNtsPR2&ZAD`*?_;h z(O=G_Z5Wvo_%5e8d zUE21;VOO57y<%U0miW{T^l&#eZ%u&ioo}rFm!{3UpA;AJTRp-N@&xbw)R6Zle*Zkh zPW?!m18J`$FK-9X!z!myE)?>)~xo^hk&(5zv5b`ngcbx#Zio4e*^1R*~F8WWNnCMHpls|oX zH)b5t#Jd44)pt5E{r%GLrgn)swT|-^5Mb=eVFn4o1)W%rAC(``aVWW#9UaV=Dam)zvo#@Z~#C=6B)yw0uDQ3!(SkUO!>J zjs3@OEz9$%*>!CAFd6#9FJjprDQNWge(&`CGr27D(z33eF(2;z zsv$RO8@~*BW*uwzoAm2ui7&`|euCacj%J4k$f=^<{}FlvJJxU3W8^z$Y#^uj6S>!M zP*%%h>wen6ju&W&`$?U2;^6G_h&*M-G3)QNL0*+3@<)9&2fe_(idpmy_{RUVz!z%K zu+XpFaqgwXKE6u*1o+E^CULH6;GR8oX24smCz*ZWi|3bL4h61UCtu)q^@4@2;M*a9 zg8Wwop^;O}|Kib!Pg^d`hb;x)Y)-Rn=#L#Sn6E{B#E5kp_br@DTrw?x{?$T8;M^Hy z$v+bLr~Z)mQA)=ibT>E;=P^EU(6xZyYKora@1!ASJor4z0aKZ`Idi}-_AS!0csDJx zogsBV%HiH+YGWUV(Is{zmGfFN`YYvsK5cryYZSA;((t3sm__D8J&gkV2>wrYnkjg{ zx_)pEI4W0rx+AR-#UAsinjsYlXz`!F88ENuXye~|%tw8t4}FNMg?UN* z1AEKP9K& z=XoAYrS3)Qo@iq%dSbLZ8pq5p=VbHHuTlu>JsUo)ZF26E_uKn3_>{O=JraMf{Olr< zI4^Pd!-1k7Bb>i&GmeaBgXbD~)Zmx<7TaBsi(>t?A9d9FB$RQDyz&H&n;G<^(9Kr| zp0qc*L!10W=fYgX@5afLcmnkGyink1^_?kgxoJ{&f$P`mxQXE$Hb`A5r2Hn{hi#MZcb`94PdTvmcpwbLHb=>r%!$ zE-vN3Z`JG9uYvDMVLks6_~gD0=!d`93^PH6^$~u$$h|S^L;>@Dc3D`+hslBtJ|({} z!7sHcg}9jw;N#d-hQ)Ymoo)Pl8#y<^fcb2^IQ12N!@NVh2)`#=XlE68sP}LA^8Mo* z+($|~_|E<_=I8#!@~PN~%6j|OcNxF59@QR5|Hd`W4FcC==Z+EcTQ@^90RFRN9;V={ z?za~Df^#Q_g5TRD49DQ_#g?AS{Q7r$xq$Jv|GAC)kjFaaJK*cRj?Z#_y4OnS&q$07 z?b_AfX}ue2;sU;@&*k}%(RYaAm7l-zt^~lVGll2yMLFB+?=0k(L^c~bwAMq%hEADD z=DR`{KY8LP+JnoFwfz0GCHph6`|U4&{R8nYZXJwF4}KiyKiamzi;fPE_tfo^a1`SQ z{SkEJex+JuA-|i>Pz(K>cC@J1$-TqqRpc9oP56=c zF7V>d5P#C>t$e1?R#i>CCI7;%@|T<;dTpDkb|dJDKiOUAG0vwPd+7aobaW2fYmhGwzUw(7Q`Gf9{1?&lWsf)}^Qj&*HF#9jFC5~A%;%PxGcWin z^Yv5cQ@J=ip9SD$<7f7B@J7C;693oj5|0f8?%9hu?{gnR(G>g@=0r5$vt)3B9FZSL zTvPbo+^VB@k%x$T*`T}8bPhb>+f~(6$X^m0eu}?t92MH|5BlR6|AKjQ@43z2QI9#% zXI?h$i(U0t|0>$<<%!IXbsZb2M!lGE*C`Av z^iM}_MF&jsGWa3u)@?tSW9F|O16%9a8jl=T>Tyf-utiym;YOy88} z&KCP$;5)hX_Fps4(qZ(CkX25srJEzp|i`^d#SI>Emb#wT~ zf`@FZ7k1R>IpEc*UhRk-AwPRP=K&veW98hbc5nM}LEw)gV`gLjb?<_8w|76YmRs;i z-YGy&?l}py?E(Mj#qF=k`+6EVkPuNBB}ZSUUK6db_jex8Chs;qN7_YR{w{WtSKFq+6IQ318H1j~D#e z!6Bsoa;D!$o>J;wW{wyAoz#dQ0CA#`Z}Pk^N8mGcQ}#Wi9kZU1mhJupJft}q3b-5s zF|p{`bL=ALK8%wO?()0T%WW?F08K%{#ds`~8K-rT6Zp#wy9#_8 z{tH|9CRF3QjO)fv8e^I7)cpJq{%Rndt;KxQ3+#klx`kt_=r`HXz-5>0qMTQ<$$JG) zBF8xjIJMaSG5e{s{IkCe%?dHdQakD zODhVv%={JfFzax0?pUI?7x|@DH9LtIpY?B{xtNDBxcx%lX*IjQ^3XrG2Temag2rKZ zsAAy54_=%@VfA@i_-$4wgxG^hwm(BI;HTW`LLQ^x1U-R%VLwXdkviX76?p#^$IAlG zO=IN9I69djM;7;9lDW#fSl=cx-)~PY5}6m$W}UQ=H772Eb8Z0V?AYu5BvmuahO7iayAuvy|VfXMg>s z%)@{D=+FbM;$ER9`qw+iqGypW>K#EP_iDRwkH43n_v0^xzex|AdFYqZSH`szIof^F z$Sb}--!tw7;6E9BIk751zf0-@X0GY(2&<_lm`#U5y*2`8!Su-Kghw7r5Txocxx|)6tnv zZSbx)igm%`WwYrXz1dMzz~bN}xi(z>;0OGykLs3zxJuqTi7y05{SzVO9REcjrvlf^G~hS%{;1UlPu#aw zxMzVsxi8D6O(pLTJmwmstU|8T$Nh$k<98$I$UO4R$f9i<&rdA+QQ^3$TO!w)#A1F0 z@(-kSH1*)&mskz&2;X=!<#^zi@TuB`p7-|JidQy(<P7?Xr)C9=OK-2>3QL zi#pSd>EytRp;&w`xkkU-(-}t0yOcKDp$3lL+C?u0&iFfz;8W!0!6FXk5?^o0{H_;& z<|^>nRdw|2qnLK)Z=TXt)_%EH^k{Qhy%Ks1vkCMkPOwUXH*oa+Kc3z& zwvp^Q)GSk&7l}lV_D|vmY!LR2A&-WN#D@UwHYUkbko~m_EY=8KH9MMaUcDLR?WDLC^OIJL;NlWIqGj@+|iu0Q?|x@huzOa4ZYTX?eMcT=)rp07&zdW3n;NO zq#s3T%=OOX`zjkt@*Qragb99gsT;1yp>I6J4-v__K&5Fvanrvo{WO*)&r5eN2iA5k zn3)CMjj}&MZ_`d+&WjPR*VgP4U?-=V_`%>0&VB2e$NWTm6#o5j{?5?E?u!%bgYaG4 z*ag0H@q6D71CR3k%h9d}TtE0$M1AJLFzzb)|BSFy;4Wq+@5A4^&`(8{yw4;~o+{u^ z3_~YPX@t*^f7Vf%$yne|_PZ9}abJzX&cSYI9Iw!R#c<2S)q533ztwX@J|v!<_IFgi zFLYdd^agyb)`$IjRq$KS{+IV|d+00UrQ}~J#y!y<%{$V6*)SCIeus6Fz+G?dGB0ZT zZu6)?Ss4G@N2eZm=A7xec)fkO0)McM(%XylxA9=06A|z<>V7XoPIfaHp~ps)>Z7&gU@oi9flQ$ zybWj22Ysl66!0Kl(d{(reD1$UJTEziP236oqUpRM@CqEHWj6$$_Sia;(rsDjfzW<> zVYO+u(g^cYJ;3=m=tg{Ix$nY#g%$^MJnYvH2dam?iuXAdxinIH%%>jyk0yS9-sJGK z;m_{n8%A2r39gI>+I9-Hk~-8CWk(A{J^;r^ZSEpZ8*4jr&*8y=u4JB!x04QVJ?Ktc z=*9k{(eN0zUBUv==v(43kn?mgEX56n@wwaVp$}Bia*r4%?2d(p~MCLvdzhqUo<-`rA8F-)M}<69B&lsb!Ng%vD|97v4%; zd?w@OS{GyZ$xhZLO1}I0mqv9H{Xfp1*Lb!+9Y{VR?txO09|6DO+@!8}o;i%DZ*m^n zZF=aD!6WATjnHl~amEhsGM|ul z^X*qm`7W{iZeGD3Tn93i_KBn0=KVVPRnU9P&zZOhJjDGc6mZys+7IsmZ=uy0ouB8REA-jHUw!FZL5JMkt&G%9c3VR!>wc~*{Z6X8 z&)|-N+8QK)bN#o!Am_s1GvB)8`;D-)e<<+$>+h|=m)P;Tb|~%DF6KJ4Q~0}`J5&8b z8U*+?y>YqMnZh+7fsI2uR(A{eL+(ay3aS-DKLKB0hb^4E0oN51NzSFfKY%|7YhFbY zS0nnb>iZ-5v6bI=n|2DX`@@mB9u@q({XNY+XutMqm=}J|)LS8N*v5eK8+>=_xBiQR zT>LzK^|8nIsV{#DAB6UM<$Dupul05tyz%qLF?>uM`pVEXc}9()uQYlDJq5pVAL5R* z_fCm*E8lV57WQ3KShyIv4*a+lC*Bo$Tymh!VSMLrJj2fBJN(PKhCOVEzI7_3g)np( zc}I?Q89A4A4f5UYN*t=gIhg&!hBd)!Nl{~kCJ?c9_UgH=le0(sP(GbXgHM1>bXC}7`JAi(?HTy{5YCg?$ zX{S(j;y}hNb=)eU#R{*L*A@g`ch_1|j8`&qdc}lqNwbotEQWo~7**@Q zxe=DJn~`(JVm}}IZAhA$2>$uF^nAV{yf$#aJ9jp)9NGzz?<&4qe|Yn*51+3y{f5$S zPMy;q^>E}X>OJe~GwKx&S5(fk757g(zo$5P>vYz`eprdP9}ql0d#QaU_#VcY8KAIt zV_7d@|9|HWd`G-r;{kHuk+){<2WjZmI=RCp6W*8+Ru3>EW-Dz@sD=ETzTog#T7M1I(}f7+vdVKCVqNSP1FlY~54`Q4-gUHoL#HCcBC__-Jd^c$H(izfFm zuuhSA7jX`@{wK=MM6(2&fi19D_$hS#L8>iyrQiCyRCS zY+{T(A^3JRbg})jNaSI?OoCzgu8V&};7whNA`keD#cD_KJaV62gv?v&tvd(lZS1^p zwk9v2XY2N>Qd&AEe$`@>A72gNPwb|sIxBb_eY)B3 zsF&9R#>qqu#|rpT3H zJUcWg-PzNpX-cjwWS*6Ez{E*2{~NoqPEK?0Ld%-gORQ`P|Ni)Ra98Y7hk$umr*0qS zs`C2*2DZccr8D}*3UC)LMzR0AJWmRpfB9}7xOL%o=Q|_n*<6S86ZyT+I*fqZE?tgO zCiqGmAn&ANui`X(CgIs8eq?6_>Wa6)Im(2F zU+>-{u4kQnVmILnyw)Mtf4bI4t%$L z+<=~u-H$G(C3V0G`$2aZC-^;bw8+b6?&WqwS$;bu_!XDfhX`r+X203;c>ncldE|^| z!uK71ztWqWg|zfp{Xro8yq-*=Pq0&hCikp(*jvXkZiU!=FaYj?<`ydXv%)XC!H0dG3(4fm%md83uRWUn zuKgoWsq@!1oXL`+|0VJZnDi&}fl2$>*`Iz+>bvgVZ%6RG?$*$E^p|Te zUqaWe?kn)_woaY__j)a}@<#$6QQVs*@TYpG@D=mxQt(TG^FDD+3i-;%!MxzpCZB5r zf7U*33O^Nhk8^$mUv`F@2b%Yo-5sfy=ykE*kjtyTU-h78Z|g@{kM>@x`0t9`T>ru8 zJ?UrDI;u&j#(vEKKJu@^chEC8+5*pYM;$fbi}mmIbOY%nPe2a7Qr!D#Ri(eiEcqzu z@1XS}_5_y1X$$_~Tw9<0D89=ERrH9rZuo=HLI1m{oe!P+p}p10jveSU@gIHc!8cde z+>G#pVH}oB%8W1igSe&IeAlF2wU&7%bltmt*P$%@#Eqn?;!I0@zYK{6wdrw>v{-$oL7ghk=vxKU%{XpPxmE^NB(9A{TF&6bQS(Qt4H{c zlM^{-7~aDCN3SxE8ghS|+F<{m`5QfvkbGCV!gb)xKN^3`H_$Jd@s|DVkbUIV?Fc@} zudbG&0)CKsEg|i1Co>uOo%;y0p5U!@i*aTi3ANyv;yxbmxBW?s9!eMQRu?Bi-x-G8 zVO*=Dr1BVt9OqL)#^s9c!2jk(;squ0v_Y8O6pxu_%$Kxl1lbdzm!6*E;5>SR$Wr?6 z6CdI6{*Ac-@*3G+cmE^u6&2#`W%P2o*pqxClp`EX;8kes59l{4BvP$o=z?7z(_d&8 z5-)WG?;Hp6rQOERABVv0v)@Lo!u-S0mh_X0F0t!%;d&>797O1ucuCQ2SZ>u#>G#U# zRfHwT^qZ3T%$nMhw9CD^{LVZ~JT!EeOptf%#eBJzP|W+7Rmiuw-e6%P^?H7NY$C_j z>GOD#?d#mr%6`VmNvUQE9~k2}sv)=R8}XidzmCxBtRw5jroc1v@>Q0`5&N^;M|9s| zJWC^Kug`X^{O*^?vj>0G=gHFrKBNAit$1Eha^8gZ&9LU7&xsGhFRm)x#nuxA9kXl< zuzo4JA@c&`_w+>cmMXr2Ui6R`))iXh`szgT+&abCN;8z-hfk7I`p)e=dg=l1R_-s) zX%9bAoOO6s_XaNS3#0wkw(yJOdl33Bk7^kQdZr9aw9pn!@L@cyfcs9 z{|od*y7=QO*GDq$AKrga{LV#{oZ5*Sw@Cyf_+>tM&X@h>!)d+bqr@`$!X7xE{SobP zFPRJ9u#dg;or&}luJr$0O5#ec14rS9AN|h_4|w{MAO8!=Lg!aM!@m&PtS1B`bULtm)7_mehmib1?CGD{_?(hRtG-Tz0-5< zPSGN;!Q5~ppHI$VoTXjh+8B)GoF?)53+*xEMc%J!-=Tl~sLt=QPA3i@eAHARV<-c>cZxSzo8J-k+l2+deX_xzp(XWn-r_Xpz`-4vZt!#%G^cQ+m68a-| z=Oz3KjAt!%Ju=aRx7ip~GVa=ET(Fm9og#2?;hL1)vOA?)ocSSkGaBHe)CA5VVFQcl z4bL8)}j>3vl$#C(alm{b;|E1n&f}I@NtN&AS zk+Kk6#<7Xs*1;9*MaD%>%rwuIn$&@hd?=|Sd0(E!>*ANu7n}n_E|GU#3oDpg#M6b9 zp})&~7}}wWAtv(tT8TJc^ypc0nfiqs{tXkoVvSQP((aDB1^;ThZ;fSsBhF8+$vC%t ziK{@bOvFxzZ0nbYpHbgd``L{Aj-N-ys~9gQhY@;!c)1}7ZJTqEN=kW{4cOZ1<0EU#omh}(v zUF@eq;4K8b`0@046hl?d{`r~srQo|Q@w-KK@~s;Q1%HOh!QM=hxABB94(G&hu-NC6x`90q_5O{Ch)Uot;=V-I#@!Z;bs01H1vv6ao4jHh9hEyZ*F+~RxetGL;9ct70l@T?2Q{3GU3nTNAqGlkyqP2>W*)d^gU z{BwQ_KSNX?drQj%Z_?i7z<+~llYqFX-}IT*|+7fxm+| z@pjjQZ^%r#&_%;A^Ji*dgA6{8~8~hfyM&jzedgJO}r&uho6xWmqp- zpF9Fi_9F+={WWLopM@39k3L&!3tzN;B>Eq_x9ol|M6Vo}AFWA$nMQqI=r-u=<5z&6 zM#4u@ZveT)Pu5w&=z{6M$p}S|7;+bVfl6E?&Kp>KD@Av)arx=tX>`&T8d*7yPTWAB|*OJ~;c^tI&;j`)r5$g|O3s4%qLX z_zmh8=9U8Jk3Z%jslVjsYzCS5*{-5#Z+({tgiGRK>WzP)^8HbP!0;xZ0 zjuXIV!;VIpaU$Pn68vPJ<&o5D>!VKv@59v50K6)ek+}gJLQnV@zPb6r9l(K9&N!^uttJY=Pfj`Py#socR@6EY4(yU9BuN?HlM!v9hh?-d(6=*!|V0Tv$Zemc&TJ>cTrb>vkSuH-5MCHf|H!MI~*G!pPR zdVRH@gg=W{IT52|y{@|0Vd%fMV*EJwR-b96X|Jtrj%B=-hi+5oq=#$Jh4rD<=hCk6 z(v0Xid$QZwanN^@{OD7M@0JF~#1Dryr#(*PxnUU0Um@qrOKXa8n(Qt$r3@#FeUtYW z;j3?$)Kep4?9eXDJU0Y?=Zyp6cb!`=%_R*DydPaw`ys#IJNc_U4>;~$dG9*#Y!KH7 zpQ1-g|I`w_&=~K99{8Wda^8k@RBCc2bO{=#S?EwWJj%W%e3qD0x}q=YaTp1Fj;_XV zA?tm^knd(QTV8n|I%wo`BR9BD7F&RY&;&^}H| z8MBU+c9?IWPkMcRIUGS}?vuzadlj={Wbo7QK3^95&AgT3fQjH~aUoBd@%J_RE|jx= zpu-Arm0umX9_>~~ja?6Tt)0mC;KzlG*m2mw=dIf^K1yd&z(f2U`E$X0oNKs(e`1+D z{ge&2r5xbGF9{yZmqt7=nQvG{C=wsNi!*;m^kC5{m6+$# z#UCV|&WAFNMl&ODJ=i;%^3Ai>3iMYMmQz0RQW&|(>AL;2wenr)j33}jKM;EFeR%VZ zd^bD$-(N_Xd(_{Oa;e+dbSU$?2bM=a?Yxl?xZPt0mgo7&$(oe) znesvP=WWIbeYH|IBIJ{JPV~KJkDoS)dkKkOQbWNf*(W~@aQK5(E2Q5r?8MSeuXS!I z%JXh+EOg-fA9SVuvmEv<_gi%}{%5n8Ouh|2GQaLh{4?K?xKw`coQi#hJV`z*zBg99 zx6Ez$ebYI2;FBfHZSb(iy7?QTAF5OT3Hj-|vW`dJmi>m5_Xm<+R};soMJ}-`u9c+B z_59eOT|MgmI3zCT$1lG4zTh+S{hz{j=%E+?-~ZcT9=MEGekS-H9iL}q9`F3Bg*>9! zbofZhel7J4f#*^`T9fwT*4b?Z+@r=91|s*l^3Q*Q{;b@2triOX&Mu!klX)w0;;iC( z&W+cF{;^}-<89%^#YSJ+9o76fv<3GK$FBpg zC4MLD2gX^-E6a~+z+X)L_DNOf#4&+2yd}gZqg5M5ho^j=7*6Z?JTN_h3%3aF3 z*uh`Y-j(sx_n+_6l*w-hw-jr}>`2GN(2`s>1jiy&0A$Ne74q#T{= z8KJj!^bok#w?2OpzTh5_&auq%8yrW*UPZ6uiF2krGrQOUoLf@;edxFmv)@1M*TQaN zo;%^-y91x>3{DK;hnW{5=GD+9Ty!MkAN6b3rS!w_rj(tMv+aO?-}g&Wru;npajs@U z9tFM|oo}~=zuO<4nhy1?FcN;U;nRea#2Xt@D*PUz=W6(4=%p|@3O)&Gua!BQ(rGcM z=Hz+h<{yAxn*Fyy&*QsWU%7V?$vC|DW%LX8L7biwrJ)OR3sDz-BOdh*dPW!i$L8Ll z=zrWij1TtM{g)$?@AbsUq^Zw&EZ3x6{6lqVw>jve-|%<*b6(HA&obB(=z;L|B!nM6 zVOv+8y{djo+OOLG{*wCZsc*G=(!TEo%fg?hcQdmgay?#5MnZ>cizm0GU4ntJmP5O#rLAS5>&n&bchl&Bi@b*J9_xy%7QLYI<#UmLt~tfthzi+}f2t@S zokoApv)%vRYrqxSPp+-ee`qg!`3v)=@MBf`nM$^^W+F&0>N?mr{S&K4v^vpADa%yWsx% zFulP!mCV_pynjD+rKg}<Fy|lYbJ^W{{=uP3H{LRa!(2YD>iIKo0_v znAK3_&g%Xbp$GfgZd2qTcXMslVH}M_cFK?DL6{f%3{QoS>KacZ$p?6{R$pZQfzHE1poAgHVQR9iO>HoHW9uGf&Kcm7Qe)HwOl=@pHaR`)u zk{^yj#%bB#g8$GfgP;0%3+=guof|^`WN>&QcyKM;*G%yD>rcmw6Zw?F2)V9Q%M!m5 z6*L=jP!s<4AEHg{2X_|sP2P{<--mnH?|k{zefgeqt8EW_Gp&!m;?V!e-SZedV9)No z`=`@(TG_79@t+QdDY(jhe16zYLvOV0ob(qwv0{lgSiQWLlXiXX?^N(XYw<30XWU%$ z7IaBi2Z4eP+@4w&c`{nE4`Z@lsiBXITdFO5pw4UCGOjvb9t!-wYSd4li#-|D>LKmx zgD@-eo8iaLLg>%5Y>0j*-vaG~Hrr|11U~i!+amXV(83-=q9>D8&H9{l`$hyFt>dTI zPrw_rI7ya8d>8ZC)4_{8+jS@TBNO^&*!R{o;*1A+BJHhS)>4Xc z&^_;jqEAN?v;+3eK_?;Ypmz*Y+Mj4?-%hUHnN#4^<(#KtedFg{>;X^WfRcjmB5`-R zhJA_SPwLSfe3M>3oD54cj>_OA@0|46moLvr-BI1G)^qpUl4g6OTRAn%6srfzJz@T{qsuvJme8swvbnIJ-KLH z5qvYFJmVaqFUHTnyAYH3LB_rLa^t4FZ{2c}Va0^sM-JarvXcbgg|;sJ-6G!?m<}Jy z_;J2^M8ActrUqW(bzl5H{#@!A!L7Avx%TyI)9(^9q4djsQA6akneiV{pY=yhK^N|` zc|0G&=cy5XWS;-Rf909r(VG94A2>V{Ct^rHyZ^4uc;Scq`g~g2{j=9UzKe7f^!X>i zr3*j1)9gt5PFc=nsN&s}tlL#_khzLHq>F#Pk~#2zb5i@FC-4L{*@yCM=VBH4Q1rdL73cC4mjqwBC_P|PmZ#}WpO;ewHvO|+{910&rwsu*k z@(g{a<1q*y^`VcgPcu&(-oJl*=UWOoZN?9pQm^YbI{c2@yfd{+jf7&}q`zbUew2-d z7J5zk#s`YB_4@yYyrkE+GP@6;SLNwpfZlW}gWd%DkoB2z>H<$MmV7N9_Ue{v(*7=q z>e9TI_+@Foj5|x(X_KeWPoE9DFSh-?H;ZTQdJ~ zt)iy=!eS;rlJcbfi?-l-e~?pY$`;$UCjCFo$+=1VDc7?{A5?|*$y>W%llbW6UQj2;IvAEk1O%jpfz&Xb@> z+Mm73J;3zWcdtBD)XPP8TJrv)e_!-z;Xx~GNIR>W6Y3KW5d`3=3)`Xi)gAk_CqI&L zDqJRe!UxPN*PE2s53?V^N95~F%%;IR8XpX4zu=rWcO%=W_}|9PqrV%sN9Q76nmCAv zGXB5JMueVTY2AvU|N5^*);&k&jruWog_W)Fo|HD%9zi$oD5Fo2tEY+X+XBafAAUNO zTeo491zv_^`hA2MdaNx>Kro z*r};x_MyP_)gu?aOs`)b?k1(b-@Co}vG6%=hOVw5zdo9o{vB%>xo7{>x6l{xO_h5- zDY_->J}q}R#I{ucL6H)Z_VITG1{*TJ%%S3HmWnO_(ANp)pD#NHZ_uZs6v zqk!K_v5q1>kNSjVCDQzEgaLMHR4Mhl3FMspAL7gL`_<2?;FB(%Cnf_8KOQxWBlz;x zVO`=8uxBmewJC4SkPiSjEq951rqKRc_ioc;JX<6-aOg)H&1)w02L5~=eSqJeyb{#o zdayl#Q;pod2S2ta&+T4gZxTp*k)G?(|Ar6Q;$S1izqtx*+`1%vDx|p8Xo<8yMejvH{@Q!XpkfBB)vy;B@a@6GURqvu%Cp2TIL7wo|a^E=-s z*y!`Wrg zKCYB1_rE3WwBGj@74`Gecm?k1-LK0zKk|{^tjTx}f6RU&aNT)T@~C0YE{62a{zS%q0#ZJ~RjbB>(@ohIY9173;0zoz-U68H(mTibB4o2DG(tz;VhXWC*O z_trT_>I?l!M%$;KVs|C)delqRpG@^~J@foLRn;K>0ra2gj6-RUaM?suMsE?9YXWco z*3y~+4wW==Chcz;v&e0FT^+4Wbt+a~-a z`AeZ+L5(MU;gfde0Dl&GDt`%G(GT2j!uQ;lJ+~wJtm`zcid-v8(+>00K>SA7wK4Z* zLGP_l*QC9pAm~b&%E|soWV?sAKaqMyaKi6VfhhW>l-Zk`NvW62>4P+7z15x0yQ|;& zsOf;;udbX_75tw_X2xQV`498EA^f{@J9#O5X_YMcWgpAPmK5-xUKq?nzLFt`BzPQ%~hQ=-6G8cqj5? zj$21UkM1wp6ZpOOUw7*-$T~E@u}AylCDjdcLy=F+@TDDtV|E_=(AceA31$BGlgSWx zbNPJULvIHA&87$brM1UyNVy*begr=zI%?{-<(egfd1!qSOPr%B&f}05depg|C}bby zi=FS(1@GRehVSr0KCxPqg-37x{mX)HXLrF*GtOD_Ag62SVeCxw1n~uvH3xW?)z+1$ z;#9VV(R4o3mrJ4GF$pE!OxcaNe9x3|-on2tc3EqsBz$w_LpNp8U!MK!H1M#Wj-K%B z8+YdRWc=TGX=h4zE1zKenHNEvl5=qxL-9OtWF11hgxqsLIUE~|hbqPi84u+28z*C- zU--B^m5WT*w?uB(cY109A8dC)JL^4%g;!b8zMk3zTZ0MbZauq~NhnX+8K{zsWAf9l z!VkosBxHV6#C!VPG`}arkM|CGaGYs*=x<{JeUURGYbpmmFAw`)cjP`HiKDWkTg${x zV2`Z3ttNI9^GaFd9>3K!t%aV(hO4tOGGzQ;57AeX7fzV@v^zDvIAFkaa4 ziRBBSZ_cl83O$ie=o;cTk6qC#dOYQ;LaLhYE9%#SuF!}0HzEOPmu#sS=vwR&h$(mu z_a*+6dkT*~{}KGM{$aTfe{e27W$83_RHD^{j%vdtepBH1Ot}Y&a_ROUm!>4Y`&j5h zx>58=W8+=f#hNTy6I7ek<;g%a5h38_&;QJPi}KJboYfSAJ6gck1G#@6hkc&zwM^ zcS^No_%>Y}^vZp`#e9*w#NN^NDd%KON*RB}_Y)2vfp0O_dYjR*=qP>qQ^gpUpDP4otOy>^ZH1N$L2)DH4S zdj2VYAJv}6dBbj&h8|YeXn4T0wwtUu^s9*PLr-82yU0^im|e>HA?LK>c+~NDPR+lV zp+7rF&Aq1>cRh5`lkh`M_LCg)YF;4cjy?1P#sdxwIOoTAY}>mg`MHnpB-?4;_bRpd zG<2`(%=8>_x5qhi}P-@5}noEWXxHorqld%d3Ab9`xNxF_cfBL$N-O!eNk1NOvBZfop4~}(%>k~U87_;y z_Wh+z`ToU!RdIPwp2pju{9e1#e&`5YC!c18zPfS`{TImS&^;!493h-^5wpm|Q$cwqKkPvt_r?k{|_O=-`l zuYx!8tkHI4e#k%OAAY~xyq;IQXMdORRmHc-^XSpv+|I2&d~+*Vm-~6mVn5lw3EUgF z0~x1uv2%3SM~*j=CpbKKZv?q1-pSCifDgUGehc)#u4zg?%4swd@0&YBWYJ!g1Xqse zB|q5-p`1%RnL~a2V+jYk-s=usk$NQ|Uigjgi2UXzf#_+QsQY`Ag-<39`CFVq7|dPM zHM6336W@|j{i@O8{qs0-J9^t7-+?@9&V@yG8hv-1(6qBWNQnL`%uVsezN%XH((r$O zb8LY(=gS<%lXZI`^_jqR|4OSRc!pQ&__y(|=i{V&|J_vgO!#Z5kIp;UW$uJ;`Awv#c|1`Oic7Kd{JD4p=s&=(wZpxNgn3lXBoe?7W{lG;nQz1n-~3_-XIMuw$+pU(RN= zz%$w*scKeOU=yD%Y>ccic02)#LX(=Kxvdtp|$h1iuz%;XfG!jNFzL5-Ikb&jca#RM&sy%3&lBX~GX?&BO8PGlPL@=(`}0;7{|R+-yPDChKur@N+@&#>ptYID$eeMgNN4)D0k6GVtm`ArKg{bS|V2x{;C^6H$>jJo_fnet{(iKztmE%H}Qc_^o{H@n9Pq2#)F!~ zKOm>9|Kh10&@EZVl8^bj<+||m$lr%Pp50zuYYUyQyYgxHU6uW@;2rdCy1XwWG5kdi z*hgm`i)_-q8$uuWWxBo@1TQ|4=RG!r9Lj2tafOdaBi0dp(Qecm$Y;@DV^TU$EF><`LMi=f7~nl%fD$(?@g#bxUfRbjMN}$^c(JU-(=h-iGj#h z!OZVMAM~Z3SZc$;j!yXUU8358uS|(!p}yMCL7a1tr)2+vaWOZ}qGRMnZCtQFrbRAG zxu$|no1?Q!2Rb>U>=E=Je~Ie~9SDc)EAC6FR*8c_9?1_2{qd_OL*_O3&wrnFoLmAz z{sO+Q*JFcL$X`NUWab_2>*c;QMPA;-VjB1iJvasr;`l`mX!5uW!VtZkZA?}b-{;ui zlXfJ}gGNph;n-qyWga&oneTPo@SsET+E@d3CGSg|bx7Wv=jWzYKl#Q~j`vyt{Dgho z(%4><^Kz^m;IV-BZGc)4WvJhm*bwJqmb5 z^1xfTnCvb{>BOmxqG$2{>pfH{nsvEKT}??`rwY2U1p=NF?tE zIF3z9g`Y~`6TBSC#ox_87C4Dp(H{Jze0K{zD9%0Pncr7GDW8KaaSwU)hq-=#u#*$G zPX{DIf-d_P#z(pw6=nu;FWmC%+It%RFXyaW|3vVe#7B1oz7Kk(Ur2xXH&PF2pL}PB zL!Rv$Y$V7ye&a~um{@n2~MR#t%cXn!IY{P%HRlnXByc6@*bRIJ5sU4wHrnffZ zfY(|*HSs84xs%MGSMkd&qyONeiRf1}_sMWdXUiI$dC2>SbF9#tb#p7^`!^4XSBE}t z)|L+^Y4|+V7%9zse!QIlKlJVF&;C~ryn@BrII18I|9ZziSmlR;FZ)-H$2j>$`I>z9egb>xzOLk52mNAhXg3#Y zevk8hO;dk&IRhSwbI9DsBk$w+yp*JEVqU=?+7Wx(vEA@qoZe<$+kdZ@27m1Kfc7fJ zd3VVHu8DhgA#{!MP8{+5DtSlY{&n1Y2GX&9!FYma@yfNq6c1yo2fnIutdB$v?VJ62 zJ%T<7zj`L+r+FPrpW7F{wnIBAA7nIosG1c0%s3~O1~N`AiMbX0?KjSm1JBM^9p7WU z)Tm#r%eb*_symEZ^IF(bz^8rpK9fV{@J^qWO?4G-?pZ7$tR7Cgv{UAr>b$A4OR#yJbU<*<)lx)0v?YhGPLKcGMB(2;(yA7nfZwQ|9u z@WY$W^(kppCm#Ja^`rMg%I*0t{?r3L!ctC!FZ@p(4Sx1*E1nVk@oF606S{G&ui?Q* zW&8r^4NvTE%VAu))A`J^A0H^jTh<9a<3wJNgShR+|%F1~t&La`70za)UEtY%*J{g1fGvR+DGnR21 zB_+>8X#e4FCp$7fSR+|infT|<;6rmg@xsI3ga0e%Pn+ypMC`{)yr3q22!9jvho9G1 zgkG(B?^wzklf;No;ofW4SDYgrCS_hVi%vJLY4k`ip7<7r_LsOgQp!YUoE1LW96Sm<+PxWzKS!;T9-IDH2O7-~I13Nv ze;554*{>fwf^XC0d%^EE#YX{;wtYwZ2Fyd)6(TQ5243JaR@5{3z5abCpg-)O=1BOy z)vZkJ(meYZf;aB|hVb`Y-MuYz87;Sa4s=!o6niqh_*)|Ivo22P*SU4&dQ^Sxm+_lY zztuXL;^P{bk_o)sDhm+!u@f-v^hdfV^hZSexx^F04^{C4M7CIh z0B^x)UinfQdR6^VU7l4#=3B+N!v^1b=y|{4!7p`xkQ4ihWu_tYTDaXpKbjkv%N+4x z>Ees;Z?;X*SD!X}XjY~HeX6pAO8}+N5sn;ybtYm z?h&PDm-i27FC^~V0AJ$jgHo&*hl$I54y;>VJ}I3Gzcs$m>{-XYXUVxAr+CH%wjrhCzW9=f9%xKd+W{UuTKln-7o$1Zr|10P zEcyujfL=;(DDqcMmP6#`DE4CyJ)&BY_bn_MGZV|(z%R7#l!U$(0pTK#OHPoa+;H2- z4c~plJ`eD3=y5BhnHOei@zuHel%f9!hpsjP9wqYU^BPh#E(h*lRbX#!^fViul2p_Y3NbeGE|!WW(N6z$gz>k z_R`EZ2bsQ3qnAh@+!eZ2`&<8yz>{+%-q$RuP07PV9A9_5Dg5%VM{Q#eO@G-m?O!&qPCeW5=? zz8~!FT)EK%&cgj)&y7THr2gaAK7q~!*BF#_S?s~uw4DU^iH1UdpLJiwto&#bKN0=2 zt=o#=2kwK`9{rZL$a4vQ?_5nThCGv3Iu<;lYAkYgE13;t9=t^syNT2v_D|0#xewxY z*OT>{JHvW|`*;tG4E>PT`)1i={!9{>>VaSC`tvyZ7f-+Vy2E$LU^(Xq9pa?SKj;CO z4;}Kni+@2C$L=DfJY!cQN2)sNhm^~&;CSPA%xuPixcRR=ZYt;%bAgnUq1z47Kb7t! z`{DHCYB|Ti{=kvs4UcS&nVySWXNZfFXVM+`l$?)zAHOmB?4H#P@jqmYMn>e$VuQ?~ z9(ryp^_`*b%lEz>Oy!Q_RKN}0_A|3n`E86Gsh3j|;AQ@pOpxnzG0cdbA|5r7nul%$ zj$6RL+-DSys)}(mj2ZZs^&{~gGM>nnugmBG0$!xVd^V>1I+!poArHubq961U?I5>~ z_!&GqxuUzC$RGQ8A#(7WtL4~{_2-)gm_J+^g{ zQs%0%&m#87q!FP9K4hIB^uPLK6aS;bdSQ7g*O)E);s-O47gsQ-nF zQc2O?Zqh<8K==A)NNIPB@gz;@kIs>2g+2=NX-e|jT8_xq$i+V<`eQXPsh2ps4WE;@ z{H;X7<9EY)v}Q^?0Qskte4o0=i~KVVQ$N+D@6(@0yvmEugr2Mm;47I2o{vXo)CW&p zxLaV|Pl+445z^1MTG}z0mv1*ylZgHnzt=?W(i@-jCdjiU56C!>@5cX7uSJxD-0H&! zKH!)~#{=GAo{a_I2uDHFgHJ|AW()bZjZXPo`q$g@tP7mVQn%frze2S%tZVG+e}Hg! zA#c5ODsVynFIZg--#dqAA0eG~&KS))^zSYwuvelAe$Y@?9DBq86i3=!s{hIX-*MDc-aT9MgRaDbL{_?0w=8J^9`#|4QV{InRdlL;m6*oH$eUEtwf&gxL+ z^Fb7#cYuq)41qH-AFfFKH@5DD!hac~BlC)#iO!kl@i%03S)K{=A2`6xvUs}Q8~<}W zOfGfpmIHrZn+)#=Ty840qid{tS%^e6kGwx;*s&h>py!5Z!FL*m=N01Q=fjYCq?Pdn z?)*5IHL-)M@fqtizRQiu@U3k;8a=_$S=CYmwk*P0qF*_{7QJ@1h*D z0F#pVZQ>+!;ZQgFLbu;}>9t+OIAz|ylM;KtHD=zEez#YW?@Du2f_<$2c?y;ro3t94nIlyeE_K>U<$cg4>1vpM3m zi1XmyF63$b&Fw`Oeq5J)JRZ-++)mCxKh8G9@56n9-&B^#IMn8XJ;gI=chGP2=v3>J zh2bb(gx}e(pVXz?oK#|&4`UrH%k$|scc$~|Yl?ZFdGufJNL&|wlt*XNQui-w^1PdR zKalTMD`q70$hcw;McCUFPpnrt@5!@qppj$p&UF$K z;lp_HRqPzcZUsls)2sYx?LR{Y?A|;6O=(9@M!Qm4y?oiDx^l9Vw(Lv{59~4rH_md{+E1e z4t#Xwn&dmjUuPH(G;(BV&KFh8$|$9wo5Q^_Z+%+<$E@$~dbX-$G8y=p`JIVc(chps z0B<16$63LDE15l!=W_lo(ipzEJ%8;`*HmS5pwTnHTkbdDYkQGIIlxm{OB#L}I-dn% zcM>0w)Q$=NbG_NJDfD0V&*b-R?1%DwB6y{fMouQVdP3@zoeXl0pQN2%qhI`bwWjwU z;8XUcG@L;`sif2=4)M%kd@(zj7fsenGXJHEetizRFI~J5*H4j)Li0V7an<%kpL{oh ze`~lY&m1#vP!`JPxskL}(9MAI%_n<@4f!tlN}GK$kNa;wfbUr!CT8|L=y-DPl|bm{ z-*Ola{F?u&@Lv`Eh5da?zI*gN{nP`$FF*b%`atym^K&=3yy`37E7h9n58+FXen*=J zCB}8#+3w^hB~Kc1Vy@Sk7uX-PZ!9f)#4q*w5*MVH$2Ib9axd90A3Erpe0`TuR>d6- z@_XR579^J#?_w$!BY$SmomrR&zPY$Iy)TfwZx-@>tJ=q2t9Z9&+G4NcKkD8%m-cq& zcd);=hJ>aC3LCAceDoZe&^nVqVlJ5c?!>7{s%3ImF9r zOYuEvW9km&g8zJ(q6;bl<_Yl5)ZDKv{xfv6!g45 zKWF3xPx5Xx7%$?=C&N(s&m<279>1rDNM#|}8J#c-*g2Wkz{jW&Axl~JFCu}np2#9! z%$Lb}LgdTZUqCM+Kjx6}M(_D9?~xPTI*{)~er4V=8qWq&#-qG;h-WfpQYnXdt#x&D zh+LAtb~G^2FNfVhPRc}LJ|leSUo}3J=abzV=TgSI!zF)sK_3LAI zJrKPn^1=S;{jj2y#{Od8cmB1ZBW1n<-@)(QoI-0)??n_iL*4CCuW++ePYC@|ZT2f! zFZ}d$H-?UdVA)}Q#SU;&UE%YI>QD7;vTF@_UocYmFMz9BH=Zcs3A*ePQ5F(&57AGN zU23>v>3!u?zN{D^k-elMJXn7G3?JDo1BB&e&O8!J;XV>-Hfg=uGU4}1TXXn{9zW& z-UvC>g;zfa_9)-{X}^D;@9e~KYftJs@%(Q^lmjkYlzz&|g_AV(tup)r{2bpvZplZB zp%j8&kAGjeVyLe@3LPC9dG1=q>LX?*aJnoLHl{UV!7r| z`IfxSKcL;>^*ra%sXzS1H(!=#{5XveewjNYFE9JmKKYhR`e8b_l7`>ZBsQe9^mwGC zZ2c#G55;~}Sm4ES-2G3>L5B!C0D3Hk_>18`KMr`$Jo{NbgTAo$clLJ_^)jKvi^10k z$_?|r1-%shKEoJkO@Df9FfYNs)$#zoSkIn zm0_T(in4w>fA4(+BUYdGC4n#?VP!3`CdoQyo$2N6G!O8 zx%B=sHJ#s*N|!Zs)W{X^*R@jbAg|{u=##K$46!T0{~Njs{$_Dl#=ye=d^LG8g8t0I zE%X5UyNM3-RK$5T_Wu|M$%7O@uLSD{1%88;#3eJ%=dIT~=Jm#zS`H~OBN7^XEdquG zo<{u>@S4hB7yLGKojhK&pTE1dOa1lHkJ~-UVsbf`bCBN=iLE@I2fnc&?;ot>XG6yC zy!Oo`<53KnC?NQ!-2Y}*+G(YJ__2(`cN0thXtLiL1PeQ&Z#Xspe0sY6boicdQSetq z?pd3bq&-9aX>U22Nc)q98%Vzjh8Fyro!$8~-*>r(jCRwZ&Hfw*<7&kDa@CMU=L&ZM2Hr7u06 z-EX}LT#R@m_m{D5ZRShD-=)3_eõ&a1L())n;z#8#bgPa4u-X?EpDC6x0y<7yo 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 M*oj7o9@PE+0f48t!vFvP literal 0 HcmV?d00001 diff --git a/res/gamedata/textures/fx/water_sbumpvolume.dds b/res/gamedata/textures/fx/water_sbumpvolume.dds new file mode 100644 index 0000000000000000000000000000000000000000..4318104d27883840e4c698ead23b965363987274 GIT binary patch literal 349652 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ôL~tkJ!h`x6#bz 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 z%0A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%0Dmd` zrSO-+UmAX{T`BylLUS=^2NL`;`19en!f%DY2>v4Yt?(DYzZm|I?iABDMb&OYP|o}f zZB7i?kX|dya)16mC3T1>q0A9V{u4ii-y)haYbfFJIIjN}Z_^z{Q=WZ>`o8w-kn-|h zN8IWDFa4X6=6206wCg6z6cOQ?Se>HF1RYiMmQH_65z;PfMnD=dLz_lz z@<>xvDbmExnZUl@lu(t)bbF95>APeIGv24I^h+jP=_JOS@e-{3;@PFd||Np1t6&lcoGVAR8R?R&4z2IjqkLA4gpoJWM*-)R?{05ho z`;EAh=9gfRH2(&KlcaaDO!*D`mGD=>UkQID{FU&pcl%Eb;{4(FhrhXe>pr(lje!C& zTwD0{@ay5%!>@-wVfs|29sJATFNMDp{!;i$#R?`3ejfff_>rO)`u3-i(|g<-W`|$AE&i}u4_pVuE(R(T@CN1yZ&-^aGg8h z=c=lcJ(*QT_ayr&!iWj-IO1I{nl4~fyG6R|#Uy^$qhV@ie+&F!t@&=<_6{IJ^E%Rr@N1Z-wzu*5;rVacC_y@v2@W^q` zuJB__KFPq}7JfbadieG5>*3eKuZLehWD2b%`ziYq6BTd3KZc&eM#0|}{`v41!Ec4% z3V%`G(abo)BpE@>l#GVo2!C^%dy9`Gc6Uc`biy(B~5gnRyKlDL5P_aCpysD>F5>P8Vu z-q^vBp7D&77$or}rgHvd3FE_zAu$n-lv_o2iuye5i>S{}uZdjsC@9J>mE=~C)C`Qt zb{&!%T&IG}t~;6Ux&B_ey7Bxkmd45}iLQ#<+D3Z~y{Gm1xVPgdD(HwOzvp~-p!2S= zx6`#~jPtL3-JGXStDVlX%o^{27u$hf{3OOY7!-2&HA^PA$38qyMIse_x;=Lg{wDaF z;BTt^m^;1OP7i^fhrc8I9pUdd^`Z!EL&+gey6_ft`?w zA?;Y-emeZ`kgJ)g@N3~$!aorHfuEl)9k}X|b|CzU@;5 zZ##qE<`)wDzVI)C-wJ;b{6+9v;kUv+<-hqqmMGaeeS+tM6$3s0^)IbQjuo{Vq+G;% z91D=j{!H-aAM^|38)roEkJbnC*AEJO&2bq&aXQ1d6?F#{ITLCFk9MkU{|i0X2lxD` z5Gi?8uOJ`x_oW(VYN$u+)zn{359;)H9#qK(&$>FBa~?|4ba%N{7m<-06>(^EWcm$- zE@U`C&mp+&D~JGB8JXI+om=L@m~7X%uTHym-`v=^?$Il*l?|Sai|;UhzQ89+FyHMq zTI)Qh=;y3WSmu1VY`gQ+$ztcezfzp#4?LYy_DGuRu36zUZ81om)lbSLN^kgk!r%MX zUxT_&d`H&YfB&|OiG}|r{7vvT!QTXb6Z}o^H?9AS9uL1B{*Lf>OgcvUv|}j6F|Bmj zwM4n&;Q;hs&-8OVeqZIl+R2U+-wB)EDPf!Q(3)@fIr#g-KQQ8uX<+(wMgP!|?zm?z z6n;JYdieG5>*3eKuZLd`zaIX!t;cb1SD~B%zZHHf{8sp_@LS<8g5L^%^SBQ9IryKI z-QFg>tv!Hkj~;vGucg)r6bmbso@C!`pY?%z0(=;z9!KWH2rf5J7mvaWITgU?*! z8^*XMyFy%(|CU~BIYP@%=2U`RG>?dK7LujAET;v+*oY&~%%zw!B z{!}4V{+Zs*aA$L7*U$F5*DpewAL$hbe{cAE!{2*Wy}Y@vr<`REf2Ry=g1-s=Cit7+ zZ-T!G{-#}*+1ugm887(#;P-GhBW&i_Deybag@rqArS@{TW|$oCJ1!oJcl=!E?||Q~ zP(6>Ea-Uxv%!u?5pPumdRPUji&kcVV{CfEH@ay5%!>@;555FFMJ^XR*&%2|$+zI|H z_^pe{*Nfn{I(A$tg5L^%5&Ty8lXfbl!SFAG-@fyptf%{PojG0_8P&s6WuNC6SF88z zv`flFp^fDOLe6iYf_bN`KYuEulz))>6@TG}t^7AvC-6J&Me*w&$$86{%(Lf6z!$KA zlv1BFBK55>jH+w1jQab{�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%0Dm6-Jp6h1^YG{4Uj~1?rfqomXMVc>kN#8OUj~1k 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

    1|2=2c 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 iYK)bpon^6f8g{aUpys!q2StzyBXgg(MaL literal 0 HcmV?d00001 diff --git a/src/Layers/xrRender/Backend/D3D12Backend.cpp b/src/Layers/xrRender/Backend/D3D12Backend.cpp index 9bf0c6b9f3f..336505be26a 100644 --- a/src/Layers/xrRender/Backend/D3D12Backend.cpp +++ b/src/Layers/xrRender/Backend/D3D12Backend.cpp @@ -436,6 +436,10 @@ void D3D12Backend::QueryCapabilities() { m_capabilities.bindlessTextures = true; m_capabilities.maxBindlessResources = MAX_BINDLESS_TEXTURES; m_capabilities.shaderModel = 60; // SM6.0 for D3D12 + m_capabilities.drawIndirectCount = true; + m_capabilities.multiDrawIndirect = true; + m_capabilities.descriptorIndexing = true; + m_capabilities.shaderDrawParameters = true; // Check for mesh shaders D3D12_FEATURE_DATA_D3D12_OPTIONS7 options7 = {}; diff --git a/src/Layers/xrRender/Backend/VulkanBackend.cpp b/src/Layers/xrRender/Backend/VulkanBackend.cpp index e6a7e574d95..036b5395490 100644 --- a/src/Layers/xrRender/Backend/VulkanBackend.cpp +++ b/src/Layers/xrRender/Backend/VulkanBackend.cpp @@ -187,7 +187,12 @@ bool VulkanBackend::Initialize(SDL_Window* window, u32 width, u32 height, bool e m_initialized = true; Msg("* [VulkanBackend] Initialized successfully"); - Msg("* Bindless textures: Yes (max %u)", m_capabilities.maxBindlessResources); + Msg("* Bindless textures: %s (max %u)", + m_capabilities.bindlessTextures ? "Yes" : "No", + m_capabilities.maxBindlessResources); + Msg("* drawIndirectCount: %s, multiDrawIndirect: %s", + m_capabilities.drawIndirectCount ? "Yes" : "No", + m_capabilities.multiDrawIndirect ? "Yes" : "No"); return true; } @@ -496,9 +501,28 @@ bool VulkanBackend::CreateLogicalDevice() { vulkan11Features.shaderDrawParameters = VK_FALSE; } - Msg("* [VulkanBackend] vk12.drawIndirectCount = %s (device reports: %s)", - vulkan12Features.drawIndirectCount ? "ENABLED" : "DISABLED", + m_featureDrawIndirectCount = vulkan12Features.drawIndirectCount == VK_TRUE; + m_featureMultiDrawIndirect = features2.features.multiDrawIndirect == VK_TRUE; + m_featureDescriptorIndexing = vulkan12Features.descriptorIndexing == VK_TRUE; + m_featureShaderDrawParameters = vulkan11Features.shaderDrawParameters == VK_TRUE; + + Msg("* [VulkanBackend] feature clamp summary:"); + Msg("* drawIndirectCount = %s (device: %s)", + m_featureDrawIndirectCount ? "ENABLED" : "DISABLED", sup12.drawIndirectCount ? "supported" : "unsupported"); + Msg("* multiDrawIndirect = %s (device: %s)", + m_featureMultiDrawIndirect ? "ENABLED" : "DISABLED", + sup2.features.multiDrawIndirect ? "supported" : "unsupported"); + Msg("* descriptorIndexing = %s (device: %s)", + m_featureDescriptorIndexing ? "ENABLED" : "DISABLED", + sup12.descriptorIndexing ? "supported" : "unsupported"); + Msg("* shaderDrawParameters = %s (device: %s)", + m_featureShaderDrawParameters ? "ENABLED" : "DISABLED", + sup11.shaderDrawParameters ? "supported" : "unsupported"); + Msg("* runtimeDescriptorArray = %s", + vulkan12Features.runtimeDescriptorArray ? "ENABLED" : "DISABLED"); + Msg("* partiallyBound = %s", + vulkan12Features.descriptorBindingPartiallyBound ? "ENABLED" : "DISABLED"); } VkResult result = vkCreateDevice(m_physicalDevice, &deviceCreateInfo, nullptr, &m_device); @@ -715,9 +739,13 @@ void VulkanBackend::CreateBindlessResources() { } void VulkanBackend::QueryCapabilities() { - m_capabilities.bindlessTextures = true; + m_capabilities.bindlessTextures = m_featureDescriptorIndexing; m_capabilities.maxBindlessResources = MAX_BINDLESS_TEXTURES; m_capabilities.shaderModel = 60; + m_capabilities.drawIndirectCount = m_featureDrawIndirectCount; + m_capabilities.multiDrawIndirect = m_featureMultiDrawIndirect; + m_capabilities.descriptorIndexing = m_featureDescriptorIndexing; + m_capabilities.shaderDrawParameters = m_featureShaderDrawParameters; VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(m_physicalDevice, &props); diff --git a/src/Layers/xrRender/Backend/VulkanBackend.h b/src/Layers/xrRender/Backend/VulkanBackend.h index edd29c52a25..11016c0565d 100644 --- a/src/Layers/xrRender/Backend/VulkanBackend.h +++ b/src/Layers/xrRender/Backend/VulkanBackend.h @@ -82,6 +82,12 @@ class VulkanBackend : public IRenderBackend { void CreateBindlessResources(); void QueryCapabilities(); + // Feature bits after CreateLogicalDevice clamp (Apple/MoltenVK may disable some) + bool m_featureDrawIndirectCount = false; + bool m_featureMultiDrawIndirect = false; + bool m_featureDescriptorIndexing = false; + bool m_featureShaderDrawParameters = false; + VkInstance m_instance = VK_NULL_HANDLE; VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; VkDevice m_device = VK_NULL_HANDLE; diff --git a/src/Layers/xrRender/Bindless/BindlessTypes.h b/src/Layers/xrRender/Bindless/BindlessTypes.h index 629e50528d0..c6467b3d7a3 100644 --- a/src/Layers/xrRender/Bindless/BindlessTypes.h +++ b/src/Layers/xrRender/Bindless/BindlessTypes.h @@ -42,14 +42,15 @@ inline const char* GetTextureTypeName(TextureType type) { // GPU-side material representation - must match HLSL exactly! // Uses SM6 bindless texture indices from ResourceDescriptorHeap // -// Layout (32 bytes total): +// Layout (48 bytes total): // Bytes 0-15: Texture descriptor indices (4× u32) // Bytes 16-31: Material properties +// Bytes 32-47: Extended bump / tessellation struct alignas(16) MaterialData { // Descriptor heap indices (UINT32_MAX = invalid/not present) u32 diffuseIndex; // Base color / albedo texture - u32 normalIndex; // Normal map texture + u32 normalIndex; // Normal map texture (s_bump) u32 detailIndex; // Detail texture u32 pbrIndex; // Packed Metallic/Roughness/AO texture @@ -58,8 +59,14 @@ struct alignas(16) MaterialData { float alphaRef; // Alpha test threshold (0.5 typical) u32 flags; // Material flags (see MaterialFlags) u32 shaderVariant; // Index into ShaderVariantRegistry (0=default) + + // Extended (must stay in sync with HLSL MaterialData) + u32 normalXIndex; // bump# (s_bumpX) — error + height + u32 detailBumpIndex; // s_detailBump + u32 detailBumpXIndex; // s_detailBumpX + u32 tessMethod; // 0=off, 1=PN, 2=HM, 3=PN+HM (CBlender_Compile) }; -static_assert(sizeof(MaterialData) == 32, "MaterialData must be 32 bytes for GPU alignment"); +static_assert(sizeof(MaterialData) == 48, "MaterialData must be 48 bytes for GPU alignment"); // Material flags (must match HLSL) enum MaterialFlags : u32 { @@ -73,6 +80,11 @@ enum MaterialFlags : u32 { MAT_FLAG_HAS_PBR_LAYER = (1 << 7), // Terrain has PBR detail textures MAT_FLAG_ALPHA_BLEND = (1 << 8), // Transparent alpha blending MAT_FLAG_WATER = (1 << 9), // Water surface (Fresnel reflect/refract) + MAT_FLAG_HAS_NORMAL_X = (1 << 10), // Has bump# (normalXIndex valid) + MAT_FLAG_HAS_DETAIL_BUMP = (1 << 11), // Has detail bump pair + MAT_FLAG_HAS_LMAP = (1 << 12), // Terrain/object has lightmap (lmapIndex valid) + MAT_FLAG_FOLIAGE = (1 << 13), // Trees / bushes / leaves — subsurface scattering + MAT_FLAG_HAS_SSS_MAP = (1 << 14), // detailBumpXIndex = SSS thickness (R), future hook }; // ═══════════════════════════════════════════════════════ @@ -81,12 +93,14 @@ enum MaterialFlags : u32 { // Terrain uses 4-layer detail blending with RGBA mask // Each layer has: color, normal, and optional PBR textures // -// Layout (64 bytes total): +// Layout (96 bytes total): // Bytes 0-7: Base and mask indices // Bytes 8-23: Detail color indices (R/G/B/A) // Bytes 24-39: Detail normal indices (R/G/B/A) // Bytes 40-55: Detail PBR indices (R/G/B/A) -// Bytes 56-63: Properties +// Bytes 56-71: Detail bump# indices (R/G/B/A) +// Bytes 72-79: Properties (detailScale + flags) +// Bytes 80-95: Lightmap index + pad constexpr u32 MAX_TERRAIN_MATERIALS = 512 * 4; @@ -101,22 +115,34 @@ struct alignas(16) TerrainMaterialData { u32 detailB_Index; // Detail texture for mask.b channel u32 detailA_Index; // Detail texture for mask.a channel - // Detail normal textures (s_dn_r/g/b/a) - u32 normalR_Index; // Detail normal for mask.r channel - u32 normalG_Index; // Detail normal for mask.g channel - u32 normalB_Index; // Detail normal for mask.b channel - u32 normalA_Index; // Detail normal for mask.a channel + // Detail normal textures (s_dn_r/g/b/a) — s_bump + u32 normalR_Index; + u32 normalG_Index; + u32 normalB_Index; + u32 normalA_Index; // Detail PBR textures (s_pbr_r/g/b/a) - optional - u32 pbrR_Index; // PBR for mask.r channel (R=metal, G=rough, B=AO, A=parallax) - u32 pbrG_Index; // PBR for mask.g channel - u32 pbrB_Index; // PBR for mask.b channel - u32 pbrA_Index; // PBR for mask.a channel + u32 pbrR_Index; + u32 pbrG_Index; + u32 pbrB_Index; + u32 pbrA_Index; + + // Detail bump# (s_bumpX) for dual-bump — same naming as object materials + u32 normalXR_Index; + u32 normalXG_Index; + u32 normalXB_Index; + u32 normalXA_Index; // Properties - float detailScale; // Uniform tiling scale for all 4 detail layers - u32 flags; // MAT_FLAG_TERRAIN, MAT_FLAG_HAS_PBR_LAYER + float detailScale; + u32 flags; // MAT_FLAG_TERRAIN, MAT_FLAG_HAS_PBR_LAYER, MAT_FLAG_HAS_NORMAL_X, MAT_FLAG_HAS_LMAP + + // Lightmap (s_lmap / L_textures[1]) — classic Blender_BmmD + u32 lmapIndex; + u32 pad0; + u32 pad1; + u32 pad2; }; -static_assert(sizeof(TerrainMaterialData) == 64, "TerrainMaterialData must be 64 bytes for GPU alignment"); +static_assert(sizeof(TerrainMaterialData) == 96, "TerrainMaterialData must be 96 bytes for GPU alignment"); } // namespace xray::render::fg::bindless diff --git a/src/Layers/xrRender/Bindless/MaterialBuffer.h b/src/Layers/xrRender/Bindless/MaterialBuffer.h index 3f8925633df..dbba0549d37 100644 --- a/src/Layers/xrRender/Bindless/MaterialBuffer.h +++ b/src/Layers/xrRender/Bindless/MaterialBuffer.h @@ -14,7 +14,10 @@ class MaterialBuffer : public GPUStructuredBuffer { u32 RegisterMaterial(const MaterialData& material); void UpdateMaterial(u32 materialID, const MaterialData& material); - const MaterialData* GetMaterial(u32 materialID) const { return Get(materialID); } + const MaterialData* GetMaterial(u32 materialID) const + { + return materialID < m_materialCount ? Get(materialID) : nullptr; + } u32 GetMaterialCount() const { return m_materialCount; } diff --git a/src/Layers/xrRender/Bindless/TerrainMaterialBuffer.cpp b/src/Layers/xrRender/Bindless/TerrainMaterialBuffer.cpp index d8ae7aa2461..91ebc588e7a 100644 --- a/src/Layers/xrRender/Bindless/TerrainMaterialBuffer.cpp +++ b/src/Layers/xrRender/Bindless/TerrainMaterialBuffer.cpp @@ -31,6 +31,10 @@ void TerrainMaterialBuffer::Initialize(fg::RenderDevice* device) defaultMat.pbrG_Index = INVALID_TEXTURE_INDEX; defaultMat.pbrB_Index = INVALID_TEXTURE_INDEX; defaultMat.pbrA_Index = INVALID_TEXTURE_INDEX; + defaultMat.normalXR_Index = INVALID_TEXTURE_INDEX; + defaultMat.normalXG_Index = INVALID_TEXTURE_INDEX; + defaultMat.normalXB_Index = INVALID_TEXTURE_INDEX; + defaultMat.normalXA_Index = INVALID_TEXTURE_INDEX; defaultMat.detailScale = 4.0f; m_data.resize(MAX_TERRAIN_MATERIALS); diff --git a/src/Layers/xrRender/Bindless/TerrainMaterialBuffer.h b/src/Layers/xrRender/Bindless/TerrainMaterialBuffer.h index dea849071f3..d2bfea22417 100644 --- a/src/Layers/xrRender/Bindless/TerrainMaterialBuffer.h +++ b/src/Layers/xrRender/Bindless/TerrainMaterialBuffer.h @@ -14,7 +14,10 @@ class TerrainMaterialBuffer : public GPUStructuredBuffer { u32 RegisterMaterial(const TerrainMaterialData& material); void UpdateMaterial(u32 materialID, const TerrainMaterialData& material); - const TerrainMaterialData* GetMaterial(u32 materialID) const { return Get(materialID); } + const TerrainMaterialData* GetMaterial(u32 materialID) const + { + return materialID < m_materialCount ? Get(materialID) : nullptr; + } u32 GetMaterialCount() const { return m_materialCount; } diff --git a/src/Layers/xrRender/CMakeLists.txt b/src/Layers/xrRender/CMakeLists.txt index c257fc27e8a..8554035440a 100644 --- a/src/Layers/xrRender/CMakeLists.txt +++ b/src/Layers/xrRender/CMakeLists.txt @@ -63,6 +63,8 @@ target_sources(xrRender PRIVATE FrameGraphPasses/ShaderConstants.cpp FrameGraphPasses/ForwardColorPassSetup.h FrameGraphPasses/ForwardColorPassSetup.cpp + FrameGraphPasses/DepthPrepassSetup.h + FrameGraphPasses/DepthPrepassSetup.cpp FrameGraphPasses/HiZBuildPassSetup.h FrameGraphPasses/HiZBuildPassSetup.cpp FrameGraphPasses/SkinningPassSetup.h @@ -73,8 +75,20 @@ target_sources(xrRender PRIVATE FrameGraphPasses/StatGraphPassSetup.cpp FrameGraphPasses/DebugDrawPassSetup.h FrameGraphPasses/DebugDrawPassSetup.cpp + FrameGraphPasses/RainGPUSimManager.h + FrameGraphPasses/RainGPUSimManager.cpp FrameGraphPasses/RainPassSetup.h FrameGraphPasses/RainPassSetup.cpp + FrameGraphPasses/WetSurfacesPassSetup.h + FrameGraphPasses/WetSurfacesPassSetup.cpp + FrameGraphPasses/SceneReflectionPassSetup.h + FrameGraphPasses/SceneReflectionPassSetup.cpp + FrameGraphPasses/RainShadowPassSetup.h + FrameGraphPasses/RainShadowPassSetup.cpp + FrameGraphPasses/AmbientOcclusionPassSetup.h + FrameGraphPasses/AmbientOcclusionPassSetup.cpp + FrameGraphPasses/SunShaftsPassSetup.h + FrameGraphPasses/SunShaftsPassSetup.cpp FrameGraphPasses/ThunderboltPassSetup.h FrameGraphPasses/ThunderboltPassSetup.cpp FrameGraphPasses/LensFlarePassSetup.h @@ -115,6 +129,19 @@ target_sources(xrRender PRIVATE FrameGraphPasses/ExposurePassSetup.cpp FrameGraphPasses/TonemapPassSetup.h FrameGraphPasses/TonemapPassSetup.cpp + FrameGraphPasses/VolumetricPassSetup.h + FrameGraphPasses/VolumetricPassSetup.cpp + FrameGraphPasses/ShadowPassSetup.h + FrameGraphPasses/ShadowPassSetup.cpp + FrameGraphPasses/LocalShadowPassSetup.h + FrameGraphPasses/LocalShadowPassSetup.cpp + FrameGraphPasses/ShadowCascadeFit.h + FrameGraphPasses/ShadowCascadeFit.cpp + Volumetrics/IVolumetricSource.h + Volumetrics/WorldFogSource.h + Volumetrics/WorldFogSource.cpp + Volumetrics/VolumetricRenderer.h + Volumetrics/VolumetricRenderer.cpp FrameGraphPasses/UIPassSetup.h FrameGraphPasses/UIPassSetup.cpp FrameGraphPasses/SkyPassSetup.h @@ -136,6 +163,20 @@ target_sources(xrRender PRIVATE FrameGraphPasses/ReSTIRGIPassSetup.h FrameGraphPasses/ReSTIRGIPassSetup.cpp FrameGraphPasses/ClusterLightPassSetup.cpp + FrameGraphPasses/TAAPassSetup.h + FrameGraphPasses/TAAPassSetup.cpp + FrameGraphPasses/BloomPassSetup.h + FrameGraphPasses/BloomPassSetup.cpp + FrameGraphPasses/CASPassSetup.h + FrameGraphPasses/CASPassSetup.cpp + FrameGraphPasses/SSRPassSetup.h + FrameGraphPasses/SSRPassSetup.cpp + FrameGraphPasses/SSGIPassSetup.h + FrameGraphPasses/SSGIPassSetup.cpp + FrameGraphPasses/ContactShadowsPassSetup.h + FrameGraphPasses/ContactShadowsPassSetup.cpp + FrameGraphPasses/DofPassSetup.h + FrameGraphPasses/DofPassSetup.cpp RayTracing/RTAccelStructManager.h RayTracing/RTAccelStructManager.cpp @@ -263,6 +304,7 @@ target_sources(xrRender PRIVATE r__scene.cpp r__scene.h r__sector.cpp + r__sector_traversal.cpp xrStripify.cpp BufferUtils.cpp diff --git a/src/Layers/xrRender/ClusteredLightManager.cpp b/src/Layers/xrRender/ClusteredLightManager.cpp index c71ea1e7229..60b8fd62d79 100644 --- a/src/Layers/xrRender/ClusteredLightManager.cpp +++ b/src/Layers/xrRender/ClusteredLightManager.cpp @@ -5,8 +5,11 @@ #include "Layers/xrRender/RenderContext/RenderDevice.h" #include "Layers/xrRender/ResourceManager/FGResourceManager.h" #include "Layers/xrRender/ResourceManager/TextureManager.h" +#include "Layers/xrRender/xrRender_console.h" #include "xrEngine/IRenderBackend.h" #include "xrCore/Threading/ParallelFor.hpp" +#include +#include namespace xray::render::fg { @@ -112,6 +115,9 @@ void ClusteredLightManager::Shutdown() m_statsScheduled = 0; m_visibleLightCountCPU = 0; m_lightsCPU.clear(); + m_lightSources.clear(); + m_localShadowTiles.clear(); + m_stickyShadowSlots = {}; m_spotTextureCache.clear(); m_device = nullptr; } @@ -119,12 +125,67 @@ void ClusteredLightManager::Shutdown() void ClusteredLightManager::BeginFrame() { m_lightsCPU.clear(); + m_lightSources.clear(); + m_localShadowTiles.clear(); m_numLights = 0; m_numPoint = 0; m_numSpot = 0; m_numOmni = 0; } +Fmatrix ClusteredLightManager::BuildSpotClipVP(const light* L) +{ + Fmatrix spotVP; + spotVP.identity(); + if (!L) + return spotVP; + + Fvector L_dir, L_up, L_right; + L_dir.set(L->direction); + float l_dir_m = L_dir.magnitude(); + if (_valid(l_dir_m) && l_dir_m > EPS_S) + L_dir.div(l_dir_m); + else + L_dir.set(0, 0, 1); + + if (L->right.square_magnitude() > EPS) + { + L_right.set(L->right); + L_right.normalize(); + L_up.crossproduct(L_dir, L_right); + L_up.normalize(); + L_right.crossproduct(L_up, L_dir); + L_right.normalize(); + } + else + { + L_up.set(0, 1, 0); + if (_abs(L_up.dotproduct(L_dir)) > .99f) + L_up.set(0, 0, 1); + L_right.crossproduct(L_up, L_dir); + L_right.normalize(); + L_up.crossproduct(L_dir, L_right); + L_up.normalize(); + } + + Fmatrix spotView; + spotView.build_camera_dir(L->position, L_dir, L_up); + + Fmatrix spotProj; + // Classic Light_Render_Direct_ComputeXFS: near=virtual_size, far=range, + // FOV pad +3.5° (spot) / +11.5° (point faces → OMNIPART). + const float nearPlane = std::max(L->virtual_size, 0.05f); + const float tan_shift = (L->flags.type == IRender_Light::OMNIPART) + ? deg2rad(11.5f) + : deg2rad(3.5f); + const float fov = std::min(L->cone + tan_shift, PI * 0.98f); + const float farPlane = std::max(L->range + EPS_S, nearPlane + 0.5f); + spotProj.build_projection(fov, 1.f, nearPlane, farPlane); + + spotVP.mul(spotProj, spotView); + return spotVP; +} + GPULightData ClusteredLightManager::BuildGPULightData(const light* L) { GPULightData gpu; @@ -155,47 +216,9 @@ GPULightData ClusteredLightManager::BuildGPULightData(const light* L) float texIdxBits; std::memcpy(&texIdxBits, &texIdx, sizeof(float)); + // .w = local shadow tile (asuint): 0 = none; set later by AssignLocalShadowTiles gpu.spotParamsAndType.set(offset, 1.0f, texIdxBits, 0.0f); - - if (texIdx != 0) - { - Fvector L_dir, L_up, L_right; - L_dir.set(L->direction); - float l_dir_m = L_dir.magnitude(); - if (_valid(l_dir_m) && l_dir_m > EPS_S) - L_dir.div(l_dir_m); - else - L_dir.set(0, 0, 1); - - if (L->right.square_magnitude() > EPS) - { - L_right.set(L->right); - L_right.normalize(); - L_up.crossproduct(L_dir, L_right); - L_up.normalize(); - L_right.crossproduct(L_up, L_dir); - L_right.normalize(); - } - else - { - L_up.set(0, 1, 0); - if (_abs(L_up.dotproduct(L_dir)) > .99f) - L_up.set(0, 0, 1); - L_right.crossproduct(L_up, L_dir); - L_right.normalize(); - L_up.crossproduct(L_dir, L_right); - L_up.normalize(); - } - - Fmatrix spotView; - spotView.build_camera_dir(L->position, L_dir, L_up); - - Fmatrix spotProj; - float nearPlane = std::max(L->virtual_size, 0.01f); - spotProj.build_projection(L->cone + deg2rad(3.5f), 1.f, nearPlane, range + EPS_S); - - gpu.spotVP.mul(spotProj, spotView); - } + gpu.spotVP = BuildSpotClipVP(L); } else { @@ -212,18 +235,45 @@ void ClusteredLightManager::CollectLight(const light* L) return; m_lightsCPU.push_back(BuildGPULightData(L)); + m_lightSources.push_back(L); m_numLights++; } void ClusteredLightManager::CollectLightsParallel(const xr_vector& lights) { - const u32 count = std::min(static_cast(lights.size()), MAX_LIGHTS); + if (lights.empty()) + return; + + // Expand shadowed POINT → 6 OMNIPART faces (classic Export path). + xr_vector expanded; + expanded.reserve(lights.size() + 32); + for (const light* L : lights) + { + if (!L) + continue; + if (L->flags.type == IRender_Light::POINT && L->flags.bShadow) + { + light* mutableL = const_cast(L); + mutableL->EnsureOmniparts(); + for (int f = 0; f < 6; ++f) + { + if (L->omnipart[f]) + expanded.push_back(L->omnipart[f]); + } + } + else + { + expanded.push_back(L); + } + } + + const u32 count = std::min(static_cast(expanded.size()), MAX_LIGHTS); if (count == 0) return; for (u32 i = 0; i < count; i++) { - const light* L = lights[i]; + const light* L = expanded[i]; const u32 lt = L->flags.type; const bool isSpot = (lt == IRender_Light::SPOT || lt == IRender_Light::OMNIPART); if (isSpot && !L->spot_texture_name.empty()) @@ -231,18 +281,22 @@ void ClusteredLightManager::CollectLightsParallel(const xr_vector& } m_lightsCPU.resize(count); + m_lightSources.resize(count); m_numLights = count; + for (u32 i = 0; i < count; ++i) + m_lightSources[i] = expanded[i]; + xr_parallel_for(TaskRange(0, count), [&](const TaskRange& range) { for (u32 i = range.begin(); i != range.end(); ++i) - m_lightsCPU[i] = BuildGPULightData(lights[i]); + m_lightsCPU[i] = BuildGPULightData(expanded[i]); }); if (psDeviceFlags.test(rsStatistic)) { for (u32 i = 0; i < count; i++) { - const u32 lt = lights[i]->flags.type; + const u32 lt = expanded[i]->flags.type; if (lt == IRender_Light::POINT) m_numPoint++; else if (lt == IRender_Light::SPOT) @@ -253,18 +307,168 @@ void ClusteredLightManager::CollectLightsParallel(const xr_vector& } } +void ClusteredLightManager::AssignLocalShadowTiles(const Fvector& cameraPos) +{ + m_localShadowTiles.clear(); + if (ps_r_local_shadows == 0 || m_numLights == 0 || m_lightSources.size() != m_numLights) + { + m_stickyShadowSlots = {}; + return; + } + + struct Candidate + { + u32 idx = 0; + float score = 0.f; + Fmatrix clipVP; + const light* L = nullptr; + }; + + // light* → candidate (first occurrence wins; OMNIPART faces are distinct lights) + xr_map byLight; + + for (u32 i = 0; i < m_numLights; ++i) + { + const light* L = m_lightSources[i]; + if (!L || !L->flags.bShadow) + continue; + const u32 lt = L->flags.type; + if (lt != IRender_Light::SPOT && lt != IRender_Light::OMNIPART) + continue; + + const float distSq = cameraPos.distance_to_sqr(L->position); + const float intensity = std::max({L->color.r, L->color.g, L->color.b, 0.01f}); + float score = intensity * L->range / (1.f + distSq); + // Static level lights (Light_DB) — room fixtures; keep them over the torch. + if (L->flags.bStatic) + score *= 2.5f; + // OMNIPART = POINT faces (Skadovsk cabin lamps). Slightly below spots but not starved. + if (lt == IRender_Light::OMNIPART) + score *= 0.85f; + // Downward faces → long floor shadows from beds/tables. + if (L->direction.y < -0.35f) + score *= 1.5f; + // Player torch: close dynamic SPOT — don't let it eat every atlas slot indoors. + if (lt == IRender_Light::SPOT && !L->flags.bStatic && distSq < 16.f) + score *= 0.35f; + + Candidate c; + c.idx = i; + c.score = score; + c.clipVP = m_lightsCPU[i].spotVP; + c.L = L; + byLight.emplace(L, c); + } + + if (byLight.empty()) + { + m_stickyShadowSlots = {}; + return; + } + + // Find admission floor from a fresh top-N ranking (for hysteresis). + xr_vector ranked; + ranked.reserve(byLight.size()); + for (auto& kv : byLight) + ranked.push_back(kv.second); + std::sort(ranked.begin(), ranked.end(), [](const Candidate& a, const Candidate& b) { + if (a.score != b.score) + return a.score > b.score; + return a.L < b.L; // stable tie-break + }); + + const u32 tileCap = std::clamp(static_cast(std::max(ps_r_local_shadow_tiles, 1)), 1u, MAX_LOCAL_SHADOW_TILES); + const u32 nWant = std::min(static_cast(ranked.size()), tileCap); + const float admitScore = (nWant > 0) ? ranked[nWant - 1].score : 0.f; + // Wide hysteresis: score flickers every frame as you walk — 0.7 was still thrashing + // (whole room shadows popping on/off right in front of the camera). + const float keepScore = admitScore * 0.35f; + constexpr u32 kStickyGraceFrames = 45; + + std::array nextSticky{}; + xr_vector sliceTaken(MAX_LOCAL_SHADOW_TILES, false); + xr_map assigned; // light* → slice + + auto packTile = [&](u32 lightIdx, u32 slice, const light* L, const Fmatrix& clipVP, float score, u32 grace) { + u32 tilePlusOne = slice + 1; + float tileBits; + std::memcpy(&tileBits, &tilePlusOne, sizeof(float)); + m_lightsCPU[lightIdx].spotParamsAndType.w = tileBits; + + LocalShadowTile tile; + tile.L = L; + tile.clipVP = clipVP; + tile.lightIndex = lightIdx; + tile.slice = slice; + m_localShadowTiles.push_back(tile); + + nextSticky[slice] = {L, score, grace}; + sliceTaken[slice] = true; + assigned[L] = slice; + }; + + // 1) Retain previous sticky occupants (score hysteresis + grace frames). + for (u32 s = 0; s < tileCap; ++s) + { + const StickyShadowSlot& prev = m_stickyShadowSlots[s]; + if (!prev.L) + continue; + auto it = byLight.find(prev.L); + if (it == byLight.end()) + { + // Light briefly left the clustered list — reserve the slice so it isn't + // stolen; resume packing when the light returns (kills on/off flicker). + if (prev.graceFrames == 0) + continue; + nextSticky[s] = {prev.L, prev.score, prev.graceFrames - 1}; + sliceTaken[s] = true; + continue; + } + const bool eligible = it->second.score >= keepScore; + if (!eligible && prev.graceFrames == 0) + continue; + const u32 grace = eligible ? kStickyGraceFrames + : (prev.graceFrames > 0 ? prev.graceFrames - 1 : 0); + packTile(it->second.idx, s, prev.L, it->second.clipVP, it->second.score, grace); + } + + // 2) Fill free slices from ranked list (skip already assigned). + for (const Candidate& c : ranked) + { + if (assigned.find(c.L) != assigned.end()) + continue; + u32 freeSlice = tileCap; + for (u32 s = 0; s < tileCap; ++s) + { + if (!sliceTaken[s]) + { + freeSlice = s; + break; + } + } + if (freeSlice >= tileCap) + break; + packTile(c.idx, freeSlice, c.L, c.clipVP, c.score, kStickyGraceFrames); + } + + m_stickyShadowSlots = nextSticky; +} + void ClusteredLightManager::AddLight(const light* L, u32 type) { if (m_numLights >= MAX_LIGHTS) return; m_lightsCPU.push_back(BuildGPULightData(L)); + m_lightSources.push_back(L); m_numLights++; } void ClusteredLightManager::BuildLightBuffer(const light_Package& package) { m_lightsCPU.clear(); + m_lightSources.clear(); + m_localShadowTiles.clear(); m_numLights = 0; for (const light* L : package.v_point) diff --git a/src/Layers/xrRender/ClusteredLightManager.h b/src/Layers/xrRender/ClusteredLightManager.h index 39758b7f1cc..2ebafbaf398 100644 --- a/src/Layers/xrRender/ClusteredLightManager.h +++ b/src/Layers/xrRender/ClusteredLightManager.h @@ -39,10 +39,19 @@ struct alignas(16) LightHiZCullCB { }; static_assert(sizeof(LightHiZCullCB) == 160, "LightHiZCullCB must be 160 bytes"); +/// One atlas slice for a shadowed spot / OMNIPART face. +struct LocalShadowTile { + const light* L = nullptr; + Fmatrix clipVP; + u32 lightIndex = 0; // index into GPU light buffer + u32 slice = 0; +}; + static constexpr u32 CLUSTER_TILE_SIZE = 64; static constexpr u32 CLUSTER_NUM_SLICES = 24; static constexpr u32 MAX_LIGHTS = 1024; static constexpr u32 MAX_LIGHT_INDICES = 1024 * 1024; +static constexpr u32 MAX_LOCAL_SHADOW_TILES = 16; class ClusteredLightManager { public: @@ -54,6 +63,8 @@ class ClusteredLightManager { void CollectLight(const light* L); void CollectLightsParallel(const xr_vector& lights); void BuildLightBuffer(const light_Package& package); + /// Pick top-N shadowed faces and pack tile index into GPULightData.w (call after collect, before Upload). + void AssignLocalShadowTiles(const Fvector& cameraPos); void Upload(nvrhi::ICommandList* cmdList); void UploadAllVisible(nvrhi::ICommandList* cmdList); @@ -71,6 +82,8 @@ class ClusteredLightManager { u32 GetTilesX() const { return m_tilesX; } u32 GetTilesY() const { return m_tilesY; } + const xr_vector& GetLocalShadowTiles() const { return m_localShadowTiles; } + ClusterCB BuildClusterCB(u32 screenWidth, u32 screenHeight, float zNear, float zFar) const; void ScheduleStatsReadback(nvrhi::ICommandList* cmdList); @@ -79,6 +92,8 @@ class ClusteredLightManager { bool IsReady() const { return m_lightDataBuffer != nullptr; } + static Fmatrix BuildSpotClipVP(const light* L); + private: void AddLight(const light* L, u32 type); GPULightData BuildGPULightData(const light* L); @@ -87,6 +102,16 @@ class ClusteredLightManager { nvrhi::DeviceHandle m_device; xr_vector m_lightsCPU; + xr_vector m_lightSources; + xr_vector m_localShadowTiles; + /// Persist light* → slice across frames to stop top-N thrashing (Skadovsk flicker). + struct StickyShadowSlot + { + const light* L = nullptr; + float score = 0.f; + u32 graceFrames = 0; // keep slot after brief score/list drops + }; + std::array m_stickyShadowSlots{}; std::array m_identityIndices; xr_map m_spotTextureCache; u32 m_numLights = 0; diff --git a/src/Layers/xrRender/FGDetailManager.cpp b/src/Layers/xrRender/FGDetailManager.cpp index f8da53338f8..3a0c4e5a205 100644 --- a/src/Layers/xrRender/FGDetailManager.cpp +++ b/src/Layers/xrRender/FGDetailManager.cpp @@ -183,6 +183,9 @@ void FGDetailManager::Unload() slot_aabbs.clear(); slotDataCPU.clear(); + heightmapCPU.clear(); + heightmapTexture = nullptr; + heightmapWidth = heightmapHeight = 0; if (dtFS) { @@ -542,6 +545,62 @@ bool FGDetailManager::LoadHeightmapTexture(nvrhi::IDevice* device) Msg(" ✓ Heightmap texture loaded: %ux%u R32_FLOAT, %u mips", texDesc.width, texDesc.height, texDesc.mipLevels); + // Keep mip0 on CPU for rain splash RayPick / terrain collide + { + const auto& mip0 = ddsData.mipLevels[0]; + const u32 count = heightmapWidth * heightmapHeight; + heightmapCPU.assign(count, HEIGHTMAP_NO_TERRAIN); + if (mip0.data && mip0.size >= count * sizeof(float)) + { + const float* src = reinterpret_cast(mip0.data); + // rowPitch may include padding + const u32 srcStride = mip0.rowPitch / sizeof(float); + for (u32 y = 0; y < heightmapHeight; ++y) + { + const float* row = src + y * srcStride; + float* dst = heightmapCPU.data() + y * heightmapWidth; + memcpy(dst, row, heightmapWidth * sizeof(float)); + } + } + Msg("* [FGDetailManager] Heightmap CPU copy: %u samples", (u32)heightmapCPU.size()); + } + + return true; +} + +bool FGDetailManager::SampleHeight(float worldX, float worldZ, float& outY) const +{ + if (heightmapCPU.empty() || heightmapWidth == 0 || heightmapHeight == 0 || + heightmapTexelSize <= 0.f) + return false; + + const float fx = (worldX - heightmapWorldMinX) / heightmapTexelSize - 0.5f; + const float fz = (worldZ - heightmapWorldMinZ) / heightmapTexelSize - 0.5f; + if (fx < 0.f || fz < 0.f || fx >= float(heightmapWidth - 1) || fz >= float(heightmapHeight - 1)) + return false; + + const u32 x0 = u32(fx); + const u32 z0 = u32(fz); + const u32 x1 = x0 + 1; + const u32 z1 = z0 + 1; + const float tx = fx - float(x0); + const float tz = fz - float(z0); + + const auto at = [&](u32 x, u32 z) -> float { + return heightmapCPU[z * heightmapWidth + x]; + }; + + const float h00 = at(x0, z0); + const float h10 = at(x1, z0); + const float h01 = at(x0, z1); + const float h11 = at(x1, z1); + if (h00 <= HEIGHTMAP_NO_TERRAIN * 0.5f || h10 <= HEIGHTMAP_NO_TERRAIN * 0.5f || + h01 <= HEIGHTMAP_NO_TERRAIN * 0.5f || h11 <= HEIGHTMAP_NO_TERRAIN * 0.5f) + return false; + + const float h0 = h00 + (h10 - h00) * tx; + const float h1 = h01 + (h11 - h01) * tx; + outY = h0 + (h1 - h0) * tz; return true; } @@ -633,6 +692,48 @@ bool FGDetailManager::LoadBuildDetailsTexture(nvrhi::IDevice* device) } } } + else + { + Msg("! [FGDetailManager] build_details_pbr.dds missing — CoP grass uses default roughness/AO (SSS still on)"); + } + + // Procedural blades (r__detail_gpu 1): vein detail map + resources::DDSData veinData; + if (resources::DDSLoader::LoadFromFile("shaders\\grass_vein", veinData) && + veinData.isValid && !veinData.mipLevels.empty()) + { + nvrhi::TextureDesc veinDesc; + veinDesc.width = veinData.desc.width; + veinDesc.height = veinData.desc.height; + veinDesc.depth = 1; + veinDesc.arraySize = 1; + veinDesc.mipLevels = veinData.desc.mipLevels; + veinDesc.format = veinData.desc.format; + veinDesc.dimension = nvrhi::TextureDimension::Texture2D; + veinDesc.initialState = nvrhi::ResourceStates::ShaderResource; + veinDesc.keepInitialState = true; + veinDesc.debugName = "GrassVein"; + + grassVeinTexture = device->createTexture(veinDesc); + if (grassVeinTexture) + { + for (u32 mip = 0; mip < veinData.mipLevels.size(); mip++) + { + const auto& ml = veinData.mipLevels[mip]; + renderDevice->UploadTextureDataToNVRHI(grassVeinTexture, 0, mip, ml.data, ml.size, ml.rowPitch, 0); + } + if (GEnv.Backend) + { + grassVeinBindlessIndex = GEnv.Backend->RegisterBindlessTexture(grassVeinTexture); + Msg("* [FGDetailManager] shaders/grass_vein.dds loaded: %ux%u, bindless=%u", + veinDesc.width, veinDesc.height, grassVeinBindlessIndex); + } + } + } + else + { + Msg("! [FGDetailManager] shaders/grass_vein.dds not found — procedural blades lose vein detail"); + } return true; } @@ -1370,6 +1471,11 @@ void FGDetailManager::DestroyGPUBuffers() pulledVertexBuffer = nullptr; pulledIndexBuffer = nullptr; buildDetailsTexture = nullptr; + buildDetailsPbrTexture = nullptr; + grassVeinTexture = nullptr; + buildDetailsBindlessIndex = 0; + buildDetailsPbrBindlessIndex = 0; + grassVeinBindlessIndex = 0; perlin4dTexture = nullptr; perlin4dComputeShader = nullptr; @@ -2035,7 +2141,7 @@ bool FGDetailManager::CreateGraphicsPipeline(fg::RenderDevice* renderDevice, con return false; } - graphicsBindingLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("DetailGPU", *vsRefl, *psRefl, device); + graphicsBindingLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("DetailGPU_CSMLadder", *vsRefl, *psRefl, device); if (!graphicsBindingLayout) { Msg("! [FGDetailManager] Failed to create graphics binding layout"); @@ -2050,7 +2156,7 @@ bool FGDetailManager::CreateGraphicsPipeline(fg::RenderDevice* renderDevice, con return false; } - decalBindingLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("DetailDecal", *decalVsRefl, *decalPsRefl, device); + decalBindingLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("DetailDecal_CSMLadder", *decalVsRefl, *decalPsRefl, device); if (!decalBindingLayout) { Msg("! [FGDetailManager] Failed to create decal binding layout"); @@ -2140,7 +2246,7 @@ bool FGDetailManager::CreateGraphicsPipeline(fg::RenderDevice* renderDevice, con auto* bbVsRefl = shaderLoader->GetCachedReflection("detail_billboard", ".vs"); auto* bbPsRefl = shaderLoader->GetCachedReflection("detail_billboard", ".ps"); if (bbVsRefl && bbPsRefl) - billboardBindingLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("DetailBillboard", *bbVsRefl, *bbPsRefl, device); + billboardBindingLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("DetailBillboard_CSMLadder", *bbVsRefl, *bbPsRefl, device); if (!billboardBindingLayout) billboardBindingLayout = decalBindingLayout; diff --git a/src/Layers/xrRender/FGDetailManager.h b/src/Layers/xrRender/FGDetailManager.h index 376e8a990cb..c4428a01fc5 100644 --- a/src/Layers/xrRender/FGDetailManager.h +++ b/src/Layers/xrRender/FGDetailManager.h @@ -86,6 +86,8 @@ class FGDetailManager float grass_blade_height; u32 buildDetailsIndex; u32 buildDetailsPbrIndex; + u32 grassVeinIndex; + u32 pad0, pad1, pad2; }; struct DetailCullParams @@ -193,6 +195,8 @@ class FGDetailManager u32 buildDetailsBindlessIndex = 0; nvrhi::TextureHandle buildDetailsPbrTexture; u32 buildDetailsPbrBindlessIndex = 0; + nvrhi::TextureHandle grassVeinTexture; + u32 grassVeinBindlessIndex = 0; nvrhi::BufferHandle visibleSlotIDsBuffer; nvrhi::BufferHandle visibleSlotCounterBuffer; @@ -280,6 +284,9 @@ class FGDetailManager nvrhi::TextureHandle heightmapTexture; + // CPU mip0 copy for rain RayPick / gameplay queries + xr_vector heightmapCPU; + float m_lastDensity = -1.0f; bool m_instancesNeedRegeneration = true; @@ -307,6 +314,13 @@ class FGDetailManager void Unload(); bool BakeHeightmap(); bool LoadHeightmapTexture(nvrhi::IDevice* device); + bool SampleHeight(float worldX, float worldZ, float& outY) const; + + nvrhi::ITexture* GetHeightmapTexture() const { return heightmapTexture; } + float GetHeightmapWorldMinX() const { return heightmapWorldMinX; } + float GetHeightmapWorldMinZ() const { return heightmapWorldMinZ; } + float GetHeightmapTexelSize() const { return heightmapTexelSize; } + bool HasHeightmapGPU() const { return heightmapTexture != nullptr && heightmapTexelSize > 0.f; } bool LoadBuildDetailsTexture(nvrhi::IDevice* device); void PackSlotData(); bool CreateGPUBuffers(nvrhi::IDevice* device); diff --git a/src/Layers/xrRender/FGRenderBase.h b/src/Layers/xrRender/FGRenderBase.h index 9a6555867cb..5a75542ace2 100644 --- a/src/Layers/xrRender/FGRenderBase.h +++ b/src/Layers/xrRender/FGRenderBase.h @@ -46,7 +46,8 @@ class FGRenderBase : public IRender, public pureFrame void ResourcesStoreNecessaryTextures() override; void ResourcesDumpMemoryUsage() override; - bool HWSupportsShaderYUV2RGB() override { return false; } + // Theora packs V/U/Y into RGBA8; hud\movie.ps converts to RGB on GPU + bool HWSupportsShaderYUV2RGB() override { return true; } DeviceState GetDeviceState() override; bool GetForceGPU_REF() override; diff --git a/src/Layers/xrRender/FrameGraph/BindingSetBuilder.cpp b/src/Layers/xrRender/FrameGraph/BindingSetBuilder.cpp index e106c81c476..583af39e883 100644 --- a/src/Layers/xrRender/FrameGraph/BindingSetBuilder.cpp +++ b/src/Layers/xrRender/FrameGraph/BindingSetBuilder.cpp @@ -149,9 +149,19 @@ int BindingSetBuilder::FindCBSlot(const char* name) const BindingSetBuilder& BindingSetBuilder::Texture(const char* name, nvrhi::ITexture* texture, nvrhi::Format format, nvrhi::TextureSubresourceSet subresources) { - int slot = FindSRVSlot(name); - if (slot >= 0) - m_desc.bindings.push_back(nvrhi::BindingSetItem::Texture_SRV(slot, texture, format, subresources)); + // Bind every matching slot — VS+PS can declare the same name at different + // auto-assigned registers; binding only the first leaves layout holes (t27/t34). + bool found = false; + for (const auto& r : m_lists->srvs) + { + if (!NameMatches(r.name, name)) + continue; + m_desc.bindings.push_back(nvrhi::BindingSetItem::Texture_SRV( + r.slot, texture, format, subresources)); + found = true; + } + if (!found) + FindSRVSlot(name); // logs missing return *this; } diff --git a/src/Layers/xrRender/FrameGraph/FrameGraph.cpp b/src/Layers/xrRender/FrameGraph/FrameGraph.cpp index c265300e5ad..e9ce9f245e3 100644 --- a/src/Layers/xrRender/FrameGraph/FrameGraph.cpp +++ b/src/Layers/xrRender/FrameGraph/FrameGraph.cpp @@ -1177,6 +1177,21 @@ void FrameGraph::OptimizeMemoryAliasing() { compatible = false; } + // Exact dimensions required — aliasing a half-res bloom mip onto a + // full-res RT makes DrawFS write into the top-left corner only + // (viewport = logical size) while UV sampling covers the whole + // oversized texture → black frame + nested thumbnails. + if (current->desc.type != ResourceDesc::Type::Buffer) { + if (current->desc.width != candidate->desc.width || + current->desc.height != candidate->desc.height || + current->desc.depth != candidate->desc.depth || + current->desc.arraySize != candidate->desc.arraySize || + current->desc.mipLevels != candidate->desc.mipLevels || + current->desc.sampleCount != candidate->desc.sampleCount) { + compatible = false; + } + } + // Candidate must be large enough if (candidate->memorySize < current->memorySize) { compatible = false; diff --git a/src/Layers/xrRender/FrameGraph/PassResourceCache.cpp b/src/Layers/xrRender/FrameGraph/PassResourceCache.cpp index f3b311a3997..6c13c1a1680 100644 --- a/src/Layers/xrRender/FrameGraph/PassResourceCache.cpp +++ b/src/Layers/xrRender/FrameGraph/PassResourceCache.cpp @@ -4,6 +4,8 @@ #include "PassResourceCache.h" #include "BindingLayoutBuilder.h" #include "ShaderCache.h" +#include "Layers/xrRender/r2_types.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" namespace xray::render::framegraph { @@ -131,12 +133,13 @@ nvrhi::ITexture* PassResourceCache::GetDummyShadowMap(nvrhi::IDevice* device) { nvrhi::TextureDesc desc; desc.width = 1; desc.height = 1; - desc.arraySize = 3; + desc.arraySize = 4; desc.format = nvrhi::Format::D32; desc.debugName = "DummyShadowMap"; desc.initialState = nvrhi::ResourceStates::ShaderResource; desc.keepInitialState = true; desc.dimension = nvrhi::TextureDimension::Texture2DArray; + desc.isShaderResource = true; m_dummyShadowMap = device->createTexture(desc); } return m_dummyShadowMap; @@ -152,11 +155,79 @@ nvrhi::ITexture* PassResourceCache::GetDummyShadowMap2D(nvrhi::IDevice* device) desc.initialState = nvrhi::ResourceStates::ShaderResource; desc.keepInitialState = true; desc.dimension = nvrhi::TextureDimension::Texture2D; + desc.isShaderResource = true; m_dummyShadowMap2D = device->createTexture(desc); } return m_dummyShadowMap2D; } +nvrhi::ITexture* PassResourceCache::GetDummyContactDepth(nvrhi::IDevice* device) { + if (!m_dummyContactDepth && device) { + nvrhi::TextureDesc desc; + desc.width = 1; + desc.height = 1; + desc.format = nvrhi::Format::R32_FLOAT; + desc.debugName = "DummyContactDepth"; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + desc.dimension = nvrhi::TextureDimension::Texture2D; + desc.isShaderResource = true; + m_dummyContactDepth = device->createTexture(desc); + if (m_dummyContactDepth) { + // Far depth — ContactShadow thickness test never hits + nvrhi::CommandListHandle cmd = device->createCommandList(); + cmd->open(); + float farD = 1.0f; + cmd->writeTexture(m_dummyContactDepth, 0, 0, &farD, sizeof(farD)); + cmd->close(); + device->executeCommandList(cmd); + } + } + return m_dummyContactDepth; +} + +nvrhi::ITexture* PassResourceCache::GetDummyContactHistory(nvrhi::IDevice* device) { + if (!m_dummyContactHistory && device) { + nvrhi::TextureDesc desc; + desc.width = 1; + desc.height = 1; + desc.format = nvrhi::Format::RGBA16_FLOAT; + desc.debugName = "DummyContactHistory"; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + desc.dimension = nvrhi::TextureDimension::Texture2D; + desc.isShaderResource = true; + m_dummyContactHistory = device->createTexture(desc); + if (m_dummyContactHistory) { + nvrhi::CommandListHandle cmd = device->createCommandList(); + cmd->open(); + // 1 = fully lit + float white[4] = {1.f, 0.f, 0.f, 0.f}; + cmd->writeTexture(m_dummyContactHistory, 0, 0, white, sizeof(white)); + cmd->close(); + device->executeCommandList(cmd); + } + } + return m_dummyContactHistory; +} + +nvrhi::ITexture* PassResourceCache::GetDummyCubeMap(nvrhi::IDevice* device) { + if (!m_dummyCubeMap) { + nvrhi::TextureDesc desc; + desc.width = 1; + desc.height = 1; + desc.format = nvrhi::Format::RGBA8_UNORM; + desc.dimension = nvrhi::TextureDimension::TextureCube; + desc.arraySize = 6; + desc.debugName = "DummyCubeMap"; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + desc.isShaderResource = true; + m_dummyCubeMap = device->createTexture(desc); + } + return m_dummyCubeMap; +} + nvrhi::ISampler* PassResourceCache::GetSamplerByName(const char* smpName, nvrhi::IDevice* device) { if (strstr(smpName, "smp_nofilter") || strstr(smpName, "smp_smap") || strstr(smpName, "smp_jitter")) @@ -165,8 +236,10 @@ nvrhi::ISampler* PassResourceCache::GetSamplerByName(const char* smpName, nvrhi: return GetLinearClampSampler(device); if (strstr(smpName, "smp_base") || strstr(smpName, "smp_material") || strstr(smpName, "smp_bump")) return GetAnisoWrapSampler(device); - if (strstr(smpName, "smp_shadowcmp")) + if (strstr(smpName, "smp_shadowcmp") || strstr(smpName, "smp_raincmp")) return GetShadowCmpSampler(device); + if (strstr(smpName, "smp_shadow_point") || strstr(smpName, "smp_shadowpoint")) + return GetPointClampSampler(device); return GetLinearWrapSampler(device); } @@ -488,6 +561,9 @@ void PassResourceCache::Clear() { m_commonShadowCmp = nullptr; m_dummyShadowMap = nullptr; m_dummyShadowMap2D = nullptr; + m_dummyContactDepth = nullptr; + m_dummyContactHistory = nullptr; + m_dummyCubeMap = nullptr; Msg("* [PassResourceCache] Cleared all caches"); } diff --git a/src/Layers/xrRender/FrameGraph/PassResourceCache.h b/src/Layers/xrRender/FrameGraph/PassResourceCache.h index cb2d72e4248..de1c458508e 100644 --- a/src/Layers/xrRender/FrameGraph/PassResourceCache.h +++ b/src/Layers/xrRender/FrameGraph/PassResourceCache.h @@ -51,6 +51,11 @@ class PassResourceCache { nvrhi::ISampler* GetShadowCmpSampler(nvrhi::IDevice* device); nvrhi::ITexture* GetDummyShadowMap(nvrhi::IDevice* device); nvrhi::ITexture* GetDummyShadowMap2D(nvrhi::IDevice* device); + /// 1x1 far depth (1.0) for g_ContactDepth @ t27 when prev-frame depth is unavailable + nvrhi::ITexture* GetDummyContactDepth(nvrhi::IDevice* device); + /// 1x1 white (1.0 = lit) for g_ContactHistory @ t28 + nvrhi::ITexture* GetDummyContactHistory(nvrhi::IDevice* device); + nvrhi::ITexture* GetDummyCubeMap(nvrhi::IDevice* device); nvrhi::ISampler* GetSamplerByName(const char* smpName, nvrhi::IDevice* device); @@ -197,6 +202,9 @@ class PassResourceCache { nvrhi::SamplerHandle m_commonShadowCmp; nvrhi::TextureHandle m_dummyShadowMap; nvrhi::TextureHandle m_dummyShadowMap2D; + nvrhi::TextureHandle m_dummyContactDepth; + nvrhi::TextureHandle m_dummyContactHistory; + nvrhi::TextureHandle m_dummyCubeMap; Stats m_stats; diff --git a/src/Layers/xrRender/FrameGraph/ShaderCache.h b/src/Layers/xrRender/FrameGraph/ShaderCache.h index e3e3284a808..9e6fb109cb9 100644 --- a/src/Layers/xrRender/FrameGraph/ShaderCache.h +++ b/src/Layers/xrRender/FrameGraph/ShaderCache.h @@ -32,7 +32,8 @@ struct ExtractedReflection /// Disk cache for compiled shader bytecode + reflection metadata /// Cache format: shaders_cache_fg/./ /// -/// Binary format (CACHE_VERSION = 2): +/// Binary format (CACHE_VERSION = 15): +/// terrain hemi+lmap, water SSR/soft, wet v2, grass CSM cast /// [4 bytes] Cache version /// [4 bytes] Source hash (CRC32 of shader source + macros) /// [4 bytes] Bytecode size @@ -131,7 +132,7 @@ class ShaderCache ExtractedReflection& outReflection ); - static constexpr u32 CACHE_VERSION = 6; + static constexpr u32 CACHE_VERSION = 168; // grass SSS + vein CB + CoP billboard glow Stats m_stats; bool m_cacheEnabled; xr_string m_backendSubdir; diff --git a/src/Layers/xrRender/FrameGraph/ShaderLoader.cpp b/src/Layers/xrRender/FrameGraph/ShaderLoader.cpp index 69f06db0945..4a3987fbe61 100644 --- a/src/Layers/xrRender/FrameGraph/ShaderLoader.cpp +++ b/src/Layers/xrRender/FrameGraph/ShaderLoader.cpp @@ -29,6 +29,12 @@ bool TryGetNvrhiShaderType(xray::render::SlangCompiler::Stage stage, nvrhi::Shad case xray::render::SlangCompiler::Stage::Mesh: outType = nvrhi::ShaderType::Mesh; return true; + case xray::render::SlangCompiler::Stage::Hull: + outType = nvrhi::ShaderType::Hull; + return true; + case xray::render::SlangCompiler::Stage::Domain: + outType = nvrhi::ShaderType::Domain; + return true; default: return false; } @@ -50,6 +56,13 @@ void ResolveShaderSourceRelativePath( shName[size] = 0; } + // Engine UI/legacy names may use either separator; VFS catalog keys use '\'. + for (char* p = shName; *p; ++p) + { + if (*p == '/') + *p = '\\'; + } + // Only remove skinning suffix (_0, _1, _2, _3, _4) for vertex shaders // Skinning only affects vertex processing, not pixel shaders // This avoids incorrectly stripping suffixes from shaders like "bloom_luminance_1" @@ -471,6 +484,146 @@ ShaderLoader::ShaderResult ShaderLoader::LoadPixelShader( return result; } +ShaderLoader::ShaderResult ShaderLoader::LoadHullShader(const char* name, const char* entryPoint) +{ + ShaderResult result; + xr_string cacheKey = xr_string(name) + ".hs"; + auto handleIt = m_handleCache.find(cacheKey); + if (handleIt != m_handleCache.end()) { + result.handle = handleIt->second; + auto reflIt = m_reflectionCache.find(cacheKey); + if (reflIt != m_reflectionCache.end()) + result.reflection = xr_new(*reflIt->second); + return result; + } + + IReader* fs = OpenShaderFile(name, ".hs"); + if (!fs) + return result; + WatchShaderFile(cacheKey, name, ".hs", entryPoint, xray::render::SlangCompiler::Stage::Hull); + + u32 sourceHash = ShaderCache::ComputeHash((const char*)fs->pointer(), fs->length()); + ExtractedReflection deserializedReflection; + bool cacheHit = m_cache.TryLoad(name, ".hs", sourceHash, result.bytecode, &deserializedReflection); + if (cacheHit) + { + nvrhi::ShaderDesc desc; + desc.shaderType = nvrhi::ShaderType::Hull; + desc.debugName = name; + result.handle = GEnv.Render->GetRenderDevice()->GetNVRHIDevice()->createShader( + desc, result.bytecode.data(), result.bytecode.size()); + fs->close(); + if (!result.handle) + return result; + result.reflection = xr_new(deserializedReflection); + m_handleCache[cacheKey] = result.handle; + m_reflectionCache[cacheKey] = xr_new(deserializedReflection); + return result; + } + + xr_string sourceCode; + sourceCode.assign((const char*)fs->pointer(), fs->length()); + string_path fullPath; + strconcat(sizeof(fullPath), fullPath, GEnv.Render->getShaderPath(), name, ".hs"); + auto compileResult = m_slangCompiler->CompileFromSource( + sourceCode.c_str(), entryPoint, xray::render::SlangCompiler::Stage::Hull, m_target, fullPath); + fs->close(); + if (!compileResult.IsValid()) + { + Msg("! [ShaderLoader] Compilation failed for %s.hs", name); + if (!compileResult.errorMessage.empty()) + Msg("! Error: %s", compileResult.errorMessage.c_str()); + return result; + } + + nvrhi::ShaderDesc desc; + desc.shaderType = nvrhi::ShaderType::Hull; + desc.debugName = name; + result.handle = GEnv.Render->GetRenderDevice()->GetNVRHIDevice()->createShader( + desc, compileResult.bytecode.data(), compileResult.bytecode.size()); + if (!result.handle) + return result; + + result.bytecode = std::move(compileResult.bytecode); + auto extractedReflection = ShaderReflector::ExtractReflection( + compileResult.reflection, compileResult.linkedProgram, compileResult.vkShifts); + result.reflection = xr_new(extractedReflection); + m_cache.Save(name, ".hs", sourceHash, result.bytecode, &extractedReflection); + m_handleCache[cacheKey] = result.handle; + m_reflectionCache[cacheKey] = xr_new(extractedReflection); + return result; +} + +ShaderLoader::ShaderResult ShaderLoader::LoadDomainShader(const char* name, const char* entryPoint) +{ + ShaderResult result; + xr_string cacheKey = xr_string(name) + ".ds"; + auto handleIt = m_handleCache.find(cacheKey); + if (handleIt != m_handleCache.end()) { + result.handle = handleIt->second; + auto reflIt = m_reflectionCache.find(cacheKey); + if (reflIt != m_reflectionCache.end()) + result.reflection = xr_new(*reflIt->second); + return result; + } + + IReader* fs = OpenShaderFile(name, ".ds"); + if (!fs) + return result; + WatchShaderFile(cacheKey, name, ".ds", entryPoint, xray::render::SlangCompiler::Stage::Domain); + + u32 sourceHash = ShaderCache::ComputeHash((const char*)fs->pointer(), fs->length()); + ExtractedReflection deserializedReflection; + bool cacheHit = m_cache.TryLoad(name, ".ds", sourceHash, result.bytecode, &deserializedReflection); + if (cacheHit) + { + nvrhi::ShaderDesc desc; + desc.shaderType = nvrhi::ShaderType::Domain; + desc.debugName = name; + result.handle = GEnv.Render->GetRenderDevice()->GetNVRHIDevice()->createShader( + desc, result.bytecode.data(), result.bytecode.size()); + fs->close(); + if (!result.handle) + return result; + result.reflection = xr_new(deserializedReflection); + m_handleCache[cacheKey] = result.handle; + m_reflectionCache[cacheKey] = xr_new(deserializedReflection); + return result; + } + + xr_string sourceCode; + sourceCode.assign((const char*)fs->pointer(), fs->length()); + string_path fullPath; + strconcat(sizeof(fullPath), fullPath, GEnv.Render->getShaderPath(), name, ".ds"); + auto compileResult = m_slangCompiler->CompileFromSource( + sourceCode.c_str(), entryPoint, xray::render::SlangCompiler::Stage::Domain, m_target, fullPath); + fs->close(); + if (!compileResult.IsValid()) + { + Msg("! [ShaderLoader] Compilation failed for %s.ds", name); + if (!compileResult.errorMessage.empty()) + Msg("! Error: %s", compileResult.errorMessage.c_str()); + return result; + } + + nvrhi::ShaderDesc desc; + desc.shaderType = nvrhi::ShaderType::Domain; + desc.debugName = name; + result.handle = GEnv.Render->GetRenderDevice()->GetNVRHIDevice()->createShader( + desc, compileResult.bytecode.data(), compileResult.bytecode.size()); + if (!result.handle) + return result; + + result.bytecode = std::move(compileResult.bytecode); + auto extractedReflection = ShaderReflector::ExtractReflection( + compileResult.reflection, compileResult.linkedProgram, compileResult.vkShifts); + result.reflection = xr_new(extractedReflection); + m_cache.Save(name, ".ds", sourceHash, result.bytecode, &extractedReflection); + m_handleCache[cacheKey] = result.handle; + m_reflectionCache[cacheKey] = xr_new(extractedReflection); + return result; +} + // ══════════════════════════════════════════════════════════ // LOAD COMPUTE SHADER // ══════════════════════════════════════════════════════════ diff --git a/src/Layers/xrRender/FrameGraph/ShaderLoader.h b/src/Layers/xrRender/FrameGraph/ShaderLoader.h index fb9e202c962..3c49c7ee129 100644 --- a/src/Layers/xrRender/FrameGraph/ShaderLoader.h +++ b/src/Layers/xrRender/FrameGraph/ShaderLoader.h @@ -99,6 +99,22 @@ class ShaderLoader { const char* entryPoint = "main" ); + ///

    + /// Load and compile hull (tessellation control) shader + /// + ShaderResult LoadHullShader( + const char* name, + const char* entryPoint = "main" + ); + + /// + /// Load and compile domain (tessellation evaluation) shader + /// + ShaderResult LoadDomainShader( + const char* name, + const char* entryPoint = "main" + ); + /// /// Load and compile compute shader from res/gamedata/shaders/r5/ /// Always returns shader with reflection data diff --git a/src/Layers/xrRender/FrameGraph/ShaderReflection.cpp b/src/Layers/xrRender/FrameGraph/ShaderReflection.cpp index 04fc0a6627e..7844eda7273 100644 --- a/src/Layers/xrRender/FrameGraph/ShaderReflection.cpp +++ b/src/Layers/xrRender/FrameGraph/ShaderReflection.cpp @@ -1065,6 +1065,7 @@ static void FilterReflectionByUsage(ExtractedReflection& result, slang::ICompone return used; }; + // Slang entry-point metadata can mark resources sampled only from helpers as unused. Msg(" [FilterReflection] Before: %u SRVs, %u UAVs, %u samplers, %u CBs", result.rtBindings.inputTextures.size(), result.rtBindings.uavBindings.size(), result.rtBindings.samplers.size(), result.constantLayout.constantBuffers.buffers.size()); diff --git a/src/Layers/xrRender/FrameGraph/ShaderReflection.h b/src/Layers/xrRender/FrameGraph/ShaderReflection.h index 60366a85884..b929339dea1 100644 --- a/src/Layers/xrRender/FrameGraph/ShaderReflection.h +++ b/src/Layers/xrRender/FrameGraph/ShaderReflection.h @@ -162,7 +162,8 @@ struct ShaderRTBindings { state.mipFilter = FilterMode::Point; state.maxAnisotropy = 1; } - else if (strstr(name.c_str(), "smp_shadowcmp") != nullptr) { + else if (strstr(name.c_str(), "smp_shadowcmp") != nullptr || + strstr(name.c_str(), "smp_raincmp") != nullptr) { state.addressMode = AddressMode::Clamp; state.minFilter = FilterMode::Linear; state.magFilter = FilterMode::Linear; diff --git a/src/Layers/xrRender/FrameGraphPasses/AmbientOcclusionPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/AmbientOcclusionPassSetup.cpp new file mode 100644 index 00000000000..b8d9207ff7f --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/AmbientOcclusionPassSetup.cpp @@ -0,0 +1,486 @@ +#include "stdafx.h" +#include "AmbientOcclusionPassSetup.h" +#include "ShaderConstants.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/xrRender_console.h" +#include "xrEngine/device.h" + +namespace xray::render::fg::passes { + +using namespace framegraph; + +struct alignas(16) AOConstantsGPU +{ + Fvector4 ao_screen; // xy size, zw 1/size + Fvector4 ao_params; // radius, bias, strength, quality + Fvector4 ao_proj; // gtao focal, frame, pad, pad +}; +static_assert(sizeof(AOConstantsGPU) == 48, "AOConstantsGPU must be 48 bytes"); + +struct AOPassData +{ + VirtualResourceHandle sceneColor; + VirtualResourceHandle depth; + VirtualResourceHandle normal; + VirtualResourceHandle worldPos; + VirtualResourceHandle aoRaw; + VirtualResourceHandle aoFiltered; + VirtualResourceHandle gtaoPacked; + VirtualResourceHandle output; + u32 width = 0; + u32 height = 0; + u32 genWidth = 0; + u32 genHeight = 0; + u32 mode = 0; + u32 quality = 0; + bool doBlur = false; + bool halfRes = false; + AmbientOcclusionPassState* passState = nullptr; +}; + +static nvrhi::GraphicsPipelineHandle CreateFullscreenPS( + const char* cacheName, + nvrhi::IShader* vs, + nvrhi::IShader* ps, + nvrhi::IBindingLayout* layout, + nvrhi::Format colorFormat, + nvrhi::IDevice* device) +{ + if (!vs || !ps || !layout) + return nullptr; + + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs); + desc.setPixelShader(ps); + desc.addBindingLayout(layout); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.blendState.targets[0].setBlendEnable(false); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.addColorFormat(colorFormat); + return GetPassResourceCache().GetOrCreatePipeline(cacheName, desc, fbInfo, device); +} + +void InitializeAmbientOcclusionPass(nvrhi::IDevice* device, AmbientOcclusionPassState& state) +{ + if (state.initialized || !device) + return; + + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + { + state.initialized = true; + return; + } + + auto vs = loader->LoadVertexShader("fullscreen"); + auto ssaoPs = loader->LoadPixelShader("ao/ssao"); + auto hbaoPs = loader->LoadPixelShader("ao/hbao"); + auto hdaoPs = loader->LoadPixelShader("ao/hdao"); + auto gtaoRenderPs = loader->LoadPixelShader("ao/gtao_render"); + auto gtaoFilterPs = loader->LoadPixelShader("ao/gtao_filter"); + auto blurPs = loader->LoadPixelShader("ao/ao_blur"); + auto applyPs = loader->LoadPixelShader("ao/ao_apply"); + + if (!vs.handle) + { + Msg("! [AO] Failed to load fullscreen VS"); + state.initialized = true; + return; + } + + auto& cache = GetPassResourceCache(); + + if (ssaoPs.handle && ssaoPs.reflection) + { + state.genLayout = cache.GetOrCreateBindingLayoutFromReflection( + "AO_Gen", *vs.reflection, *ssaoPs.reflection, device); + state.ssaoPipeline = CreateFullscreenPS( + "AO_SSAO", vs.handle, ssaoPs.handle, state.genLayout, nvrhi::Format::R16_FLOAT, device); + } + if (hbaoPs.handle && hbaoPs.reflection && state.genLayout) + { + state.hbaoPipeline = CreateFullscreenPS( + "AO_HBAO", vs.handle, hbaoPs.handle, state.genLayout, nvrhi::Format::R16_FLOAT, device); + } + else if (hbaoPs.handle && hbaoPs.reflection) + { + state.genLayout = cache.GetOrCreateBindingLayoutFromReflection( + "AO_Gen", *vs.reflection, *hbaoPs.reflection, device); + state.hbaoPipeline = CreateFullscreenPS( + "AO_HBAO", vs.handle, hbaoPs.handle, state.genLayout, nvrhi::Format::R16_FLOAT, device); + } + if (hdaoPs.handle && hdaoPs.reflection) + { + if (!state.genLayout) + state.genLayout = cache.GetOrCreateBindingLayoutFromReflection( + "AO_Gen", *vs.reflection, *hdaoPs.reflection, device); + state.hdaoPipeline = CreateFullscreenPS( + "AO_HDAO", vs.handle, hdaoPs.handle, state.genLayout, nvrhi::Format::R16_FLOAT, device); + } + + if (gtaoRenderPs.handle && gtaoRenderPs.reflection) + { + state.gtaoRenderLayout = cache.GetOrCreateBindingLayoutFromReflection( + "AO_GTAO_Render", *vs.reflection, *gtaoRenderPs.reflection, device); + state.gtaoRenderPipeline = CreateFullscreenPS( + "AO_GTAO_Render", vs.handle, gtaoRenderPs.handle, state.gtaoRenderLayout, + nvrhi::Format::R32_UINT, device); + } + if (gtaoFilterPs.handle && gtaoFilterPs.reflection) + { + state.gtaoFilterLayout = cache.GetOrCreateBindingLayoutFromReflection( + "AO_GTAO_Filter_v2", *vs.reflection, *gtaoFilterPs.reflection, device); + state.gtaoFilterPipeline = CreateFullscreenPS( + "AO_GTAO_Filter_v2", vs.handle, gtaoFilterPs.handle, state.gtaoFilterLayout, + nvrhi::Format::R16_FLOAT, device); + } + if (blurPs.handle && blurPs.reflection) + { + state.blurLayout = cache.GetOrCreateBindingLayoutFromReflection( + "AO_Blur", *vs.reflection, *blurPs.reflection, device); + state.blurPipeline = CreateFullscreenPS( + "AO_Blur", vs.handle, blurPs.handle, state.blurLayout, nvrhi::Format::R16_FLOAT, device); + } + if (applyPs.handle && applyPs.reflection) + { + state.applyLayout = cache.GetOrCreateBindingLayoutFromReflection( + "AO_Apply_v2", *vs.reflection, *applyPs.reflection, device); + state.applyPipeline = CreateFullscreenPS( + "AO_Apply_v2", vs.handle, applyPs.handle, state.applyLayout, nvrhi::Format::RGBA16_FLOAT, device); + } + + state.initialized = true; + Msg("* [AO] Pass initialized (ssao=%d hbao=%d hdao=%d gtao=%d apply=%d)", + state.ssaoPipeline ? 1 : 0, + state.hbaoPipeline ? 1 : 0, + state.hdaoPipeline ? 1 : 0, + state.gtaoRenderPipeline ? 1 : 0, + state.applyPipeline ? 1 : 0); +} + +static AOConstantsGPU MakeAOConstants(u32 width, u32 height, u32 quality) +{ + AOConstantsGPU c{}; + const float w = static_cast(width); + const float h = static_cast(height); + c.ao_screen.set(w, h, 1.0f / w, 1.0f / h); + + const float q = static_cast(std::max(1u, quality)); + // Per-mode radii live in shaders (classic r3 / IX-Ray). CB keeps quality + unused slots. + const float radius = 1.0f; // unused by classic HDAO/HBAO/GTAO; SSAO uses fixed kernel + const float bias = 0.0f; // classic HBAO g_AngleBias + const float strength = 1.0f; // apply uses fixed ambient weight; modes bake their own contrast + c.ao_params.set(radius, bias, strength, q); + + // IX-Ray gtao_parameters = focal * 0.5; SSAO/HBAO use full focal via WorldRadiusToUV when needed + const float focal = h / (2.0f * std::tan(Device.fFOV * 0.5f * (3.14159265f / 180.0f))); + c.ao_proj.set(focal * 0.5f, static_cast(Device.dwFrame), 0.f, 0.f); + return c; +} + +static void DrawFullscreen( + nvrhi::ICommandList* cmdList, + nvrhi::IDevice* device, + nvrhi::IGraphicsPipeline* pipeline, + nvrhi::IBindingSet* bindingSet, + nvrhi::ITexture* outputTex, + u32 width, + u32 height, + const char* fbName) +{ + if (!pipeline || !bindingSet || !outputTex) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(outputTex); + auto framebuffer = GetPassResourceCache().GetOrCreateFramebuffer(fbName, fbDesc, device); + + nvrhi::Viewport viewport; + viewport.minX = 0; + viewport.minY = 0; + viewport.maxX = static_cast(width); + viewport.maxY = static_cast(height); + viewport.minZ = 0.0f; + viewport.maxZ = 1.0f; + + nvrhi::GraphicsState state; + state.pipeline = pipeline; + state.framebuffer = framebuffer; + state.viewport.addViewportAndScissorRect(viewport); + state.addBindingSet(bindingSet); + cmdList->setGraphicsState(state); + cmdList->draw(nvrhi::DrawArguments().setVertexCount(3)); +} + +VirtualResourceHandle setupAmbientOcclusionPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle sceneColor, + VirtualResourceHandle depth, + VirtualResourceHandle normal, + VirtualResourceHandle worldPos, + u32 width, + u32 height, + AmbientOcclusionPassState& passState) +{ + if (device && device->GetNVRHIDevice()) + InitializeAmbientOcclusionPass(device->GetNVRHIDevice(), passState); + + const u32 mode = ps_r_ssao_mode; + const u32 quality = ps_r_ssao; + if (mode == ssao_mode_off || quality == 0 || !passState.applyPipeline) + return sceneColor; + + const bool isGtao = (mode == ssao_mode_gtao); + const bool isHbao = (mode == ssao_mode_hbao); + const bool isHdao = (mode == ssao_mode_hdao); + nvrhi::IGraphicsPipeline* genPipe = nullptr; + if (isGtao) + genPipe = passState.gtaoRenderPipeline; + else if (isHbao) + genPipe = passState.hbaoPipeline; + else if (isHdao) + genPipe = passState.hdaoPipeline; + else + genPipe = passState.ssaoPipeline; + + if (!genPipe) + return sceneColor; + + const bool doBlur = !isGtao && (ps_r2_ls_flags_ext.test(R2FLAGEXT_SSAO_BLUR) || quality >= 2); + // GTAO always half-res gen+filter (apply bilinear-upsamples). Other modes honor r2_ssao_half_data. + const bool halfRes = isGtao || ps_r2_ls_flags_ext.test(R2FLAGEXT_SSAO_HALF_DATA); + const u32 genW = halfRes ? std::max(1u, (width + 1) / 2) : width; + const u32 genH = halfRes ? std::max(1u, (height + 1) / 2) : height; + + ResourceDesc aoDesc; + aoDesc.type = ResourceDesc::Type::Texture2D; + aoDesc.width = genW; + aoDesc.height = genH; + aoDesc.format = nvrhi::Format::R16_FLOAT; + aoDesc.isRenderTarget = true; + aoDesc.isTransient = true; + aoDesc.debugName = "rt_AO"; + auto aoRaw = fg.CreateTexture("rt_AO", aoDesc); + + VirtualResourceHandle aoFiltered = aoRaw; + if (doBlur || isGtao) + { + ResourceDesc filtDesc = aoDesc; + filtDesc.debugName = "rt_AO_Filtered"; + aoFiltered = fg.CreateTexture("rt_AO_Filtered", filtDesc); + } + + VirtualResourceHandle gtaoPacked; + if (isGtao) + { + ResourceDesc packedDesc; + packedDesc.type = ResourceDesc::Type::Texture2D; + packedDesc.width = genW; + packedDesc.height = genH; + packedDesc.format = nvrhi::Format::R32_UINT; + packedDesc.isRenderTarget = true; + packedDesc.isTransient = true; + packedDesc.debugName = "rt_GTAO_Packed"; + gtaoPacked = fg.CreateTexture("rt_GTAO_Packed", packedDesc); + } + + ResourceDesc outDesc; + outDesc.type = ResourceDesc::Type::Texture2D; + outDesc.width = width; + outDesc.height = height; + outDesc.format = nvrhi::Format::RGBA16_FLOAT; + outDesc.isRenderTarget = true; + outDesc.isUAV = true; + outDesc.isTransient = true; + outDesc.debugName = "rt_AO_Applied"; + auto output = fg.CreateTexture("rt_AO_Applied", outDesc); + + auto& passData = fg.addCallbackPass( + "AmbientOcclusion", + [=, &passState](FrameGraph& builder, PassHandle passHandle, AOPassData& data) { + RenderPassBuilder passBuilder(builder, passHandle); + data.width = width; + data.height = height; + data.genWidth = genW; + data.genHeight = genH; + data.mode = mode; + data.quality = quality; + data.doBlur = doBlur; + data.halfRes = halfRes; + data.passState = &passState; + data.sceneColor = passBuilder.read(sceneColor, ResourceState::ShaderResource); + data.depth = passBuilder.read(depth, ResourceState::ShaderResource); + data.normal = passBuilder.read(normal, ResourceState::ShaderResource); + data.worldPos = passBuilder.read(worldPos, ResourceState::ShaderResource); + if (isGtao) + { + data.gtaoPacked = passBuilder.write(gtaoPacked, ResourceState::RenderTarget); + data.aoFiltered = passBuilder.write(aoFiltered, ResourceState::RenderTarget); + } + else + { + data.aoRaw = passBuilder.write(aoRaw, ResourceState::RenderTarget); + if (doBlur) + data.aoFiltered = passBuilder.write(aoFiltered, ResourceState::RenderTarget); + else + data.aoFiltered = data.aoRaw; + } + data.output = passBuilder.write(output, ResourceState::RenderTarget); + }, + [](const AOPassData& data, const FrameGraph& fg, fg::RenderContext* ctx) { + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + nvrhi::IDevice* nvDevice = cmdList->getDevice(); + auto* ps = data.passState; + if (!ps || !ps->applyPipeline) + return; + + auto* colorTex = fg.GetPhysicalTexture(data.sceneColor); + auto* depthTex = fg.GetPhysicalTexture(data.depth); + auto* normalTex = fg.GetPhysicalTexture(data.normal); + auto* worldPosTex = fg.GetPhysicalTexture(data.worldPos); + auto* outputTex = fg.GetPhysicalTexture(data.output); + if (!colorTex || !depthTex || !normalTex || !worldPosTex || !outputTex) + return; + + auto& cache = GetPassResourceCache(); + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(StaticGlobals), ctx->GetDevice()); + auto aoCB = cache.GetOrCreateVolatileCB( + "AO", "AOConstants", sizeof(AOConstantsGPU), ctx->GetDevice()); + + // Constants match gen resolution (half or full); apply upsamples via linear sampler. + AOConstantsGPU aoConsts = MakeAOConstants(data.genWidth, data.genHeight, data.quality); + cmdList->writeBuffer(aoCB, &aoConsts, sizeof(aoConsts)); + + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsRefl = loader->GetCachedReflection("fullscreen", ".vs"); + if (!vsRefl) + return; + + nvrhi::ITexture* aoForApply = nullptr; + const bool isGtao = (data.mode == ssao_mode_gtao); + const bool isHbao = (data.mode == ssao_mode_hbao); + const bool isHdao = (data.mode == ssao_mode_hdao); + const u32 gw = data.genWidth; + const u32 gh = data.genHeight; + + if (isGtao) + { + auto* packedTex = fg.GetPhysicalTexture(data.gtaoPacked); + auto* filteredTex = fg.GetPhysicalTexture(data.aoFiltered); + if (!packedTex || !filteredTex || !ps->gtaoRenderPipeline || !ps->gtaoFilterPipeline) + return; + + auto* gtaoRefl = loader->GetCachedReflection("ao/gtao_render", ".ps"); + if (!gtaoRefl) + return; + BindingSetBuilder genBsb(*vsRefl, *gtaoRefl, nvDevice, "AO.GTAORender"); + genBsb.ConstantBuffer("static_globals", staticGlobalsCB) + .ConstantBuffer("AOConstants", aoCB) + .Texture("g_Depth", depthTex) + .Texture("g_Normal", normalTex) + .Texture("g_WorldPos", worldPosTex); + auto genSet = cache.GetOrCreateBindingSet(genBsb.Build(), ps->gtaoRenderLayout, nvDevice); + DrawFullscreen(cmdList, nvDevice, ps->gtaoRenderPipeline, genSet, packedTex, + gw, gh, data.halfRes ? "AO_GTAO_Render_Half" : "AO_GTAO_Render"); + + cmdList->setTextureState(packedTex, nvrhi::AllSubresources, + nvrhi::ResourceStates::ShaderResource); + + auto* filtRefl = loader->GetCachedReflection("ao/gtao_filter", ".ps"); + if (!filtRefl) + return; + BindingSetBuilder filtBsb(*vsRefl, *filtRefl, nvDevice, "AO.GTAOFilter"); + filtBsb.Texture("t_gtao_packed", packedTex); + auto filtSet = cache.GetOrCreateBindingSet(filtBsb.Build(), ps->gtaoFilterLayout, nvDevice); + DrawFullscreen(cmdList, nvDevice, ps->gtaoFilterPipeline, filtSet, filteredTex, + gw, gh, data.halfRes ? "AO_GTAO_Filter_Half" : "AO_GTAO_Filter"); + + cmdList->setTextureState(filteredTex, nvrhi::AllSubresources, + nvrhi::ResourceStates::ShaderResource); + aoForApply = filteredTex; + } + else + { + auto* rawTex = fg.GetPhysicalTexture(data.aoRaw); + if (!rawTex) + return; + + const char* genShader = + isHbao ? "ao/hbao" : (isHdao ? "ao/hdao" : "ao/ssao"); + nvrhi::IGraphicsPipeline* genPipe = + isHbao ? ps->hbaoPipeline.Get() : (isHdao ? ps->hdaoPipeline.Get() : ps->ssaoPipeline.Get()); + if (!genPipe || !ps->genLayout) + return; + + auto* genRefl = loader->GetCachedReflection(genShader, ".ps"); + if (!genRefl) + return; + BindingSetBuilder genBsb(*vsRefl, *genRefl, nvDevice, "AO.Gen"); + genBsb.ConstantBuffer("static_globals", staticGlobalsCB) + .ConstantBuffer("AOConstants", aoCB) + .Texture("g_Depth", depthTex) + .Texture("g_Normal", normalTex) + .Texture("g_WorldPos", worldPosTex); + auto genSet = cache.GetOrCreateBindingSet(genBsb.Build(), ps->genLayout, nvDevice); + DrawFullscreen(cmdList, nvDevice, genPipe, genSet, rawTex, + gw, gh, data.halfRes ? "AO_Gen_Half" : "AO_Gen"); + + cmdList->setTextureState(rawTex, nvrhi::AllSubresources, + nvrhi::ResourceStates::ShaderResource); + + if (data.doBlur && ps->blurPipeline && ps->blurLayout) + { + auto* filteredTex = fg.GetPhysicalTexture(data.aoFiltered); + if (!filteredTex) + return; + auto* blurRefl = loader->GetCachedReflection("ao/ao_blur", ".ps"); + if (!blurRefl) + return; + BindingSetBuilder blurBsb(*vsRefl, *blurRefl, nvDevice, "AO.Blur"); + blurBsb.ConstantBuffer("AOConstants", aoCB) + .Texture("g_AO", rawTex) + .Texture("g_Depth", depthTex); + auto blurSet = cache.GetOrCreateBindingSet(blurBsb.Build(), ps->blurLayout, nvDevice); + DrawFullscreen(cmdList, nvDevice, ps->blurPipeline, blurSet, filteredTex, + gw, gh, data.halfRes ? "AO_Blur_Half" : "AO_Blur"); + cmdList->setTextureState(filteredTex, nvrhi::AllSubresources, + nvrhi::ResourceStates::ShaderResource); + aoForApply = filteredTex; + } + else + { + aoForApply = rawTex; + } + } + + if (!aoForApply) + return; + + auto* applyRefl = loader->GetCachedReflection("ao/ao_apply", ".ps"); + if (!applyRefl) + return; + BindingSetBuilder applyBsb(*vsRefl, *applyRefl, nvDevice, "AO.Apply"); + applyBsb.Texture("g_Color", colorTex) + .Texture("g_AO", aoForApply) + .Texture("g_Depth", depthTex); + auto applySet = cache.GetOrCreateBindingSet(applyBsb.Build(), ps->applyLayout, nvDevice); + DrawFullscreen(cmdList, nvDevice, ps->applyPipeline, applySet, outputTex, + data.width, data.height, "AO_Apply"); + }); + + return passData.output; +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/AmbientOcclusionPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/AmbientOcclusionPassSetup.h new file mode 100644 index 00000000000..da098085708 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/AmbientOcclusionPassSetup.h @@ -0,0 +1,49 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include "Layers/xrRender/FrameGraph/IPass.h" +#include + +namespace xray::render::framegraph { + class FrameGraph; +} + +namespace xray::render::fg { + class RenderDevice; +} + +namespace xray::render::fg::passes { + +struct AmbientOcclusionPassState { + nvrhi::GraphicsPipelineHandle ssaoPipeline; + nvrhi::GraphicsPipelineHandle hbaoPipeline; + nvrhi::GraphicsPipelineHandle hdaoPipeline; + nvrhi::GraphicsPipelineHandle gtaoRenderPipeline; + nvrhi::GraphicsPipelineHandle gtaoFilterPipeline; + nvrhi::GraphicsPipelineHandle blurPipeline; + nvrhi::GraphicsPipelineHandle applyPipeline; + + nvrhi::BindingLayoutHandle genLayout; // depth+normal+worldPos+AOConstants + nvrhi::BindingLayoutHandle gtaoRenderLayout; + nvrhi::BindingLayoutHandle gtaoFilterLayout; + nvrhi::BindingLayoutHandle blurLayout; + nvrhi::BindingLayoutHandle applyLayout; + + bool initialized = false; +}; + +void InitializeAmbientOcclusionPass(nvrhi::IDevice* device, AmbientOcclusionPassState& state); + +framegraph::VirtualResourceHandle setupAmbientOcclusionPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle normal, + framegraph::VirtualResourceHandle worldPos, + u32 width, + u32 height, + AmbientOcclusionPassState& state); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.cpp new file mode 100644 index 00000000000..2fa452cd73b --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.cpp @@ -0,0 +1,329 @@ +#include "stdafx.h" +#include "BloomPassSetup.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/xrRender_console.h" + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +static constexpr u32 kBloomCBVersions = 64; + +// Classic r2 threshold (~1e-5) is for LDR combine space. HDR forward needs a +// real bright-pass floor or the whole frame enters the bloom pyramid. +static float BloomThresholdHDR() +{ + const float t = ps_r2_ls_bloom_threshold; + return (t < 0.05f) ? 1.25f : t; +} + +static float BloomIntensity() +{ + return std::clamp(ps_r2_ls_bloom_kernel_scale * 0.55f, 0.1f, 1.25f); +} + +nvrhi::GraphicsPipelineHandle MakeFSPipe( + const char* name, nvrhi::IDevice* nv, + nvrhi::ShaderHandle vs, nvrhi::ShaderHandle ps, + nvrhi::BindingLayoutHandle layout, nvrhi::Format fmt, bool additive) +{ + auto& cache = GetPassResourceCache(); + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs); + desc.setPixelShader(ps); + desc.addBindingLayout(layout); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + auto& blend = desc.renderState.blendState.targets[0]; + if (additive) + { + blend.setBlendEnable(true); + blend.setSrcBlend(nvrhi::BlendFactor::One); + blend.setDestBlend(nvrhi::BlendFactor::One); + blend.setSrcBlendAlpha(nvrhi::BlendFactor::One); + blend.setDestBlendAlpha(nvrhi::BlendFactor::One); + } + else + { + blend.setBlendEnable(false); + } + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + nvrhi::FramebufferInfoEx fb; + fb.addColorFormat(fmt); + return cache.GetOrCreatePipeline(name, desc, fb, nv); +} + +void InitBloom(nvrhi::IDevice* nv, BloomPassState& st) +{ + if (st.initialized && st.extractPipe && st.downPipe && st.upPipe && st.compPipe) + return; + st.initialized = false; + + if (!nv) + { + st.initialized = true; + return; + } + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + { + st.initialized = true; + return; + } + auto vs = loader->LoadVertexShader("fullscreen"); + auto ex = loader->LoadPixelShader("bloom\\extract"); + auto dn = loader->LoadPixelShader("bloom\\downsample"); + auto up = loader->LoadPixelShader("bloom\\upsample"); + auto cp = loader->LoadPixelShader("bloom\\composite"); + if (!vs.handle || !ex.handle || !dn.handle || !up.handle || !cp.handle) + { + st.initialized = true; + return; + } + auto& cache = GetPassResourceCache(); + st.extractLayout = cache.GetOrCreateBindingLayoutFromReflection("BloomExtract_v3", *vs.reflection, *ex.reflection, nv); + st.downLayout = cache.GetOrCreateBindingLayoutFromReflection("BloomDown_v3", *vs.reflection, *dn.reflection, nv); + st.upLayout = cache.GetOrCreateBindingLayoutFromReflection("BloomUp_v3", *vs.reflection, *up.reflection, nv); + st.compLayout = cache.GetOrCreateBindingLayoutFromReflection("BloomComp_v3", *vs.reflection, *cp.reflection, nv); + if (st.extractLayout) + st.extractPipe = MakeFSPipe("BloomExtract_v3", nv, vs.handle, ex.handle, st.extractLayout, nvrhi::Format::RGBA16_FLOAT, false); + if (st.downLayout) + st.downPipe = MakeFSPipe("BloomDown_v3", nv, vs.handle, dn.handle, st.downLayout, nvrhi::Format::RGBA16_FLOAT, false); + if (st.upLayout) + st.upPipe = MakeFSPipe("BloomUp_v3", nv, vs.handle, up.handle, st.upLayout, nvrhi::Format::RGBA16_FLOAT, false); + if (st.compLayout) + st.compPipe = MakeFSPipe("BloomCompAdd_v3", nv, vs.handle, cp.handle, st.compLayout, nvrhi::Format::RGBA16_FLOAT, true); + st.initialized = true; +} + +bool DrawFS( + nvrhi::ICommandList* cmd, nvrhi::IDevice* nv, + nvrhi::GraphicsPipelineHandle pipe, nvrhi::BindingSetHandle set, + nvrhi::ITexture* rt, u32 w, u32 h, const char* fbName) +{ + if (!cmd || !nv || !pipe || !set || !rt) + return false; + // Guard against pool/alias size mismatches (viewport write into corner). + const auto& d = rt->getDesc(); + if (d.width < w || d.height < h) + { + Msg("! [Bloom] RT '%s' too small (%ux%u < %ux%u) — skip", fbName, d.width, d.height, w, h); + return false; + } + auto& cache = GetPassResourceCache(); + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(rt); + auto fb = cache.GetOrCreateFramebuffer(fbName, fbDesc, nv); + if (!fb) + return false; + nvrhi::GraphicsState gs; + gs.pipeline = pipe; + gs.framebuffer = fb; + gs.bindings = {set}; + gs.viewport.addViewportAndScissorRect(nvrhi::Viewport(float(w), float(h))); + cmd->setGraphicsState(gs); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); + return true; +} +} // namespace + +VirtualResourceHandle setupBloomPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle sceneColor, + u32 width, + u32 height, + BloomPassState& state) +{ + if (!device || !device->GetNVRHIDevice()) + return sceneColor; + InitBloom(device->GetNVRHIDevice(), state); + if (!state.extractPipe || !state.downPipe || !state.upPipe || !state.compPipe) + return sceneColor; + + // Non-transient: never alias bloom pyramids onto unrelated full-res RTs. + auto makeRT = [&](const char* name, u32 w, u32 h) { + ResourceDesc d; + d.type = ResourceDesc::Type::Texture2D; + d.width = w; + d.height = h; + d.format = nvrhi::Format::RGBA16_FLOAT; + d.isRenderTarget = true; + d.isTransient = false; + d.debugName = name; + return fg.CreateTexture(name, d); + }; + + u32 mipW[kBloomMips], mipH[kBloomMips]; + VirtualResourceHandle mips[kBloomMips]; + mipW[0] = std::max(1u, width / 2); + mipH[0] = std::max(1u, height / 2); + mips[0] = makeRT("rt_Bloom0", mipW[0], mipH[0]); + for (u32 i = 1; i < kBloomMips; ++i) + { + mipW[i] = std::max(1u, mipW[i - 1] / 2); + mipH[i] = std::max(1u, mipH[i - 1] / 2); + char name[32]; + xr_sprintf(name, "rt_Bloom%u", i); + mips[i] = makeRT(name, mipW[i], mipH[i]); + } + + VirtualResourceHandle upMips[kBloomMips - 1]; + for (u32 i = 0; i < kBloomMips - 1; ++i) + { + char name[32]; + xr_sprintf(name, "rt_BloomUp%u", i); + upMips[i] = makeRT(name, mipW[kBloomMips - 2 - i], mipH[kBloomMips - 2 - i]); + } + + auto output = makeRT("rt_BloomComposite", width, height); + + struct PassData + { + VirtualResourceHandle scene; + VirtualResourceHandle mips[kBloomMips]; + VirtualResourceHandle upMips[kBloomMips - 1]; + VirtualResourceHandle output; + u32 mipW[kBloomMips], mipH[kBloomMips]; + u32 width, height; + BloomPassState* st = nullptr; + fg::RenderDevice* device = nullptr; + }; + + auto& pd = fg.addCallbackPass( + "Bloom", + [&](FrameGraph& b, PassHandle ph, PassData& data) { + RenderPassBuilder pb(b, ph); + data.st = &state; + data.device = device; + data.width = width; + data.height = height; + data.scene = pb.read(sceneColor, ResourceState::ShaderResource); + for (u32 i = 0; i < kBloomMips; ++i) + { + data.mips[i] = pb.write(mips[i], ResourceState::RenderTarget); + data.mipW[i] = mipW[i]; + data.mipH[i] = mipH[i]; + } + for (u32 i = 0; i < kBloomMips - 1; ++i) + data.upMips[i] = pb.write(upMips[i], ResourceState::RenderTarget); + data.output = pb.write(output, ResourceState::RenderTarget); + pb.sideEffects(); + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd ? cmd->getDevice() : nullptr; + auto* st = data.st; + if (!cmd || !nv || !st) + return; + auto& cache = GetPassResourceCache(); + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); + auto* exR = loader->GetCachedReflection("bloom\\extract", ".ps"); + auto* dnR = loader->GetCachedReflection("bloom\\downsample", ".ps"); + auto* upR = loader->GetCachedReflection("bloom\\upsample", ".ps"); + auto* cpR = loader->GetCachedReflection("bloom\\composite", ".ps"); + if (!vsR || !exR || !dnR || !upR || !cpR) + return; + + auto* scene = graph.GetPhysicalTexture(data.scene); + nvrhi::ITexture* mipTex[kBloomMips]; + for (u32 i = 0; i < kBloomMips; ++i) + mipTex[i] = graph.GetPhysicalTexture(data.mips[i]); + if (!scene || !mipTex[0]) + return; + + struct alignas(16) P4 { float x, y, z, w; }; + auto* cb = cache.GetOrCreateVolatileCB( + "Bloom", "BloomParams", sizeof(P4), data.device, kBloomCBVersions); + if (!cb) + return; + + const float thr = BloomThresholdHDR(); + const float intensity = BloomIntensity(); + + // Extract → mip0 + { + P4 p{thr, 0.5f, 0, 0}; + cmd->writeBuffer(cb, &p, sizeof(p)); + BindingSetBuilder bsb(*vsR, *exR, nv, "Bloom.Extract"); + bsb.ConstantBuffer("BloomParams", cb); + bsb.Texture("g_Color", scene); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), st->extractLayout, nv); + DrawFS(cmd, nv, st->extractPipe, set, mipTex[0], data.mipW[0], data.mipH[0], "BloomExtractFB"); + cmd->setTextureState(mipTex[0], nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + + // Downsample chain + for (u32 i = 1; i < kBloomMips; ++i) + { + if (!mipTex[i] || !mipTex[i - 1]) + break; + P4 p{1.f / float(data.mipW[i - 1]), 1.f / float(data.mipH[i - 1]), 0, 0}; + cmd->writeBuffer(cb, &p, sizeof(p)); + BindingSetBuilder bsb(*vsR, *dnR, nv, "Bloom.Down"); + bsb.ConstantBuffer("BloomParams", cb); + bsb.Texture("g_Color", mipTex[i - 1]); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), st->downLayout, nv); + char fbName[32]; + xr_sprintf(fbName, "BloomDown%u", i); + DrawFS(cmd, nv, st->downPipe, set, mipTex[i], data.mipW[i], data.mipH[i], fbName); + cmd->setTextureState(mipTex[i], nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + + // Upsample + accumulate toward larger mips + nvrhi::ITexture* bloomFull = mipTex[kBloomMips - 1]; + for (int i = int(kBloomMips) - 2; i >= 0; --i) + { + auto* upRT = graph.GetPhysicalTexture(data.upMips[kBloomMips - 2 - i]); + auto* highMip = mipTex[i]; + if (!upRT || !bloomFull || !highMip) + break; + P4 p{1.f / float(data.mipW[i + 1]), 1.f / float(data.mipH[i + 1]), 0, 0}; + cmd->writeBuffer(cb, &p, sizeof(p)); + BindingSetBuilder bsb(*vsR, *upR, nv, "Bloom.Up"); + bsb.ConstantBuffer("BloomParams", cb); + bsb.Texture("g_Low", bloomFull).Texture("g_High", highMip); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), st->upLayout, nv); + char fbName[32]; + xr_sprintf(fbName, "BloomUp%u", i); + if (!DrawFS(cmd, nv, st->upPipe, set, upRT, data.mipW[i], data.mipH[i], fbName)) + break; + cmd->setTextureState(upRT, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + bloomFull = upRT; + } + + auto* out = graph.GetPhysicalTexture(data.output); + if (!out || !bloomFull) + return; + + // Preserve scene: copy HDR, then add bloom (additive PSO). + cmd->setTextureState(scene, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(out, nvrhi::TextureSlice(), scene, nvrhi::TextureSlice()); + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); + cmd->setTextureState(bloomFull, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + + P4 p{intensity, 0, 0, 0}; + cmd->writeBuffer(cb, &p, sizeof(p)); + BindingSetBuilder bsb(*vsR, *cpR, nv, "Bloom.Comp"); + bsb.ConstantBuffer("BloomParams", cb); + bsb.Texture("g_Bloom", bloomFull); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), st->compLayout, nv); + DrawFS(cmd, nv, st->compPipe, set, out, data.width, data.height, "BloomCompFB"); + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + }); + + return pd.output; +} + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.h new file mode 100644 index 00000000000..318b370211d --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.h @@ -0,0 +1,36 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes +{ + +static constexpr u32 kBloomMips = 5; + +struct BloomPassState +{ + nvrhi::GraphicsPipelineHandle extractPipe; + nvrhi::GraphicsPipelineHandle downPipe; + nvrhi::GraphicsPipelineHandle upPipe; + nvrhi::GraphicsPipelineHandle compPipe; + nvrhi::BindingLayoutHandle extractLayout; + nvrhi::BindingLayoutHandle downLayout; + nvrhi::BindingLayoutHandle upLayout; + nvrhi::BindingLayoutHandle compLayout; + bool initialized = false; +}; + +framegraph::VirtualResourceHandle setupBloomPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + u32 width, + u32 height, + BloomPassState& state); + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/CASPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/CASPassSetup.cpp new file mode 100644 index 00000000000..59a4244a378 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/CASPassSetup.cpp @@ -0,0 +1,154 @@ +#include "stdafx.h" +#include "CASPassSetup.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" + +extern ENGINE_API float ps_r_cas_sharpness; + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +void EnsureCASPipeline(nvrhi::IDevice* nv, CASPassState& st, nvrhi::Format outFmt) +{ + if (!nv) + return; + if (st.initialized && st.pipeline && st.pipelineFormat == outFmt) + return; + + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + { + st.initialized = true; + return; + } + auto vs = loader->LoadVertexShader("fullscreen"); + auto ps = loader->LoadPixelShader("cas"); + if (!vs.handle || !ps.handle) + { + st.initialized = true; + return; + } + auto& cache = GetPassResourceCache(); + if (!st.layout) + st.layout = cache.GetOrCreateBindingLayoutFromReflection("CAS_v1", *vs.reflection, *ps.reflection, nv); + if (st.layout) + { + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs.handle); + desc.setPixelShader(ps.handle); + desc.addBindingLayout(st.layout); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.blendState.targets[0].setBlendEnable(false); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + nvrhi::FramebufferInfoEx fb; + fb.addColorFormat(outFmt); + // Unique cache key per format + char pipeName[64]; + xr_sprintf(pipeName, "CAS_v1_fmt%u", (u32)outFmt); + st.pipeline = cache.GetOrCreatePipeline(pipeName, desc, fb, nv); + st.pipelineFormat = outFmt; + } + st.initialized = true; +} +} // namespace + +VirtualResourceHandle setupCASPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle ldrInput, + VirtualResourceHandle outputTarget, + u32 width, + u32 height, + CASPassState& state) +{ + if (!device || !device->GetNVRHIDevice() || !outputTarget.is_valid()) + return ldrInput; + + nvrhi::Format outFmt = nvrhi::Format::RGBA8_UNORM; + if (GEnv.Backend && GEnv.Backend->GetBackBuffer()) + outFmt = GEnv.Backend->GetBackBuffer()->getDesc().format; + + EnsureCASPipeline(device->GetNVRHIDevice(), state, outFmt); + if (!state.pipeline || !state.layout) + return ldrInput; + + struct PassData + { + VirtualResourceHandle input, output; + u32 width, height; + CASPassState* st = nullptr; + fg::RenderDevice* device = nullptr; + nvrhi::Format outFmt = nvrhi::Format::RGBA8_UNORM; + }; + + auto& pd = fg.addCallbackPass( + "CAS", + [&](FrameGraph& b, PassHandle ph, PassData& data) { + RenderPassBuilder pb(b, ph); + data.st = &state; + data.device = device; + data.width = width; + data.height = height; + data.outFmt = outFmt; + data.input = pb.read(ldrInput, ResourceState::ShaderResource); + data.output = pb.write(outputTarget, ResourceState::RenderTarget); + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd ? cmd->getDevice() : nullptr; + auto* in = graph.GetPhysicalTexture(data.input); + auto* out = graph.GetPhysicalTexture(data.output); + if (!cmd || !nv || !in || !out || !data.st) + return; + + // Prefer actual output texture format (handles swapchain BGRA etc.) + nvrhi::Format fmt = out->getDesc().format; + EnsureCASPipeline(nv, *data.st, fmt); + if (!data.st->pipeline || !data.st->layout) + return; + + auto& cache = GetPassResourceCache(); + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psR = loader->GetCachedReflection("cas", ".ps"); + if (!vsR || !psR) return; + + struct alignas(16) PCB { float sx, sy, sharp, pad; }; + auto* cb = cache.GetOrCreateVolatileCB("CAS", "CASParams", sizeof(PCB), data.device); + PCB p{float(data.width), float(data.height), ps_r_cas_sharpness, 0}; + if (cb) cmd->writeBuffer(cb, &p, sizeof(p)); + + BindingSetBuilder bsb(*vsR, *psR, nv, "CAS"); + if (cb) bsb.ConstantBuffer("CASParams", cb); + bsb.Texture("g_Color", in); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->layout, nv); + if (!set) return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(out); + auto fb = cache.GetOrCreateFramebuffer("CAS", fbDesc, nv); + if (!fb) return; + + nvrhi::GraphicsState gs; + gs.pipeline = data.st->pipeline; + gs.framebuffer = fb; + gs.bindings = {set}; + gs.viewport.addViewportAndScissorRect(nvrhi::Viewport(float(data.width), float(data.height))); + cmd->setGraphicsState(gs); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); + }); + + return pd.output; +} + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/CASPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/CASPassSetup.h new file mode 100644 index 00000000000..070a1f33d33 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/CASPassSetup.h @@ -0,0 +1,31 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes +{ + +struct CASPassState +{ + nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::BindingLayoutHandle layout; + nvrhi::Format pipelineFormat = nvrhi::Format::UNKNOWN; + bool initialized = false; +}; + +// LDR sharpen: input → outputTarget (usually backbuffer) +framegraph::VirtualResourceHandle setupCASPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle ldrInput, + framegraph::VirtualResourceHandle outputTarget, + u32 width, + u32 height, + CASPassState& state); + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/ClusterLightPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ClusterLightPassSetup.cpp index 418a3d9137b..561641171d0 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ClusterLightPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ClusterLightPassSetup.cpp @@ -120,7 +120,8 @@ void setupClusterLightPass( const Fmatrix& prevViewProj, bool hasPrevViewProj) { - bool useHiZ = hasPrevViewProj && hizPyramid.is_valid() && hizWidth > 0 && hizHeight > 0; + bool useHiZ = (ps_r_hiz_occlusion != 0) && hasPrevViewProj && hizPyramid.is_valid() && + hizWidth > 0 && hizHeight > 0; fg.addCallbackPass( "ClusterLightAssign", @@ -128,6 +129,10 @@ void setupClusterLightPass( FrameGraph& builder, PassHandle passHandle, ClusterLightPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); passBuilder.sideEffects(); + // Must stay on graphics queue: Forward+ samples cluster buffers the same + // frame. Async + sideEffects-only had no FG barrier → empty clusters / + // dead flashlight & indoor point lights on MoltenVK. + // passBuilder.asyncCompute(); if (useHiZ && hizPyramid.is_valid()) passBuilder.read(hizPyramid); @@ -234,6 +239,10 @@ void setupClusterLightPass( "Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); cmdList->writeBuffer(clusterParamsCB, &clusterCB, sizeof(clusterCB)); + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(viewParamsCB, &sg, sizeof(sg)); + } auto* csRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("cluster_light_assign", ".cs"); if (!csRefl) return; diff --git a/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.cpp new file mode 100644 index 00000000000..4008304d997 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.cpp @@ -0,0 +1,264 @@ +#include "stdafx.h" +#include "ContactShadowsPassSetup.h" +#include "ShaderConstants.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" + +extern ENGINE_API int ps_r_contact_shadows; +extern ENGINE_API float ps_r_contact_shadows_length; + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +void EnsureHistory(nvrhi::IDevice* nv, ContactShadowsPassState& st, u32 w, u32 h) +{ + if (st.history[0] && st.historyW == w && st.historyH == h) + return; + for (int i = 0; i < 2; ++i) + { + nvrhi::TextureDesc td; + td.width = w; + td.height = h; + td.format = nvrhi::Format::RGBA16_FLOAT; + td.isRenderTarget = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = (i == 0) ? "rt_ContactHist0" : "rt_ContactHist1"; + st.history[i] = nv->createTexture(td); + } + st.historyW = w; + st.historyH = h; + st.hasHistory = false; + st.historyIndex = 0; + if (st.history[0] && st.history[1]) + { + nvrhi::CommandListHandle cmd = nv->createCommandList(); + cmd->open(); + cmd->clearTextureFloat(st.history[0], nvrhi::AllSubresources, nvrhi::Color(1.f, 0.f, 0.f, 0.f)); + cmd->clearTextureFloat(st.history[1], nvrhi::AllSubresources, nvrhi::Color(1.f, 0.f, 0.f, 0.f)); + cmd->close(); + nv->executeCommandList(cmd); + } +} + +void Init(nvrhi::IDevice* nv, ContactShadowsPassState& st) +{ + if (st.initialized || !nv) + return; + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) { st.initialized = true; return; } + + auto vs = loader->LoadVertexShader("fullscreen"); + auto psMarch = loader->LoadPixelShader("contact_shadows"); + if (!vs.handle || !psMarch.handle) + { + st.initialized = true; + return; + } + + auto& cache = GetPassResourceCache(); + st.marchLayout = cache.GetOrCreateBindingLayoutFromReflection( + "ContactMarch_v2", *vs.reflection, *psMarch.reflection, nv); + if (st.marchLayout) + { + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs.handle); + desc.setPixelShader(psMarch.handle); + desc.addBindingLayout(st.marchLayout); + auto* backend = GEnv.Backend; + if (backend && backend->GetBindlessLayout()) + desc.addBindingLayout(backend->GetBindlessLayout()); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.blendState.targets[0].setBlendEnable(false); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + nvrhi::FramebufferInfoEx fb; + fb.addColorFormat(nvrhi::Format::RGBA16_FLOAT); + st.marchPipeline = cache.GetOrCreatePipeline("ContactMarch_v2", desc, fb, nv); + } + st.initialized = true; +} +} // namespace + +nvrhi::ITexture* GetContactShadowHistory(ContactShadowsPassState& state) +{ + if (!state.hasHistory || !state.history[state.historyIndex]) + return nullptr; + return state.history[state.historyIndex].Get(); +} + +VirtualResourceHandle setupContactShadowsPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle sceneColor, + VirtualResourceHandle depth, + VirtualResourceHandle motionVectors, + u32 width, + u32 height, + bool hasPrevFrame, + ContactShadowsPassState& state) +{ + if (!ps_r_contact_shadows || !device || !device->GetNVRHIDevice()) + return sceneColor; + + Init(device->GetNVRHIDevice(), state); + if (!state.marchPipeline || !state.marchLayout) + return sceneColor; + + EnsureHistory(device->GetNVRHIDevice(), state, width, height); + if (!state.history[0] || !state.history[1]) + return sceneColor; + + const u32 readIdx = state.historyIndex; + const u32 writeIdx = 1u - readIdx; + + ResourceDesc marchDesc; + marchDesc.type = ResourceDesc::Type::Texture2D; + marchDesc.width = width; + marchDesc.height = height; + marchDesc.format = nvrhi::Format::RGBA16_FLOAT; + marchDesc.isRenderTarget = true; + marchDesc.isTransient = true; + marchDesc.debugName = "rt_ContactMarch"; + auto marchRT = fg.CreateTexture("rt_ContactMarch", marchDesc); + + ResourceDesc histDesc; + histDesc.type = ResourceDesc::Type::Texture2D; + histDesc.width = width; + histDesc.height = height; + histDesc.format = nvrhi::Format::RGBA16_FLOAT; + histDesc.isImported = true; + histDesc.debugName = "rt_ContactHistRead"; + auto histRead = fg.ImportTexture("rt_ContactHistRead", state.history[readIdx].Get(), histDesc); + + const bool useTemporal = motionVectors.is_valid() && hasPrevFrame && state.hasHistory; + + struct PassData + { + VirtualResourceHandle depth, motion, hist, march; + ContactShadowsPassState* st = nullptr; + fg::RenderDevice* device = nullptr; + u32 width = 0, height = 0, writeIdx = 0; + bool useTemporal = false; + }; + + fg.addCallbackPass( + "ContactShadows", + [&, marchRT, histRead, useTemporal, writeIdx](FrameGraph& b, PassHandle ph, PassData& data) { + RenderPassBuilder pb(b, ph); + data.st = &state; + data.device = device; + data.width = width; + data.height = height; + data.writeIdx = writeIdx; + data.useTemporal = useTemporal; + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.hist = pb.read(histRead, ResourceState::ShaderResource); + if (useTemporal) + data.motion = pb.read(motionVectors, ResourceState::ShaderResource); + data.march = pb.write(marchRT, ResourceState::RenderTarget); + pb.sideEffects(); + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd ? cmd->getDevice() : nullptr; + auto* depthTex = graph.GetPhysicalTexture(data.depth); + auto* hist = graph.GetPhysicalTexture(data.hist); + auto* march = graph.GetPhysicalTexture(data.march); + if (!cmd || !nv || !depthTex || !hist || !march || !data.st) + return; + + nvrhi::ITexture* motionTex = nullptr; + if (data.useTemporal && data.motion.is_valid()) + motionTex = graph.GetPhysicalTexture(data.motion); + if (!motionTex) + { + static nvrhi::TextureHandle s_zero; + if (!s_zero) + { + nvrhi::TextureDesc td; + td.width = 1; + td.height = 1; + td.format = nvrhi::Format::RG16_FLOAT; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "DummyZeroMV_Contact"; + s_zero = nv->createTexture(td); + float z[2] = {0, 0}; + cmd->writeTexture(s_zero, 0, 0, z, sizeof(z)); + } + motionTex = s_zero; + } + + auto& cache = GetPassResourceCache(); + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psMarchR = loader->GetCachedReflection("contact_shadows", ".ps"); + if (!vsR || !psMarchR) + return; + + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + + struct alignas(16) MarchCB + { + float screenX, screenY, rayLen, hasHistory; + }; + MarchCB mcb{ + float(data.width), float(data.height), + ps_r_contact_shadows_length, + data.useTemporal ? 1.f : 0.f}; + auto* marchCB = cache.GetOrCreateVolatileCB("Contact", "MarchParams", sizeof(MarchCB), data.device); + if (marchCB) + cmd->writeBuffer(marchCB, &mcb, sizeof(mcb)); + + BindingSetBuilder bsb(*vsR, *psMarchR, nv, "Contact.March"); + if (staticGlobalsCB) + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + if (marchCB) + bsb.ConstantBuffer("ContactParams", marchCB); + bsb.Texture("g_Depth", depthTex) + .Texture("g_Motion", motionTex) + .Texture("g_History", hist); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->marchLayout, nv); + if (!set || !data.st->marchPipeline) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(march); + auto fb = cache.GetOrCreateFramebuffer("ContactMarch", fbDesc, nv); + nvrhi::GraphicsState gs; + gs.pipeline = data.st->marchPipeline; + gs.framebuffer = fb; + gs.bindings = {set}; + if (GEnv.Backend && GEnv.Backend->GetBindlessDescriptorTable()) + gs.addBindingSet(GEnv.Backend->GetBindlessDescriptorTable()); + gs.viewport.addViewportAndScissorRect( + nvrhi::Viewport(float(data.width), float(data.height))); + cmd->setGraphicsState(gs); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); + + if (data.st->history[data.writeIdx]) + { + cmd->copyTexture( + data.st->history[data.writeIdx], nvrhi::TextureSlice(), + march, nvrhi::TextureSlice()); + data.st->historyIndex = data.writeIdx; + data.st->hasHistory = true; + } + }); + + return sceneColor; +} + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.h new file mode 100644 index 00000000000..0ccbe022177 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.h @@ -0,0 +1,39 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes +{ + +struct ContactShadowsPassState +{ + nvrhi::GraphicsPipelineHandle marchPipeline; + nvrhi::BindingLayoutHandle marchLayout; + nvrhi::TextureHandle history[2]; + u32 historyW = 0; + u32 historyH = 0; + u32 historyIndex = 0; + bool hasHistory = false; + bool initialized = false; +}; + +// Returns the history texture to bind as g_ContactHistory @ t28 (may be null). +nvrhi::ITexture* GetContactShadowHistory(ContactShadowsPassState& state); + +framegraph::VirtualResourceHandle setupContactShadowsPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle motionVectors, + u32 width, + u32 height, + bool hasPrevFrame, + ContactShadowsPassState& state); + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.cpp new file mode 100644 index 00000000000..eb3a35f0f8c --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.cpp @@ -0,0 +1,295 @@ +#include "stdafx.h" +#include "DepthPrepassSetup.h" +#include "PassCommon.h" +#include "ShaderConstants.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/Geometry/MaterialCache.h" +#include "Layers/xrRender/Bindless/MaterialBuffer.h" +#include "Layers/xrRender/Bindless/TerrainMaterialBuffer.h" +#include "Layers/xrRender/xrRender_console.h" +#include "xrEngine/IRenderBackend.h" + +namespace xray::render::fg::passes +{ +using namespace framegraph; +using namespace bindless; + +struct DepthPrepassData { + VirtualResourceHandle depth; + fg::RenderDevice* device = nullptr; + const GeometryCollector* geometry = nullptr; + MaterialCache* materialCache = nullptr; + DepthPrepassState* passState = nullptr; + BindlessForwardConfig bindlessConfig; + u32 width = 0; + u32 height = 0; +}; + +static void InitializeDepthPrepass(fg::RenderDevice* device, DepthPrepassState& state) +{ + if (state.initialized) + return; + + auto* nvDevice = device->GetNVRHIDevice(); + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + if (!nvDevice || !shaderLoader) + return; + + auto vsResult = shaderLoader->LoadVertexShader("bindless_depth", "main"); + auto psResult = shaderLoader->LoadPixelShader("bindless_depth", "main"); + auto opaquePsResult = shaderLoader->LoadPixelShader("bindless_depth_opaque", "main"); + if (!vsResult.handle || !psResult.handle) + { + Msg("! [DepthPrepass] Failed to load bindless_depth shaders"); + return; + } + state.vs = vsResult.handle; + state.ps = psResult.handle; + if (opaquePsResult.handle) + state.terrainPs = opaquePsResult.handle; + + auto& cache = GetPassResourceCache(); + state.layout = cache.GetOrCreateBindingLayoutFromReflection( + "DepthPrepass_v1", *vsResult.reflection, *psResult.reflection, nvDevice); + if (!state.layout) + return; + + u32 attrCount = 0; + auto* attrs = GetUnifiedVertexAttributes(attrCount); + state.inputLayout = nvDevice->createInputLayout(attrs, attrCount, state.vs); + + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.depthFormat = nvrhi::Format::D32; + + nvrhi::GraphicsPipelineDesc pipeDesc; + pipeDesc.VS = state.vs; + pipeDesc.PS = state.ps; + pipeDesc.inputLayout = state.inputLayout; + pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; + pipeDesc.renderState.depthStencilState.depthTestEnable = true; + pipeDesc.renderState.depthStencilState.depthWriteEnable = true; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + pipeDesc.renderState.rasterState.frontCounterClockwise = false; + pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; + + auto* backend = device->GetBackend(); + nvrhi::IBindingLayout* bindlessLayout = backend ? backend->GetBindlessLayout() : nullptr; + if (bindlessLayout) + pipeDesc.bindingLayouts = {state.layout, bindlessLayout}; + else + pipeDesc.bindingLayouts = {state.layout}; + + state.pipeline = cache.GetOrCreatePipeline("DepthPrepass_v1", pipeDesc, fbInfo, nvDevice); + if (!state.pipeline) + { + Msg("! [DepthPrepass] Pipeline create failed"); + return; + } + + // Opaque terrain: same VS, empty PS (no material SRV needed — layout still OK if unused) + if (state.terrainPs && opaquePsResult.reflection) + { + state.terrainLayout = cache.GetOrCreateBindingLayoutFromReflection( + "DepthPrepass_Terrain_v1", *vsResult.reflection, *opaquePsResult.reflection, nvDevice); + if (state.terrainLayout) + { + nvrhi::GraphicsPipelineDesc terrainDesc = pipeDesc; + terrainDesc.PS = state.terrainPs; + if (bindlessLayout) + terrainDesc.bindingLayouts = {state.terrainLayout, bindlessLayout}; + else + terrainDesc.bindingLayouts = {state.terrainLayout}; + state.terrainPipeline = cache.GetOrCreatePipeline( + "DepthPrepass_Terrain_v1", terrainDesc, fbInfo, nvDevice); + } + } + + state.initialized = true; + Msg("* [DepthPrepass] Pipeline ready (terrain=%d)", state.terrainPipeline ? 1 : 0); +} + +VirtualResourceHandle setupDepthPrepass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle depthInput, + const GeometryCollector* geometry, + MaterialCache* materialCache, + u32 width, + u32 height, + const BindlessForwardConfig& bindlessConfig, + DepthPrepassState* state) +{ + if (!ps_r_depth_prepass || !state || !depthInput.is_valid()) + return depthInput; + + auto& passData = fg.addCallbackPass( + "DepthPrepass", + [&, width, height, depthInput, bindlessConfig, state]( + FrameGraph& builder, PassHandle passHandle, DepthPrepassData& data) { + RenderPassBuilder passBuilder(builder, passHandle); + data.depth = passBuilder.write(depthInput, ResourceState::DepthStencilWrite); + data.device = device; + data.geometry = geometry; + data.materialCache = materialCache; + data.passState = state; + data.bindlessConfig = bindlessConfig; + data.width = width; + data.height = height; + }, + [](const DepthPrepassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + ZoneScoped; + ZoneName("DepthPrepass", 12); + + if (!data.passState || !data.device) + return; + + InitializeDepthPrepass(data.device, *data.passState); + if (!data.passState->initialized || !data.passState->pipeline) + return; + + if (!data.bindlessConfig.UseGPUCulling() || !data.bindlessConfig.UseMegaBuffers()) + return; + + auto* depthRT = fgGraph.GetPhysicalTexture(data.depth); + if (!depthRT) + return; + + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + if (!cmdList || !nvDevice) + return; + + cmdList->clearDepthStencilTexture(depthRT, nvrhi::AllSubresources, true, 1.0f, false, 0); + + if (data.materialCache) + data.materialCache->FinalizePendingMaterials(ctx); + + auto& matBuffer = MaterialBuffer::Instance(); + matBuffer.Upload(ctx); + + auto& cache = GetPassResourceCache(); + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + + auto drawIndexBuffer = GetOrCreateDrawIndexBuffer("DepthPrepass", nvDevice); + if (!drawIndexBuffer) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.setDepthAttachment(depthRT); + auto framebuffer = cache.GetOrCreateFramebuffer("DepthPrepass", fbDesc, nvDevice); + if (!framebuffer) + return; + + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* vsRefl = shaderLoader->GetCachedReflection("bindless_depth", ".vs"); + auto* psRefl = shaderLoader->GetCachedReflection("bindless_depth", ".ps"); + if (!vsRefl || !psRefl) + return; + + nvrhi::IBindingSet* bindlessTable = nullptr; + if (auto* backend = data.device->GetBackend()) + bindlessTable = backend->GetBindlessDescriptorTable(); + + nvrhi::Viewport viewport( + 0.0f, static_cast(data.width), + 0.0f, static_cast(data.height), + 0.0f, 1.0f); + + auto drawSet = [&](const BindlessDrawSet& set) { + if (!set.IsValid()) + return; + + BindingSetBuilder bsb(*vsRefl, *psRefl, nvDevice, "DepthPrepass"); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + bsb.BufferSRV("g_InstanceData", set.instanceBuffer); + bsb.BufferSRV("g_CompactBatchIndices", set.compactBatchIndicesBuffer); + bsb.BufferSRV("g_CompactMaterialIDs", set.compactMaterialIDBuffer); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.passState->layout, nvDevice); + if (!bindingSet) + return; + + nvrhi::GraphicsState gs; + gs.pipeline = data.passState->pipeline; + gs.framebuffer = framebuffer; + gs.bindings = {bindingSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {data.bindlessConfig.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = {data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + gs.viewport.addViewport(viewport); + gs.viewport.addScissorRect(nvrhi::Rect(data.width, data.height)); + gs.indirectParams = set.compactDrawArgsBuffer; + gs.indirectCountBuffer = set.compactCountBuffer; + + cmdList->setGraphicsState(gs); + DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, set.totalObjectCount); + }; + + drawSet(data.bindlessConfig.staticSet); + drawSet(data.bindlessConfig.dynamicSet); + + // Terrain fills most outdoor pixels — critical for early-Z + if (data.passState->terrainPipeline && data.passState->terrainLayout && + data.bindlessConfig.HasTerrain() && data.bindlessConfig.UseTerrainCompaction()) + { + if (data.materialCache) + data.materialCache->FinalizePendingTerrainMaterials(ctx); + + auto* opaquePsRefl = shaderLoader->GetCachedReflection("bindless_depth_opaque", ".ps"); + if (opaquePsRefl && + data.bindlessConfig.terrainInstanceBuffer && + data.bindlessConfig.terrainCompactBatchIndicesBuffer && + data.bindlessConfig.terrainCompactMaterialIDBuffer && + data.bindlessConfig.terrainCompactDrawArgsBuffer && + data.bindlessConfig.terrainCompactCountBuffer) + { + BindingSetBuilder tbsb(*vsRefl, *opaquePsRefl, nvDevice, "DepthPrepass.Terrain"); + tbsb.ConstantBuffer("static_globals", staticGlobalsCB); + tbsb.BufferSRV("g_InstanceData", data.bindlessConfig.terrainInstanceBuffer); + tbsb.BufferSRV("g_CompactBatchIndices", data.bindlessConfig.terrainCompactBatchIndicesBuffer); + tbsb.BufferSRV("g_CompactMaterialIDs", data.bindlessConfig.terrainCompactMaterialIDBuffer); + auto terrainSet = cache.GetOrCreateBindingSet( + tbsb.Build(), data.passState->terrainLayout, nvDevice); + if (terrainSet) + { + nvrhi::GraphicsState gs; + gs.pipeline = data.passState->terrainPipeline; + gs.framebuffer = framebuffer; + gs.bindings = {terrainSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {data.bindlessConfig.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = { + data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + gs.viewport.addViewport(viewport); + gs.viewport.addScissorRect(nvrhi::Rect(data.width, data.height)); + gs.indirectParams = data.bindlessConfig.terrainCompactDrawArgsBuffer; + gs.indirectCountBuffer = data.bindlessConfig.terrainCompactCountBuffer; + cmdList->setGraphicsState(gs); + DrawIndexedIndirectCountOrFallback( + cmdList, 0, 0, data.bindlessConfig.terrainObjectCount); + } + } + } + }); + + return passData.depth; +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.h b/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.h new file mode 100644 index 00000000000..2b97d325a31 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.h @@ -0,0 +1,46 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include "Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h" +#include + +namespace xray::render { +class GeometryCollector; +class MaterialCache; +namespace fg { +class RenderDevice; +} +} + +namespace xray::render::framegraph { +class FrameGraph; +} + +namespace xray::render::fg::passes { + +struct DepthPrepassState { + nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::GraphicsPipelineHandle terrainPipeline; + nvrhi::BindingLayoutHandle layout; + nvrhi::BindingLayoutHandle terrainLayout; + nvrhi::InputLayoutHandle inputLayout; + nvrhi::ShaderHandle vs; + nvrhi::ShaderHandle ps; + nvrhi::ShaderHandle terrainPs; + bool initialized = false; +}; + +// Opaque depth fill for early-Z in Forward+. Returns the depth handle written. +framegraph::VirtualResourceHandle setupDepthPrepass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle depthInput, + const GeometryCollector* geometry, + MaterialCache* materialCache, + u32 width, + u32 height, + const BindlessForwardConfig& bindlessConfig, + DepthPrepassState* state); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/DetailCullPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DetailCullPassSetup.cpp index ff119fbdf67..261c0d21be3 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DetailCullPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DetailCullPassSetup.cpp @@ -50,7 +50,8 @@ void setupDetailCullPass( [&, hiZPyramid, hiZWidth, hiZHeight, hiZMipLevels, capturedPrevViewProj, hasPrevViewProj, gpuProfiler, detailState]( FrameGraph& builder, PassHandle passHandle, DetailCullPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); - passBuilder.asyncCompute(); + // Run on graphics queue so CascadedShadows (grass cast) sees fresh instances. + // Async compute has no FG dependency into the shadow pass. passBuilder.sideEffects(); data.device = device; diff --git a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp index 4683dad3129..5ce0ebfd743 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp @@ -53,23 +53,39 @@ DefaultOutputLayout setupDetailPass( const DefaultOutputLayout& forwardInputs, u32 width, u32 height, - xray::profiler::GPUProfiler* gpuProfiler + xray::profiler::GPUProfiler* gpuProfiler, + nvrhi::ITexture* shadowMapArray, + VirtualResourceHandle shadowMapHandle, + nvrhi::ITexture* contactDepth, + nvrhi::ITexture* contactHistory + , VirtualResourceHandle perlinReady + , nvrhi::ITexture* const* shadowCascades + , nvrhi::ITexture* localShadowAtlas ) { if (detailManager && !detailManager->graphicsPipeline) { - nvrhi::FramebufferInfo fbInfo; - fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); - fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); - fbInfo.colorFormats.push_back(nvrhi::Format::RGBA8_UNORM); - fbInfo.colorFormats.push_back(nvrhi::Format::RGBA32_FLOAT); - fbInfo.depthFormat = nvrhi::Format::D32; - detailManager->CreateGraphicsPipeline(device, fbInfo); + // Shaders must be loaded before PSO creation (else "invalid parameters") + if (!detailManager->vertexShader || !detailManager->pixelShader) + { + if (auto* loader = GEnv.Render->GetShaderLoader()) + detailManager->LoadGraphicsShaders(loader); + } + if (detailManager->vertexShader && detailManager->pixelShader) + { + nvrhi::FramebufferInfo fbInfo; + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA8_UNORM); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA32_FLOAT); + fbInfo.depthFormat = nvrhi::Format::D32; + detailManager->CreateGraphicsPipeline(device, fbInfo); + } } auto& passData = fg.addCallbackPass( "DetailDraw", - [&, width, height, gpuProfiler]( + [&, width, height, gpuProfiler, shadowMapArray, shadowMapHandle, contactDepth, contactHistory, perlinReady, shadowCascades, localShadowAtlas]( FrameGraph& builder, PassHandle passHandle, DetailPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); @@ -78,6 +94,12 @@ DefaultOutputLayout setupDetailPass( data.device = device; data.detailManager = detailManager; data.gpuProfiler = gpuProfiler; + data.shadowMapArray = shadowMapArray; + for (int i = 0; i < 3; ++i) + data.shadowCascades[i] = (shadowCascades && shadowCascades[i]) ? shadowCascades[i] : shadowMapArray; + data.localShadowAtlas = localShadowAtlas; + data.contactDepth = contactDepth; + data.contactHistory = contactHistory; data.inputColor = passBuilder.read(forwardInputs.albedo); data.depth = passBuilder.readWrite(forwardInputs.depth, ResourceState::DepthStencilWrite); @@ -87,6 +109,10 @@ DefaultOutputLayout setupDetailPass( data.baseColor = passBuilder.readWrite(forwardInputs.baseColor, ResourceState::RenderTarget); if (forwardInputs.worldPos.is_valid()) data.worldPos = passBuilder.readWrite(forwardInputs.worldPos, ResourceState::RenderTarget); + if (shadowMapHandle.is_valid()) + data.shadowMap = passBuilder.read(shadowMapHandle, ResourceState::ShaderResource); + if (perlinReady.is_valid()) + passBuilder.read(perlinReady); data.outputs.albedo = data.outputColor; data.outputs.normal = data.outputNormal; @@ -145,7 +171,8 @@ DefaultOutputLayout setupDetailPass( fbDesc.addColorAttachment(worldPosRT); fbDesc.setDepthAttachment(depthTexture); - nvrhi::FramebufferHandle framebuffer = data.device->GetNVRHIDevice()->createFramebuffer(fbDesc); + auto framebuffer = framegraph::GetPassResourceCache().GetOrCreateFramebuffer( + "DetailDraw", fbDesc, data.device->GetNVRHIDevice()); if (!framebuffer) return; @@ -157,6 +184,10 @@ DefaultOutputLayout setupDetailPass( auto& cache = framegraph::GetPassResourceCache(); auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), renderDevice); + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } auto detailGlobalsCB = cache.GetOrCreateVolatileCB("Detail", "DetailGlobals", sizeof(FGDetailManager::DetailFrameConstants), renderDevice); auto dynLightCB = cache.GetOrCreateVolatileCB("Detail", "DynLight", 48, renderDevice); @@ -188,6 +219,8 @@ DefaultOutputLayout setupDetailPass( frameConstants.grass_blade_height = ps_r3_grass_blade_height; frameConstants.buildDetailsIndex = dm->buildDetailsBindlessIndex; frameConstants.buildDetailsPbrIndex = dm->buildDetailsPbrBindlessIndex; + frameConstants.grassVeinIndex = dm->grassVeinBindlessIndex; + frameConstants.pad0 = frameConstants.pad1 = frameConstants.pad2 = 0; cmdList->writeBuffer(detailGlobalsCB, &frameConstants, sizeof(frameConstants)); u8 dummyLight[48] = {}; @@ -215,6 +248,28 @@ DefaultOutputLayout setupDetailPass( data.gpuProfiler->BeginPass(cmdList, "Details.Draw"); auto* nvDev = data.device->GetNVRHIDevice(); + nvrhi::ITexture* dummy2D = cache.GetDummyShadowMap2D(nvDev); + + auto bindShadowMap = [&](framegraph::BindingSetBuilder& bsb) { + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = data.shadowCascades[i] ? data.shadowCascades[i] + : (i == 0 && data.shadowMapArray ? data.shadowMapArray : dummy2D); + if (t) + bsb.Texture(kNames[i], t); + } + nvrhi::ITexture* contactHist = data.contactHistory + ? data.contactHistory + : cache.GetDummyContactHistory(nvDev); + if (contactHist) + bsb.Texture("g_ContactHistory", contactHist); + nvrhi::ITexture* localAtlas = data.localShadowAtlas + ? data.localShadowAtlas + : cache.GetDummyShadowMap(nvDev); + if (localAtlas) + bsb.Texture("g_LocalShadowAtlas", localAtlas); + }; auto* shaderLoader = GEnv.Render->GetShaderLoader(); auto* grassVsRefl = shaderLoader->GetCachedReflection("detail_gpu", ".vs"); @@ -233,6 +288,14 @@ DefaultOutputLayout setupDetailPass( bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); + bindShadowMap(bsb); + nvrhi::ITexture* sky0 = nullptr; + nvrhi::ITexture* sky1 = nullptr; + ResolveEnvSkyCubes(data.device, sky0, sky1); + if (!sky0) sky0 = cache.GetDummyCubeMap(nvDev); + if (!sky1) sky1 = cache.GetDummyCubeMap(nvDev); + if (sky0) bsb.Texture("s_env0", sky0); + if (sky1) bsb.Texture("s_env1", sky1); auto bindDesc = bsb.Build(); bindDesc.bindings.push_back(nvrhi::BindingSetItem::TypedBuffer_SRV(32, dm->cachedDummySlotIndirection)); return cache.GetOrCreateBindingSet(bindDesc, dm->graphicsBindingLayout, nvDev); @@ -254,6 +317,14 @@ DefaultOutputLayout setupDetailPass( bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); + bindShadowMap(bsb); + nvrhi::ITexture* sky0 = nullptr; + nvrhi::ITexture* sky1 = nullptr; + ResolveEnvSkyCubes(data.device, sky0, sky1); + if (!sky0) sky0 = cache.GetDummyCubeMap(nvDev); + if (!sky1) sky1 = cache.GetDummyCubeMap(nvDev); + if (sky0) bsb.Texture("s_env0", sky0); + if (sky1) bsb.Texture("s_env1", sky1); return cache.GetOrCreateBindingSet(bsb.Build(), layout, nvDev); }; @@ -314,6 +385,14 @@ DefaultOutputLayout setupDetailPass( decalBsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); decalBsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); decalBsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); + bindShadowMap(decalBsb); + nvrhi::ITexture* sky0 = nullptr; + nvrhi::ITexture* sky1 = nullptr; + ResolveEnvSkyCubes(data.device, sky0, sky1); + if (!sky0) sky0 = cache.GetDummyCubeMap(nvDev); + if (!sky1) sky1 = cache.GetDummyCubeMap(nvDev); + if (sky0) decalBsb.Texture("s_env0", sky0); + if (sky1) decalBsb.Texture("s_env1", sky1); nvrhi::BindingSetHandle decalBindingSet = cache.GetOrCreateBindingSet(decalBsb.Build(), dm->decalBindingLayout, nvDev); nvrhi::GraphicsState state; diff --git a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.h index a4446e7210d..ab96b9984ee 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.h @@ -41,6 +41,12 @@ struct DetailPassData { framegraph::VirtualResourceHandle outputNormal; framegraph::VirtualResourceHandle baseColor; framegraph::VirtualResourceHandle worldPos; + framegraph::VirtualResourceHandle shadowMap; + nvrhi::ITexture* shadowMapArray = nullptr; + nvrhi::ITexture* shadowCascades[3] = {}; + nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* contactDepth = nullptr; + nvrhi::ITexture* contactHistory = nullptr; fg::RenderDevice* device; fg::FGDetailManager* detailManager; framegraph::DefaultOutputLayout outputs; @@ -62,7 +68,14 @@ framegraph::DefaultOutputLayout setupDetailPass( const framegraph::DefaultOutputLayout& forwardInputs, u32 width, u32 height, - xray::profiler::GPUProfiler* gpuProfiler = nullptr + xray::profiler::GPUProfiler* gpuProfiler = nullptr, + nvrhi::ITexture* shadowMapArray = nullptr, + framegraph::VirtualResourceHandle shadowMapHandle = {}, + nvrhi::ITexture* contactDepth = nullptr, + nvrhi::ITexture* contactHistory = nullptr, + framegraph::VirtualResourceHandle perlinReady = {}, + nvrhi::ITexture* const* shadowCascades = nullptr, + nvrhi::ITexture* localShadowAtlas = nullptr ); } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/DofPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DofPassSetup.cpp new file mode 100644 index 00000000000..51a6efe8072 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/DofPassSetup.cpp @@ -0,0 +1,173 @@ +#include "stdafx.h" +#include "DofPassSetup.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/xrRender_console.h" +#include "xrEngine/device.h" + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +struct alignas(16) DofCB +{ + Fvector4 dof_params; + Fvector4 dof_kernel; + Fmatrix m_InvVP; + Fmatrix m_V; +}; +static_assert(sizeof(DofCB) % 16 == 0); + +void InitDof(nvrhi::IDevice* nv, DofPassState& st) +{ + if (st.initialized && st.pipeline && st.layout) + return; + st.initialized = false; + if (!nv) + { + st.initialized = true; + return; + } + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + { + st.initialized = true; + return; + } + auto vs = loader->LoadVertexShader("fullscreen"); + auto ps = loader->LoadPixelShader("dof"); + if (!vs.handle || !ps.handle) + { + st.initialized = true; + return; + } + auto& cache = GetPassResourceCache(); + st.layout = cache.GetOrCreateBindingLayoutFromReflection("Dof_v1", *vs.reflection, *ps.reflection, nv); + if (st.layout) + { + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs.handle); + desc.setPixelShader(ps.handle); + desc.addBindingLayout(st.layout); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.blendState.targets[0].setBlendEnable(false); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + nvrhi::FramebufferInfoEx fb; + fb.addColorFormat(nvrhi::Format::RGBA16_FLOAT); + st.pipeline = cache.GetOrCreatePipeline("Dof_v1", desc, fb, nv); + } + st.initialized = true; +} +} // namespace + +VirtualResourceHandle setupDofPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle sceneColor, + VirtualResourceHandle depth, + u32 width, + u32 height, + DofPassState& state) +{ + if (!device || !device->GetNVRHIDevice()) + return sceneColor; + if (!ps_r2_ls_flags.test(R2FLAG_DOF)) + return sceneColor; + + InitDof(device->GetNVRHIDevice(), state); + if (!state.pipeline || !state.layout || !depth.is_valid()) + return sceneColor; + + ResourceDesc outDesc; + outDesc.type = ResourceDesc::Type::Texture2D; + outDesc.width = width; + outDesc.height = height; + outDesc.format = nvrhi::Format::RGBA16_FLOAT; + outDesc.isRenderTarget = true; + outDesc.isTransient = false; + outDesc.debugName = "rt_DOF"; + auto output = fg.CreateTexture("rt_DOF", outDesc); + + struct PassData + { + VirtualResourceHandle color, depth, output; + DofPassState* st = nullptr; + fg::RenderDevice* device = nullptr; + u32 width = 0, height = 0; + }; + + auto& pd = fg.addCallbackPass( + "DOF", + [&, output](FrameGraph& b, PassHandle ph, PassData& data) { + RenderPassBuilder pb(b, ph); + data.st = &state; + data.device = device; + data.width = width; + data.height = height; + data.color = pb.read(sceneColor, ResourceState::ShaderResource); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.output = pb.write(output, ResourceState::RenderTarget); + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd ? cmd->getDevice() : nullptr; + auto* st = data.st; + if (!cmd || !nv || !st || !st->pipeline) + return; + auto* color = graph.GetPhysicalTexture(data.color); + auto* depthTex = graph.GetPhysicalTexture(data.depth); + auto* out = graph.GetPhysicalTexture(data.output); + if (!color || !depthTex || !out) + return; + + auto& cache = GetPassResourceCache(); + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psR = loader->GetCachedReflection("dof", ".ps"); + if (!vsR || !psR) + return; + + auto* cb = cache.GetOrCreateVolatileCB("DOF", "DofParams", sizeof(DofCB), data.device, 8); + if (!cb) + return; + + DofCB params{}; + params.dof_params.set(ps_r2_dof.x, ps_r2_dof.y, ps_r2_dof.z, ps_r2_dof_sky); + params.dof_kernel.set(0.5f / float(data.width), 0.5f / float(data.height), ps_r2_dof_kernel_size, 0.f); + params.m_InvVP = Device.mInvFullTransform; + params.m_V = Device.mView; + cmd->writeBuffer(cb, ¶ms, sizeof(params)); + + BindingSetBuilder bsb(*vsR, *psR, nv, "DOF"); + bsb.ConstantBuffer("DofParams", cb); + bsb.Texture("g_Color", color).Texture("g_Depth", depthTex); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), st->layout, nv); + if (!set) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(out); + auto fb = cache.GetOrCreateFramebuffer("DofFB", fbDesc, nv); + + nvrhi::GraphicsState gs; + gs.pipeline = st->pipeline; + gs.framebuffer = fb; + gs.bindings = {set}; + gs.viewport.addViewportAndScissorRect(nvrhi::Viewport(float(data.width), float(data.height))); + cmd->setGraphicsState(gs); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); + }); + + return pd.output; +} + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/DofPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/DofPassSetup.h new file mode 100644 index 00000000000..f946059682b --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/DofPassSetup.h @@ -0,0 +1,29 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes +{ + +struct DofPassState +{ + nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::BindingLayoutHandle layout; + bool initialized = false; +}; + +framegraph::VirtualResourceHandle setupDofPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + u32 width, + u32 height, + DofPassState& state); + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/ExposurePassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ExposurePassSetup.cpp index 04754714a4d..9c7198f9a5c 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ExposurePassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ExposurePassSetup.cpp @@ -10,6 +10,7 @@ #include "Layers/xrRender/RenderContext/RenderContext.h" #include "Layers/xrRender/RenderContext/RenderDevice.h" #include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/xrRender_console.h" namespace fg { @@ -20,23 +21,18 @@ namespace xray::render::fg::passes { using namespace framegraph; -// ═══════════════════════════════════════════════════════ -// EXPOSURE CONFIG -// ═══════════════════════════════════════════════════════ - +// Classic binder (phase_luminance): +// amount = R2FLAG_TONEMAP ? ps_r2_tonemap_amount : 0 +// _none=(1,0,1), _full=(middlegray,1,lowlum) +// MiddleGray = lerp(_none,_full,amount) + adaptBlend in .w ExposureConfig GetDefaultExposureConfig() { ExposureConfig config; - config.minLogLuminance = -10.0f; - config.maxLogLuminance = 4.0f; - config.lowPercentile = 0.5f; - config.highPercentile = 0.98f; - config.adaptSpeedUp = 3.0f; - config.adaptSpeedDown = 1.0f; - config.minExposure = 0.001f; - config.maxExposure = 64.0f; - config.exposureCompensation = 0.0f; - config.calibrationConstant = 12.5f; + const bool tonemapOn = ps_r2_ls_flags.test(R2FLAG_TONEMAP); + config.middleGray = ps_r2_tonemap_middlegray; + config.amount = tonemapOn ? ps_r2_tonemap_amount : 0.0f; + config.lowLum = ps_r2_tonemap_low_lum; + config.adaptation = ps_r2_tonemap_adaptation; return config; } @@ -106,6 +102,20 @@ void InitializeExposureResources(fg::RenderDevice* device, ExposurePassState& st state.exposureTexture = nvDevice->createTexture(texDesc); if (!state.exposureTexture) Msg("! [ExposurePass] Failed to create exposure texture"); + else + { + // Seed so tonemap never samples uninitialized 0 (→ crushed black). + nvrhi::CommandListHandle cmd = nvDevice->createCommandList(); + if (cmd) + { + cmd->open(); + float seed = 1.0f; + cmd->writeTexture(state.exposureTexture, 0, 0, &seed, sizeof(float)); + cmd->close(); + nvDevice->executeCommandList(cmd); + } + state.currentExposure = 1.0f; + } } if (state.computeEnabled) { @@ -145,25 +155,33 @@ void InitializeExposureResources(fg::RenderDevice* device, ExposurePassState& st Msg("* [ExposurePass] Initialized (compute=%s)", state.computeEnabled ? "enabled" : "fallback"); } -// ═══════════════════════════════════════════════════════ -// FALLBACK: Fixed exposure calculation -// ═══════════════════════════════════════════════════════ +// Classic CPU-side MiddleGray packing + adaptation blend update +static void FillClassicMiddleGray(const ExposureConfig& config, float deltaTime, ExposurePassState& state, AdaptCB& out) +{ + // f_luminance_adapt = .9 * prev + .1 * dt * adaptation + state.adaptBlend = 0.9f * state.adaptBlend + 0.1f * deltaTime * config.adaptation; + state.adaptBlend = std::clamp(state.adaptBlend, 0.0f, 1.0f); + + Fvector4 none(1.f, 0.f, 1.f, 0.f); + Fvector4 full(config.middleGray, 1.f, config.lowLum, 0.f); + Fvector4 result; + result.lerp(none, full, config.amount); + + out.middleGrayX = result.x; + out.middleGrayY = result.y; + out.middleGrayZ = result.z; + out.middleGrayW = state.adaptBlend; +} static float ComputeFallbackExposure(const ExposureConfig& config, float deltaTime, ExposurePassState& state) { - float targetExposure = 1.0f; - - targetExposure *= std::exp2(config.exposureCompensation); - - targetExposure = std::clamp(targetExposure, config.minExposure, config.maxExposure); - - float adaptSpeed = (targetExposure > state.currentExposure) - ? config.adaptSpeedUp - : config.adaptSpeedDown; - - float adaptFactor = 1.0f - std::exp(-deltaTime * adaptSpeed); - state.currentExposure = std::lerp(state.currentExposure, targetExposure, adaptFactor); - + // No scene: hold at 1 (classic scale with amount=0) + AdaptCB cb{}; + FillClassicMiddleGray(config, deltaTime, state, cb); + const float Lw = 1.0f; + float scale = cb.middleGrayX / std::max(Lw * cb.middleGrayY + cb.middleGrayZ, 1e-6f); + scale = std::clamp(scale, 1.f / 128.f, 20.f); + state.currentExposure = std::lerp(state.currentExposure, scale, cb.middleGrayW); return state.currentExposure; } @@ -183,17 +201,26 @@ ExposureOutput setupExposurePass( { InitializeExposureResources(device, state); - // Create exposure texture resource in framegraph + if (!state.exposureTexture) + { + ExposureOutput empty{}; + return empty; + } + + // Import the persistent 1×1 exposure texture — adapt CS writes here and + // tonemap must read the SAME resource (CreateTexture was a separate empty RT). ResourceDesc exposureDesc; exposureDesc.type = ResourceDesc::Type::Texture2D; exposureDesc.debugName = "Exposure"; exposureDesc.width = 1; exposureDesc.height = 1; exposureDesc.format = nvrhi::Format::R32_FLOAT; - exposureDesc.isRenderTarget = false; exposureDesc.isUAV = true; + exposureDesc.allowUAV = true; + exposureDesc.isImported = true; - VirtualResourceHandle exposureHandle = fg.CreateTexture("exposure_rt", exposureDesc); + VirtualResourceHandle exposureHandle = fg.ImportTexture( + "Exposure", state.exposureTexture.Get(), exposureDesc); // Create histogram buffer resource ResourceDesc histogramDesc; @@ -248,9 +275,10 @@ ExposureOutput setupExposurePass( ctx->ClearBufferUint(ps->histogramBuffer.Get(), 0); { + // Fixed log range matching luminance_histogram.cs / exposure_adapt.cs HistogramCB histCB; - histCB.minLogLum = data.config.minLogLuminance; - histCB.logLumRange = data.config.maxLogLuminance - data.config.minLogLuminance; + histCB.minLogLum = -10.0f; + histCB.logLumRange = 14.0f; histCB.width = data.width; histCB.height = data.height; @@ -274,19 +302,8 @@ ExposureOutput setupExposurePass( } { - AdaptCB adaptCB; - adaptCB.minLogLum = data.config.minLogLuminance; - adaptCB.logLumRange = data.config.maxLogLuminance - data.config.minLogLuminance; - adaptCB.lowPercentile = data.config.lowPercentile; - adaptCB.highPercentile = data.config.highPercentile; - adaptCB.adaptSpeedUp = data.config.adaptSpeedUp; - adaptCB.adaptSpeedDown = data.config.adaptSpeedDown; - adaptCB.deltaTime = data.deltaTime; - adaptCB.exposureCompensation = data.config.exposureCompensation; - adaptCB.minExposure = data.config.minExposure; - adaptCB.maxExposure = data.config.maxExposure; - adaptCB.calibrationConstant = data.config.calibrationConstant; - adaptCB.padding = 0.0f; + AdaptCB adaptCB{}; + FillClassicMiddleGray(data.config, data.deltaTime, *ps, adaptCB); cmdList->writeBuffer(adaptCBHandle, &adaptCB, sizeof(adaptCB)); diff --git a/src/Layers/xrRender/FrameGraphPasses/ExposurePassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ExposurePassSetup.h index 874c52b48ca..6a818b4f800 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ExposurePassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/ExposurePassSetup.h @@ -5,7 +5,6 @@ #include "Layers/xrRender/FrameGraph/FGResource.h" #include -// Forward declarations namespace xray::render { namespace fg { class RenderDevice; @@ -18,29 +17,10 @@ namespace xray::render::framegraph { namespace xray::render::fg::passes { -// ═══════════════════════════════════════════════════════ -// EXPOSURE PASS (Auto-Exposure / Eye Adaptation) -// ═══════════════════════════════════════════════════════ -// -// Computes scene exposure for HDR rendering using histogram-based -// auto-exposure with temporal eye adaptation. -// -// PIPELINE: -// 1. Luminance Histogram - Compute shader generates 64-bin histogram -// from HDR scene in log2 luminance space -// 2. Exposure Adaptation - Compute shader analyzes histogram, -// skips extreme values, computes target exposure, applies -// temporal smoothing for eye adaptation effect -// -// OUTPUT: -// - 1x1 R32_FLOAT texture containing exposure value -// - Sky pass reads this via s_tonemap.Load(int3(0,0,0)).x -// - Tonemap pass uses same exposure for HDR->LDR conversion -// -// REFERENCES: -// - Krzysztof Narkowicz: "Automatic Exposure" (2016) -// - Epic Games: "Auto Exposure in UE 4.25" (2020) -// - Hillaire: "A Scalable and Production Ready Sky and Atmosphere" (2020) +// Classic CoP auto-exposure (MiddleGray / bloom_luminance_3): +// scale = middlegray.x / (Lw * middlegray.y + middlegray.z) +// out = lerp(prev, scale, adaptBlend); clamp [1/128, 20] +// Wired to r2_tonemap* console vars. struct ExposurePassState { nvrhi::BufferHandle histogramBuffer; @@ -52,28 +32,14 @@ struct ExposurePassState { bool initialized = false; bool computeEnabled = false; float currentExposure = 1.0f; + float adaptBlend = 0.5f; // classic f_luminance_adapt init }; struct ExposureConfig { - // Histogram parameters - float minLogLuminance = -10.0f; // Minimum log2 luminance (EV) - float maxLogLuminance = 4.0f; // Maximum log2 luminance (EV) - - // Percentile clamping (skip extreme values) - float lowPercentile = 0.5f; // Skip darkest 50% of pixels - float highPercentile = 0.98f; // Skip brightest 2% of pixels - - // Eye adaptation speed (f-stops per second) - float adaptSpeedUp = 3.0f; // Speed when brightening - float adaptSpeedDown = 1.0f; // Speed when darkening (slower) - - // Exposure limits - float minExposure = 0.001f; // Minimum exposure value - float maxExposure = 64.0f; // Maximum exposure value - - // Calibration - float exposureCompensation = 0.0f; // Manual EV adjustment - float calibrationConstant = 12.5f; // Reflected-light meter constant K + float middleGray = 1.0f; + float amount = 0.7f; // 0 when r2_tonemap off + float lowLum = 0.0001f; + float adaptation = 1.0f; // r2_tonemap_adaptation }; struct ExposurePassData { @@ -88,16 +54,13 @@ struct ExposurePassData { ExposurePassState* passState; }; -// Output handles from exposure pass struct ExposureOutput { - framegraph::VirtualResourceHandle exposureTexture; // 1x1 R32_FLOAT - framegraph::VirtualResourceHandle histogramBuffer; // 64 u32 bins (for debug) + framegraph::VirtualResourceHandle exposureTexture; + framegraph::VirtualResourceHandle histogramBuffer; }; void InitializeExposureResources(fg::RenderDevice* device, ExposurePassState& state); -// Setup the exposure pass -// Returns handle to 1x1 exposure texture ExposureOutput setupExposurePass( framegraph::FrameGraph& fg, fg::RenderDevice* device, @@ -109,6 +72,7 @@ ExposureOutput setupExposurePass( ExposurePassState& state ); +// Builds config from r2_tonemap* / R2FLAG_TONEMAP (classic binder). ExposureConfig GetDefaultExposureConfig(); nvrhi::ITexture* GetExposureTexture(const ExposurePassState& state); diff --git a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp index d7f45e43325..8ef03a484a6 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp @@ -22,12 +22,16 @@ #include "Layers/xrRender/FrameGraph/PassResourceCache.h" #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" #include "PassCommon.h" +#include "xrCDB/Frustum.h" #include "Layers/xrRender/ClusteredLightManager.h" +#include "Layers/xrRender/r_FrameGraphRenderer.h" +#include "Layers/xrRender/xrRender_console.h" #include "xrCore/FMesh.hpp" namespace xray::render::fg { extern float r__dtex_range; + extern xray::render::FrameGraphRenderer RImplementation; } namespace xray::render::fg::passes { @@ -58,7 +62,7 @@ void InitializeForwardResources(fg::RenderDevice* device, const nvrhi::Framebuff auto& cache = framegraph::GetPassResourceCache(); - state.bindlessLayout = cache.GetOrCreateBindingLayoutFromReflection("ForwardColor", *vsResult.reflection, *psResult.reflection, nvDevice); + state.bindlessLayout = cache.GetOrCreateBindingLayoutFromReflection("ForwardColor_PBR_v3_CSMLadder", *vsResult.reflection, *psResult.reflection, nvDevice); u32 attrCount = 0; auto* attrs = GetUnifiedVertexAttributes(attrCount); @@ -91,19 +95,58 @@ void InitializeForwardResources(fg::RenderDevice* device, const nvrhi::Framebuff pipeDesc.renderState.rasterState.frontCounterClockwise = false; pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; - state.bindlessPipeline = cache.GetOrCreatePipeline("ForwardColor", pipeDesc, fbInfo, nvDevice); + state.bindlessPipeline = cache.GetOrCreatePipeline("ForwardColor_PBR_v3_CSMLadder", pipeDesc, fbInfo, nvDevice); if (!state.bindlessPipeline) { Msg("! [BindlessForward] Failed to create pipeline"); return; } + // After DepthPrepass: Equal + no write → Hi-Z rejects overdraw, skips redundant depth stores + { + nvrhi::GraphicsPipelineDesc equalDesc = pipeDesc; + equalDesc.renderState.depthStencilState.depthWriteEnable = false; + equalDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::Equal; + state.bindlessEqualPipeline = cache.GetOrCreatePipeline( + "ForwardColor_PBR_Equal_v1", equalDesc, fbInfo, nvDevice); + } + QueryBindingLayoutFromPipeline(state.bindlessPipeline, state.bindlessLayout); + // Optional tessellation PSO (PatchList). + // Apple/MoltenVK: do not create — Metal tess temp buffers can kernel-panic. +#if !defined(XR_PLATFORM_APPLE) + { + auto hsResult = shaderLoader->LoadHullShader("bindless_tess", "main"); + auto dsResult = shaderLoader->LoadDomainShader("bindless_tess", "main"); + if (hsResult.handle && dsResult.handle) { + state.bindlessHS = hsResult.handle; + state.bindlessDS = dsResult.handle; + + nvrhi::GraphicsPipelineDesc tessDesc = pipeDesc; + tessDesc.HS = state.bindlessHS; + tessDesc.DS = state.bindlessDS; + tessDesc.primType = nvrhi::PrimitiveType::PatchList; + tessDesc.patchControlPoints = 3; + tessDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + state.bindlessTessPipeline = cache.GetOrCreatePipeline( + "ForwardColor_Tess_v5", tessDesc, fbInfo, nvDevice); + if (state.bindlessTessPipeline) + Msg("* [BindlessForward] Tessellation pipeline ready"); + else + Msg("! [BindlessForward] Tessellation pipeline create failed (VkResult logged by NVRHI)"); + } else { + Msg("! [BindlessForward] Tess HS/DS shaders missing — tessellation disabled"); + } + } +#else + Msg("* [BindlessForward] Tessellation disabled on Apple/MoltenVK (IOGPU safety)"); +#endif + auto terrainPsResult = shaderLoader->LoadPixelShader("bindless_terrain", "main"); if (terrainPsResult.handle) { state.terrainPS = terrainPsResult.handle; state.terrainLayout = cache.GetOrCreateBindingLayoutFromReflection( - "ForwardColor_Terrain", *vsResult.reflection, *terrainPsResult.reflection, nvDevice); + "ForwardColor_Terrain_PBR_v3_CSMLadder", *vsResult.reflection, *terrainPsResult.reflection, nvDevice); if (state.terrainLayout) { nvrhi::GraphicsPipelineDesc terrainPipeDesc; @@ -120,9 +163,16 @@ void InitializeForwardResources(fg::RenderDevice* device, const nvrhi::Framebuff terrainPipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; terrainPipeDesc.renderState.rasterState.frontCounterClockwise = false; terrainPipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; - state.terrainPipeline = cache.GetOrCreatePipeline("ForwardColor_Terrain", terrainPipeDesc, fbInfo, nvDevice); + state.terrainPipeline = cache.GetOrCreatePipeline("ForwardColor_Terrain_PBR_v3_CSMLadder", terrainPipeDesc, fbInfo, nvDevice); if (state.terrainPipeline) + { + nvrhi::GraphicsPipelineDesc terrainEqual = terrainPipeDesc; + terrainEqual.renderState.depthStencilState.depthWriteEnable = false; + terrainEqual.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::Equal; + state.terrainEqualPipeline = cache.GetOrCreatePipeline( + "ForwardColor_Terrain_Equal_v2_CSMLadder", terrainEqual, fbInfo, nvDevice); state.terrainInitialized = true; + } } } @@ -182,6 +232,11 @@ static void renderBindlessForward( auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), device); auto drawIndexBuffer = GetOrCreateDrawIndexBuffer("ForwardColor", nvDevice); + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + auto lightingData = FillLightingConstants(); cmdList->writeBuffer(lightingCB, &lightingData, sizeof(lightingData)); @@ -192,6 +247,49 @@ static void renderBindlessForward( auto* psReflection = shaderLoader->GetCachedReflection("bindless_forward", ".ps"); auto& clm = ClusteredLightManager::Instance(); + auto& passCache = framegraph::GetPassResourceCache(); + + nvrhi::ITexture* shadowTex = config.shadowCascades[0] + ? config.shadowCascades[0] + : (config.shadowMapArray ? config.shadowMapArray : nullptr); + if (!shadowTex) + shadowTex = passCache.GetDummyShadowMap2D(nvDevice); + nvrhi::ITexture* sky0 = config.envSky0 + ? config.envSky0 + : passCache.GetDummyCubeMap(nvDevice); + nvrhi::ITexture* sky1 = config.envSky1 + ? config.envSky1 + : passCache.GetDummyCubeMap(nvDevice); + + auto bindShadowMap = [&](framegraph::BindingSetBuilder& bsb) { + nvrhi::ITexture* dummy2D = passCache.GetDummyShadowMap2D(nvDevice); + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = (config.shadowCascades[i]) ? config.shadowCascades[i] + : (i == 0 && shadowTex ? shadowTex : dummy2D); + if (t) + bsb.Texture(kNames[i], t); + } + // Bind by name — unused g_ContactDepth must not be forced into the set + nvrhi::ITexture* contactHist = config.contactHistory + ? config.contactHistory + : passCache.GetDummyContactHistory(nvDevice); + if (contactHist) + bsb.Texture("g_ContactHistory", contactHist); + nvrhi::ITexture* localAtlas = config.localShadowAtlas + ? config.localShadowAtlas + : passCache.GetDummyShadowMap(nvDevice); + if (localAtlas) + bsb.Texture("g_LocalShadowAtlas", localAtlas); + }; + + auto bindSkyCubes = [&](framegraph::BindingSetBuilder& bsb) { + if (sky0) + bsb.Texture("s_env0", sky0); + if (sky1) + bsb.Texture("s_env1", sky1); + }; auto createBindingSetForSet = [&](const BindlessDrawSet& set) -> nvrhi::BindingSetHandle { framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "ForwardColor"); @@ -203,8 +301,13 @@ static void renderBindlessForward( bsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); bsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); + bindShadowMap(bsb); + bindSkyCubes(bsb); - return framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), ps.bindlessLayout, nvDevice); + auto bs = passCache.GetOrCreateBindingSet(bsb.Build(), ps.bindlessLayout, nvDevice); + if (!bs) + Msg("! [ForwardColor] Binding set create failed (missing CSM/sky?)"); + return bs; }; // ═══════════════════════════════════════════════════════ @@ -230,8 +333,13 @@ static void renderBindlessForward( return; } + // Equal-depth after prepass was rejected: VS path mismatch (depth vs forward) + // fails ComparisonFunc::Equal on MoltenVK → black indoor geometry / no torch. + // Keep LessOrEqual; DepthPrepass still fills Hi-Z for overdraw rejection. + nvrhi::IGraphicsPipeline* opaquePipe = ps.bindlessPipeline.Get(); + nvrhi::GraphicsState state; - state.pipeline = ps.bindlessPipeline; + state.pipeline = opaquePipe; state.framebuffer = framebuffer; // SM6.6 bindless: Add the descriptor table from D3D12 backend @@ -261,7 +369,11 @@ static void renderBindlessForward( return; auto bindingSet = createBindingSetForSet(set); - R_ASSERT2(bindingSet, "Bindless forward binding set creation failed"); + if (!bindingSet) + { + Msg("! [ForwardColor] Skipping draw — binding set is null"); + return; + } state.bindings = { bindingSet }; if (bindlessTable) { @@ -277,7 +389,7 @@ static void renderBindlessForward( if (config.variantPartition.Enabled()) { auto* backendDev = device->GetBackend(); VariantPartitionDrawConfig vpCfg; - vpCfg.defaultPipeline = ps.bindlessPipeline.Get(); + vpCfg.defaultPipeline = opaquePipe; vpCfg.inputLayout = ps.bindlessInputLayout; vpCfg.passLayout = ps.bindlessLayout; vpCfg.bindlessLayout = backendDev ? backendDev->GetBindlessLayout() : nullptr; @@ -289,6 +401,18 @@ static void renderBindlessForward( vpCfg.variantTexBuffer = variantTexBuffer.GetBuffer(); vpCfg.instanceBuffer = config.staticSet.instanceBuffer; vpCfg.megaVertexBuffer = config.megaVertexBuffer; + vpCfg.shadowMapArray = shadowTex; + for (u32 i = 0; i < 3; ++i) + vpCfg.shadowCascades[i] = config.shadowCascades[i]; + vpCfg.localShadowAtlas = config.localShadowAtlas; + vpCfg.envSky0 = sky0; + vpCfg.envSky1 = sky1; + { + auto& clmVp = ClusteredLightManager::Instance(); + vpCfg.lightDataBuffer = clmVp.GetLightDataBuffer(); + vpCfg.clusterGridBuffer = clmVp.GetClusterGridBuffer(); + vpCfg.lightIndexListBuffer = clmVp.GetLightIndexListBuffer(); + } vpCfg.partition = config.variantPartition; vpCfg.selectTransparent = false; @@ -304,6 +428,67 @@ static void renderBindlessForward( } drawSet(config.dynamicSet); + // Tessellation: PatchList PSO + direct drawIndexed (CPU frustum). + // Avoid DrawIndexedIndirect+tess — MoltenVK freezes on some camera angles. + // Apple/MoltenVK: hard-disabled — Metal tess temp buffers can kernel-panic + // (IOGPUGroupMemory::remove_memory_object) regardless of the console flag. +#if defined(XR_PLATFORM_APPLE) + const bool tessEnabled = false; +#else + const bool tessEnabled = ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION); +#endif + if (tessEnabled && ps.bindlessTessPipeline && + config.tessObjectCount > 0 && config.tessDrawArgs && config.tessObjects && + config.tessMaterialIDBuffer && config.tessBatchIndicesBuffer && config.tessInstanceBuffer) + { + framegraph::BindingSetBuilder tessBsb(*vsReflection, *psReflection, nvDevice, "ForwardColor.Tess"); + tessBsb.ConstantBuffer("static_globals", staticGlobalsCB); + tessBsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + tessBsb.BufferSRV("g_InstanceData", config.tessInstanceBuffer); + tessBsb.BufferSRV("g_CompactBatchIndices", config.tessBatchIndicesBuffer); + tessBsb.BufferSRV("g_CompactMaterialIDs", config.tessMaterialIDBuffer); + tessBsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); + tessBsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); + tessBsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); + bindShadowMap(tessBsb); + bindSkyCubes(tessBsb); + + auto tessBindingSet = passCache.GetOrCreateBindingSet(tessBsb.Build(), ps.bindlessLayout, nvDevice); + if (tessBindingSet) + { + state.pipeline = ps.bindlessTessPipeline; + state.bindings = { tessBindingSet }; + if (bindlessTable) + state.addBindingSet(bindlessTable); + state.indirectParams = nullptr; + state.indirectCountBuffer = nullptr; + cmdList->setGraphicsState(state); + + // No CPU frustum here: bounds for some model batches are unreliable, + // and exclusive tess routing would hide the whole mesh if culled. + for (u32 i = 0; i < config.tessObjectCount; ++i) + { + const auto& args = config.tessDrawArgs[i]; + if (args.indexCountPerInstance == 0) + continue; + + nvrhi::DrawArguments da; + da.vertexCount = args.indexCountPerInstance; + da.instanceCount = 1; + da.startIndexLocation = args.startIndexLocation; + da.startVertexLocation = args.baseVertexLocation; + da.startInstanceLocation = i; + cmdList->drawIndexed(da); + } + + state.pipeline = opaquePipe; + } + else + { + Msg("! [ForwardColor] Tess binding set create failed — skip tess draws"); + } + } + // ═══════════════════════════════════════════════════════ // TERRAIN RENDERING (4-layer detail blending) // ═══════════════════════════════════════════════════════ @@ -335,36 +520,40 @@ static void renderBindlessForward( terrainBsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); terrainBsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); terrainBsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); + bindShadowMap(terrainBsb); + bindSkyCubes(terrainBsb); auto terrainBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(terrainBsb.Build(), ps.terrainLayout, nvDevice); - R_ASSERT2(terrainBindingSet, "Terrain binding set creation failed"); - - // Set up terrain graphics state - nvrhi::GraphicsState terrainState; - terrainState.pipeline = ps.terrainPipeline; - terrainState.framebuffer = framebuffer; - terrainState.bindings = { terrainBindingSet }; - - // Add bindless descriptor table - if (backend) { - auto* bindlessTable = backend->GetBindlessDescriptorTable(); - if (bindlessTable) - terrainState.addBindingSet(bindlessTable); + if (!terrainBindingSet) + { + Msg("! [ForwardColor] Terrain binding set create failed"); + } + else + { + nvrhi::GraphicsState terrainState; + terrainState.pipeline = ps.terrainPipeline.Get(); + terrainState.framebuffer = framebuffer; + terrainState.bindings = { terrainBindingSet }; + + if (backend) { + auto* bindlessTable = backend->GetBindlessDescriptorTable(); + if (bindlessTable) + terrainState.addBindingSet(bindlessTable); + } + + terrainState.vertexBuffers = { + {config.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0} + }; + terrainState.indexBuffer = { config.megaIndexBuffer, nvrhi::Format::R32_UINT, 0 }; + terrainState.indirectParams = config.terrainCompactDrawArgsBuffer; + terrainState.indirectCountBuffer = config.terrainCompactCountBuffer; + terrainState.viewport.addViewport(viewport); + terrainState.viewport.addScissorRect(nvrhi::Rect(rtDesc.width, rtDesc.height)); + + cmdList->setGraphicsState(terrainState); + DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, config.terrainObjectCount); } - - terrainState.vertexBuffers = { - {config.megaVertexBuffer, 0, 0}, - {drawIndexBuffer, 1, 0} - }; - terrainState.indexBuffer = { config.megaIndexBuffer, nvrhi::Format::R32_UINT, 0 }; - terrainState.indirectParams = config.terrainCompactDrawArgsBuffer; - terrainState.indirectCountBuffer = config.terrainCompactCountBuffer; - terrainState.viewport.addViewport(viewport); - terrainState.viewport.addScissorRect(nvrhi::Rect(rtDesc.width, rtDesc.height)); - - cmdList->setGraphicsState(terrainState); - DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, config.terrainObjectCount); - } } @@ -429,6 +618,13 @@ framegraph::DefaultOutputLayout setupForwardColorPass( data.drawArgsBuffer = passBuilder.read(drawArgsInput, ResourceState::IndirectArgument); } + if (bindlessConfig.shadowMapHandle.is_valid()) { + passBuilder.read(bindlessConfig.shadowMapHandle, ResourceState::ShaderResource); + } + if (bindlessConfig.localShadowHandle.is_valid()) { + passBuilder.read(bindlessConfig.localShadowHandle, ResourceState::ShaderResource); + } + data.outputs.albedo = data.color; data.outputs.normal = data.normal; data.outputs.baseColor = data.baseColor; @@ -454,7 +650,9 @@ framegraph::DefaultOutputLayout setupForwardColorPass( nvrhi::ICommandList* cmdList = ctx->GetCommandList(); if (cmdList) { - cmdList->clearDepthStencilTexture(depthRT, nvrhi::AllSubresources, true, 1.0f, false, 0); + // DepthPrepass already cleared+filled depth — clearing here would kill early-Z. + if (!ps_r_depth_prepass) + cmdList->clearDepthStencilTexture(depthRT, nvrhi::AllSubresources, true, 1.0f, false, 0); if (normalRT) cmdList->clearTextureFloat(normalRT, nvrhi::AllSubresources, nvrhi::Color(0.0f)); if (baseColorRT) diff --git a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h index a182dec8723..1de5355f9a5 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h @@ -13,6 +13,8 @@ namespace xray::render { class MaterialCache; namespace fg { class RenderDevice; + struct IndirectDrawArgs; + struct GPUObjectData; } } @@ -51,17 +53,57 @@ struct BindlessDrawSet { nvrhi::IBuffer* compactCountBuffer = nullptr; // Visible draw count u32 totalObjectCount = 0; // Max draw count + // View-independent (unculled) caster buffers for the shadow pass. Draw args is the + // full per-batch template (instanceCount=1), batch indices is an identity map, and + // material IDs is the full per-batch array. Used so shadows include casters outside + // the camera frustum. + nvrhi::IBuffer* castAllDrawArgsBuffer = nullptr; + nvrhi::IBuffer* castAllBatchIndicesBuffer = nullptr; + nvrhi::IBuffer* castAllMaterialIDBuffer = nullptr; + nvrhi::IBuffer* castAllCountBuffer = nullptr; + + // Light-frustum compacted CSM casters (filled per cascade by BuildLightFrustumCasters) + nvrhi::IBuffer* lightCullDrawArgsBuffer = nullptr; + nvrhi::IBuffer* lightCullBatchIndicesBuffer = nullptr; + nvrhi::IBuffer* lightCullMaterialIDBuffer = nullptr; + nvrhi::IBuffer* lightCullCountBuffer = nullptr; + bool IsValid() const { return compactDrawArgsBuffer && compactMaterialIDBuffer && compactBatchIndicesBuffer && instanceBuffer && compactCountBuffer && totalObjectCount > 0; } + + bool IsCastAllValid() const { + return castAllDrawArgsBuffer && castAllBatchIndicesBuffer && + castAllMaterialIDBuffer && castAllCountBuffer && instanceBuffer && + totalObjectCount > 0; + } + + bool IsLightCullValid() const { + return lightCullDrawArgsBuffer && lightCullBatchIndicesBuffer && + lightCullMaterialIDBuffer && lightCullCountBuffer && instanceBuffer && + totalObjectCount > 0; + } }; struct BindlessForwardConfig { BindlessDrawSet staticSet; BindlessDrawSet dynamicSet; + // Transparent (alpha-blended) set, used ONLY as a foliage shadow caster. + // Populated with the view-independent cast-all buffers so trees cast into the CSM. + BindlessDrawSet transparentCasterSet; + + // Tessellation: direct drawIndexed + CPU frustum (not MDI/PatchList). + // DrawIndexedIndirect + tess hangs / freezes on MoltenVK for some views. + nvrhi::IBuffer* tessMaterialIDBuffer = nullptr; + nvrhi::IBuffer* tessBatchIndicesBuffer = nullptr; // identity 0..N-1 + nvrhi::IBuffer* tessInstanceBuffer = nullptr; + const xray::render::fg::IndirectDrawArgs* tessDrawArgs = nullptr; + const xray::render::fg::GPUObjectData* tessObjects = nullptr; + u32 tessObjectCount = 0; + // Enable bindless rendering mode bool enabled = false; @@ -108,18 +150,44 @@ struct BindlessForwardConfig { return terrainObjectCount > 0 && (terrainDrawArgsBuffer || terrainCompactDrawArgsBuffer); } + // Cascaded shadow maps (bound in forward when non-null) + nvrhi::ITexture* shadowMapArray = nullptr; // cascade 0 @ t23 + nvrhi::ITexture* shadowCascades[3] = {}; // c0/c1/c2 @ t23/t29/t30 + framegraph::VirtualResourceHandle shadowMapHandle; + + // Dedicated HUD shadow map (t24); null = dummy + nvrhi::ITexture* hudShadowMap = nullptr; + + // Local spot/OMNIPART shadow atlas (t27); null = dummy array + nvrhi::ITexture* localShadowAtlas = nullptr; + framegraph::VirtualResourceHandle localShadowHandle; + + // Previous-frame depth for screen-space contact shadows; null = disabled + nvrhi::ITexture* contactDepth = nullptr; + // Previous-frame temporal contact factor (t28); null = dummy white + nvrhi::ITexture* contactHistory = nullptr; + + // Sky cubemaps for water reflections (t25/t26) + nvrhi::ITexture* envSky0 = nullptr; + nvrhi::ITexture* envSky1 = nullptr; + VariantPartitionConfig variantPartition; }; struct ForwardColorPassState { nvrhi::GraphicsPipelineHandle bindlessPipeline; + nvrhi::GraphicsPipelineHandle bindlessEqualPipeline; // after depth prepass: Equal, no depth write + nvrhi::GraphicsPipelineHandle bindlessTessPipeline; nvrhi::BindingLayoutHandle bindlessLayout; nvrhi::InputLayoutHandle bindlessInputLayout; nvrhi::SamplerHandle linearSampler; nvrhi::ShaderHandle bindlessVS; nvrhi::ShaderHandle bindlessPS; + nvrhi::ShaderHandle bindlessHS; + nvrhi::ShaderHandle bindlessDS; bool bindlessInitialized = false; nvrhi::GraphicsPipelineHandle terrainPipeline; + nvrhi::GraphicsPipelineHandle terrainEqualPipeline; nvrhi::BindingLayoutHandle terrainLayout; nvrhi::ShaderHandle terrainPS; bool terrainInitialized = false; diff --git a/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.cpp new file mode 100644 index 00000000000..5833b427e9d --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.cpp @@ -0,0 +1,522 @@ +#include "stdafx.h" +#include "LocalShadowPassSetup.h" +#include "PassCommon.h" +#include "ShaderConstants.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/Geometry/MaterialCache.h" +#include "Layers/xrRender/Bindless/MaterialBuffer.h" +#include "Layers/xrRender/Bindless/TerrainMaterialBuffer.h" +#include "Layers/xrRender/ResourceManager/FGResourceManager.h" +#include "Layers/xrRender/ResourceManager/TextureManager.h" +#include "Layers/xrRender/ResourceManager/NativeRTFactory.h" +#include "Layers/xrRender/xrRender_console.h" +#include "Layers/xrRender/GPUCullingManager.h" +#include "Layers/xrRender/light.h" +#include "Layers/xrRender/SkeletonCustom.h" +#include "Layers/xrRender/FSkinned.h" +#include "Layers/xrRender/SkeletonX.h" +#include "Layers/xrRender/r_FrameGraphRenderer.h" +#include "xrEngine/IRenderBackend.h" +#include "xrEngine/device.h" + +namespace xray::render::fg::passes +{ + +namespace +{ + +u32 LocalShadowResolution() +{ + // Prefer sharper long floor shadows (beds/tables). Cap at 2048. + const u32 half = std::max(ps_r2_smapsize / 2u, 512u); + return std::min(half, 2048u); +} + +} // namespace + +void InitializeLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& state) +{ + if (!device) + return; + + const u32 res = LocalShadowResolution(); + const u32 slices = MAX_LOCAL_SHADOW_TILES; + if (state.initialized && state.enabled && state.resolution == res && + state.sliceCount == slices && state.atlas) + return; + + if (state.initialized) + ShutdownLocalShadowPass(device, state); + + // Pipelines are resolution-independent but clear so bias-tuned ones rebuild cleanly. + state.opaquePipeline = nullptr; + state.foliagePipeline = nullptr; + + auto* resMgr = device->GetFGResourceManager(); + if (!resMgr || !resMgr->GetRTFactory() || !resMgr->GetTextureManager()) + { + state.initialized = true; + state.enabled = false; + return; + } + + state.resolution = res; + state.sliceCount = slices; + state.atlasHandle = resMgr->GetRTFactory()->CreateCascadedShadowMap( + res, slices, true, "rt_LocalShadowAtlas"); + state.atlas = resMgr->GetTextureManager()->GetNVRHITexture(state.atlasHandle); + state.initialized = true; + state.enabled = (state.atlas != nullptr); + if (state.enabled) + Msg("* [LocalShadow] Init: OK (%ux%u × %u slices)", res, res, slices); + else + Msg("! [LocalShadow] Failed to create local shadow atlas"); +} + +void ShutdownLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& state) +{ + if (device && device->GetFGResourceManager() && device->GetFGResourceManager()->GetRTFactory()) + { + if (state.atlasHandle.IsValid()) + device->GetFGResourceManager()->GetRTFactory()->ReleaseRenderTarget(state.atlasHandle); + } + state.atlasHandle = {}; + state.atlas = nullptr; + state.opaquePipeline = nullptr; + state.foliagePipeline = nullptr; + state.resolution = 0; + state.sliceCount = 0; + state.initialized = false; + state.enabled = false; +} + +LocalShadowOutputs setupLocalShadowPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + const BindlessForwardConfig& bindlessConfig, + ShadowPassState& shadowState, + LocalShadowPassState& state, + const xr_vector* worldSkinnedBatches, + fg::GPUCullingManager* gpuCulling) +{ + using namespace framegraph; + LocalShadowOutputs outputs; + outputs.valid = false; + + if (!device) + return outputs; + + auto& clm = ClusteredLightManager::Instance(); + const auto& tiles = clm.GetLocalShadowTiles(); + if (ps_r_local_shadows == 0 || tiles.empty()) + return outputs; + + InitializeLocalShadowPass(device, state); + if (!state.enabled || !state.atlas || !shadowState.enabled || !shadowState.pipeline) + return outputs; + + ResourceDesc desc; + desc.type = ResourceDesc::Type::Texture2D; + desc.width = state.resolution; + desc.height = state.resolution; + desc.depth = 1; + desc.arraySize = state.sliceCount; + desc.format = nvrhi::Format::D32; + desc.isDepthStencil = true; + desc.isImported = true; + desc.debugName = "rt_LocalShadowAtlas"; + + auto atlasHandle = fg.ImportTexture("rt_LocalShadowAtlas", state.atlas, desc); + + struct PassData + { + VirtualResourceHandle atlas; + LocalShadowPassState* localState = nullptr; + ShadowPassState* shadowState = nullptr; + BindlessForwardConfig bindlessConfig; + fg::RenderDevice* device = nullptr; + fg::GPUCullingManager* gpuCulling = nullptr; + const xr_vector* worldSkinnedBatches = nullptr; + xr_vector tiles; + }; + + auto& passData = fg.addCallbackPass( + "LocalShadows", + [&, atlasHandle, worldSkinnedBatches, gpuCulling](FrameGraph& builder, PassHandle passHandle, PassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.atlas = pb.write(atlasHandle, ResourceState::DepthStencilWrite); + data.localState = &state; + data.shadowState = &shadowState; + data.bindlessConfig = bindlessConfig; + data.device = device; + data.gpuCulling = gpuCulling; + data.worldSkinnedBatches = worldSkinnedBatches; + data.tiles = tiles; + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + ZoneScopedN("LocalShadows::Execute"); + if (!data.localState || !data.shadowState || data.tiles.empty()) + return; + auto* atlasTex = graph.GetPhysicalTexture(data.atlas); + if (!atlasTex) + atlasTex = data.localState->atlas; + nvrhi::ICommandList* cmd = ctx->GetCommandList(); + nvrhi::IDevice* nv = cmd ? cmd->getDevice() : nullptr; + if (!cmd || !nv || !atlasTex) + return; + + ShadowPassState& st = *data.shadowState; + LocalShadowPassState& ls = *data.localState; + + if (!data.bindlessConfig.UseGPUCulling() || !data.bindlessConfig.UseMegaBuffers()) + { + static bool s_warned = false; + if (!s_warned) + { + Msg("! [LocalShadow] GPU bindless culling unavailable — atlas empty"); + s_warned = true; + } + return; + } + + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* vsRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".vs"); + auto* psRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".ps"); + auto drawIndexBuffer = GetOrCreateDrawIndexBuffer("LocalShadow", nv); + if (!vsRefl || !psRefl || !drawIndexBuffer || !st.pipeline || !st.layout || !st.cascadeCB) + return; + + auto& cache = GetPassResourceCache(); + auto& matBuffer = bindless::MaterialBuffer::Instance(); + matBuffer.Upload(ctx); + + auto* backend = GEnv.Backend; + nvrhi::IBindingSet* bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; + + // Perspective local SMs need far less slope bias than sun ortho CSM — + // high bias was shrinking bed/table/NPC contact shadows. + if (!ls.opaquePipeline && st.vs && st.ps && st.inputLayout && st.layout) + { + nvrhi::GraphicsPipelineDesc d; + d.VS = st.vs; + d.PS = st.ps; + d.inputLayout = st.inputLayout; + d.primType = nvrhi::PrimitiveType::TriangleList; + d.renderState.depthStencilState.setDepthTestEnable(true); + d.renderState.depthStencilState.setDepthWriteEnable(true); + d.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + d.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + d.renderState.rasterState.depthBias = 0; + d.renderState.rasterState.slopeScaledDepthBias = 0.5f; + d.renderState.rasterState.depthBiasClamp = 0.001f; + if (auto* bl = backend ? backend->GetBindlessLayout() : nullptr) + d.bindingLayouts = {st.layout, bl}; + else + d.bindingLayouts = {st.layout}; + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.depthFormat = nvrhi::Format::D32; + ls.opaquePipeline = cache.GetOrCreatePipeline("LocalShadowOpaque", d, fbInfo, nv); + if (st.foliagePs && st.foliagePipeline) + { + d.PS = st.foliagePs; + ls.foliagePipeline = cache.GetOrCreatePipeline("LocalShadowFoliage", d, fbInfo, nv); + } + } + nvrhi::IGraphicsPipeline* opaquePipe = + ls.opaquePipeline ? ls.opaquePipeline.Get() : st.pipeline.Get(); + nvrhi::IGraphicsPipeline* foliagePipe = + ls.foliagePipeline ? ls.foliagePipeline.Get() : st.foliagePipeline.Get(); + + const u32 cres = ls.resolution; + nvrhi::Viewport vp(0.f, float(cres), 0.f, float(cres), 0.f, 1.f); + + { + static bool s_once = false; + if (!s_once) + { + Msg("* [LocalShadow] Drawing %u tiles @ %ux%u (skinned=%d)", + (u32)data.tiles.size(), cres, cres, + (ps_r_skinned_shadows != 0 && data.worldSkinnedBatches) ? 1 : 0); + s_once = true; + } + } + + cmd->clearDepthStencilTexture( + atlasTex, + nvrhi::TextureSubresourceSet(0, 1, 0, ls.sliceCount), + true, 1.0f, false, 0); + + // Restore identity caster maps (CSM light-cull overwrites with compact subset). + if (data.gpuCulling) + data.gpuCulling->EnsureShadowCasterIdentityBuffers(cmd, nv); + + const bool drawWorldSkinned = + (ps_r_skinned_shadows != 0) && data.worldSkinnedBatches && data.gpuCulling && + (st.skinnedPipeline || st.skinned4wPipeline || st.skinnedHqPipeline || + st.skinned2wPipeline || st.skinned3wPipeline); + nvrhi::IBuffer* skBoneBuf = + drawWorldSkinned ? data.gpuCulling->GetGlobalBoneBuffer() : nullptr; + auto* skPsRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".ps"); + auto* skVsReflNonHQ = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned", ".vs"); + auto* skVsRefl4W = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_4w", ".vs"); + auto* skVsReflHQ = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_hq", ".vs"); + auto* skVsRefl2W = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_2w", ".vs"); + auto* skVsRefl3W = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_3w", ".vs"); + nvrhi::IBuffer* skDynCB = nullptr; + nvrhi::IBuffer* skMatCB = nullptr; + const bool worldSkinnedReady = drawWorldSkinned && skBoneBuf && skPsRefl; + constexpr u32 kSkinnedVCBVersions = 2048; + constexpr u32 kMaxSkinnedPerTile = 128; + u32 skinnedWritesLeft = kSkinnedVCBVersions; + if (worldSkinnedReady) + { + skDynCB = cache.GetOrCreateVolatileCB( + "LocalShadow", "WorldSkinDyn2", sizeof(DynamicTransforms), data.device, + kSkinnedVCBVersions); + skMatCB = cache.GetOrCreateVolatileCB( + "LocalShadow", "WorldSkinMat2", sizeof(SkinnedMaterialCB), data.device, + kSkinnedVCBVersions); + } + + for (const LocalShadowTile& tile : data.tiles) + { + ZoneScopedN("LocalShadows::Tile"); + if (tile.slice >= ls.sliceCount) + continue; + + ShadowCascadeCB cbData{}; + cbData.lightVP = tile.clipVP; + cmd->writeBuffer(st.cascadeCB, &cbData, sizeof(cbData)); + + nvrhi::FramebufferDesc fbDesc; + nvrhi::TextureSubresourceSet depthSub; + depthSub.baseArraySlice = tile.slice; + depthSub.numArraySlices = 1; + fbDesc.setDepthAttachment(atlasTex, depthSub); + auto fb = cache.GetOrCreateFramebuffer( + make_string("LocalShadow_%u_%u", cres, tile.slice).c_str(), fbDesc, nv); + if (!fb) + continue; + + auto drawSet = [&](const BindlessDrawSet& set, nvrhi::IGraphicsPipeline* pipe) { + if (!pipe) + return; + if (!set.IsCastAllValid() && !set.IsValid()) + return; + const bool castAll = set.IsCastAllValid(); + nvrhi::IBuffer* batchIndices = castAll + ? set.castAllBatchIndicesBuffer : set.compactBatchIndicesBuffer; + nvrhi::IBuffer* materialIDs = castAll + ? set.castAllMaterialIDBuffer : set.compactMaterialIDBuffer; + nvrhi::IBuffer* drawArgs = castAll + ? set.castAllDrawArgsBuffer : set.compactDrawArgsBuffer; + nvrhi::IBuffer* countBuffer = castAll + ? set.castAllCountBuffer : set.compactCountBuffer; + + BindingSetBuilder bsb(*vsRefl, *psRefl, nv, "LocalShadow"); + bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + bsb.BufferSRV("g_InstanceData", set.instanceBuffer); + bsb.BufferSRV("g_CompactBatchIndices", batchIndices); + bsb.BufferSRV("g_CompactMaterialIDs", materialIDs); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), st.layout, nv); + if (!bindingSet) + return; + + nvrhi::GraphicsState gs; + gs.pipeline = pipe; + gs.framebuffer = fb; + gs.bindings = {bindingSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {data.bindlessConfig.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = {data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + gs.viewport.addViewport(vp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.indirectParams = drawArgs; + gs.indirectCountBuffer = countBuffer; + cmd->setGraphicsState(gs); + DrawIndexedIndirectCountOrFallback(cmd, 0, 0, set.totalObjectCount); + }; + + drawSet(data.bindlessConfig.staticSet, opaquePipe); + drawSet(data.bindlessConfig.dynamicSet, opaquePipe); + if (foliagePipe) + drawSet(data.bindlessConfig.transparentCasterSet, foliagePipe); + + if (st.terrainPipeline && st.terrainLayout && data.bindlessConfig.HasTerrain()) + { + if (auto* matCache = GEnv.Render ? GEnv.Render->GetMaterialCache() : nullptr) + matCache->FinalizePendingTerrainMaterials(ctx); + auto& terrainMatBuffer = bindless::TerrainMaterialBuffer::Instance(); + terrainMatBuffer.Upload(ctx); + auto* terrainPsRefl = shaderLoader->GetCachedReflection( + "shadow\\shadow_cascade_terrain", ".ps"); + const bool useCompact = data.bindlessConfig.UseTerrainCompaction(); + nvrhi::IBuffer* tInstance = data.bindlessConfig.terrainInstanceBuffer; + nvrhi::IBuffer* tBatch = useCompact + ? data.bindlessConfig.terrainCompactBatchIndicesBuffer + : data.bindlessConfig.terrainBatchIndicesBuffer; + nvrhi::IBuffer* tMat = useCompact + ? data.bindlessConfig.terrainCompactMaterialIDBuffer + : data.bindlessConfig.terrainMaterialIDBuffer; + nvrhi::IBuffer* tArgs = useCompact + ? data.bindlessConfig.terrainCompactDrawArgsBuffer + : data.bindlessConfig.terrainDrawArgsBuffer; + nvrhi::IBuffer* tCount = useCompact + ? data.bindlessConfig.terrainCompactCountBuffer + : nullptr; + if (terrainPsRefl && terrainMatBuffer.GetBuffer() && tInstance && tBatch && tMat && tArgs) + { + BindingSetBuilder tbsb(*vsRefl, *terrainPsRefl, nv, "LocalShadow.Terrain"); + tbsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + tbsb.BufferSRV("g_InstanceData", tInstance); + tbsb.BufferSRV("g_CompactBatchIndices", tBatch); + tbsb.BufferSRV("g_CompactMaterialIDs", tMat); + auto terrainSet = cache.GetOrCreateBindingSet(tbsb.Build(), st.terrainLayout, nv); + if (terrainSet) + { + nvrhi::GraphicsState gs; + gs.pipeline = st.terrainPipeline; + gs.framebuffer = fb; + gs.bindings = {terrainSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {data.bindlessConfig.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = { + data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + gs.viewport.addViewport(vp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.indirectParams = tArgs; + if (tCount) + gs.indirectCountBuffer = tCount; + cmd->setGraphicsState(gs); + DrawIndexedIndirectCountOrFallback( + cmd, 0, 0, data.bindlessConfig.terrainObjectCount); + } + } + } + + if (worldSkinnedReady && skDynCB && skMatCB && skinnedWritesLeft > 0 && tile.L) + { + // Cast beyond light range a bit so NPC/body shadows reach across the floor. + const float maxR = std::max(tile.L->range * 2.0f, 4.f); + const float maxR2 = maxR * maxR; + const Fvector& lp = tile.L->position; + u32 drawnThisTile = 0; + + for (const auto& batch : *data.worldSkinnedBatches) + { + if (skinnedWritesLeft == 0 || drawnThisTile >= kMaxSkinnedPerTile) + break; + if (!batch.isSkinned || !batch.vertexBuffer || !batch.indexBuffer) + continue; + if (lp.distance_to_sqr(batch.worldMatrix.c) > maxR2) + continue; + + CKinematics* parent = nullptr; + const u32 visualType = batch.visual ? batch.visual->getType() : 0; + if (visualType == MT_SKELETON_GEOMDEF_ST) + parent = static_cast(batch.visual)->GetParent(); + else if (visualType == MT_SKELETON_GEOMDEF_PM) + parent = static_cast(batch.visual)->GetParent(); + if (!parent) + continue; + + const u16 rm = batch.skinningRenderMode; + const u32 stride = batch.vertexStride; + nvrhi::IGraphicsPipeline* pipe = nullptr; + nvrhi::IBindingLayout* layout = nullptr; + nvrhi::IInputLayout* il = nullptr; + auto* vsReflSk = skVsReflNonHQ; + auto pick3w = [&] { + pipe = st.skinned3wPipeline.Get(); layout = st.skinned3wLayout.Get(); + il = st.skinned3wInputLayout.Get(); vsReflSk = skVsRefl3W; + }; + auto pick2w = [&] { + pipe = st.skinned2wPipeline.Get(); layout = st.skinned2wLayout.Get(); + il = st.skinned2wInputLayout.Get(); vsReflSk = skVsRefl2W; + }; + auto pick4w = [&] { + pipe = st.skinned4wPipeline.Get(); layout = st.skinned4wLayout.Get(); + il = st.skinned4wInputLayout.Get(); vsReflSk = skVsRefl4W; + }; + auto pickHq = [&] { + pipe = st.skinnedHqPipeline.Get(); layout = st.skinnedHqLayout.Get(); + il = st.skinnedHqInputLayout.Get(); vsReflSk = skVsReflHQ; + }; + auto pickNonHq = [&] { + pipe = st.skinnedPipeline.Get(); layout = st.skinnedLayout.Get(); + il = st.skinnedInputLayout.Get(); vsReflSk = skVsReflNonHQ; + }; + if (rm == 7 || rm == 8) pick3w(); + else if (rm == 5 || rm == 6) pick2w(); + else if (rm == 9 || rm == 10) pick4w(); + else if (rm == 4 || rm == 2) pickHq(); + else if (rm == 3 || rm == 1) pickNonHq(); + else if (stride == 36) pickHq(); + else if (stride == 40) pick4w(); + else if (stride == 44) pick2w(); + else pickNonHq(); + if (!pipe || !layout || !il || !vsReflSk) + continue; + + const u32 boneOffset = data.gpuCulling->GetOrUploadSkeleton(cmd, parent); + DynamicTransforms dyn{}; + FillDynamicTransforms(dyn, batch.worldMatrix); + cmd->writeBuffer(skDynCB, &dyn, sizeof(dyn)); + SkinnedMaterialCB matId{}; + matId.materialID = batch.bindlessMaterialID; + matId.skeletonBoneOffset = boneOffset; + cmd->writeBuffer(skMatCB, &matId, sizeof(matId)); + + BindingSetBuilder bsb(*vsReflSk, *skPsRefl, nv, "LocalShadow.WorldSkin"); + bsb.ConstantBuffer("dynamic_transforms", skDynCB); + bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + bsb.ConstantBuffer("SkinnedMaterialCB", skMatCB); + bsb.BufferSRV("g_BoneMatrices", skBoneBuf); + bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), layout, nv); + if (!set) + continue; + + nvrhi::GraphicsState gs; + gs.pipeline = pipe; + gs.framebuffer = fb; + gs.bindings = {set}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = {{batch.vertexBuffer, 0, 0}}; + gs.indexBuffer = {batch.indexBuffer, nvrhi::Format::R16_UINT, 0}; + gs.viewport.addViewport(vp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + cmd->setGraphicsState(gs); + cmd->drawIndexed( + nvrhi::DrawArguments() + .setVertexCount(batch.indexCount) + .setStartIndexLocation(batch.startIndex) + .setStartVertexLocation(batch.baseVertex)); + ++drawnThisTile; + --skinnedWritesLeft; + } + } + } + }); + + outputs.atlas = passData.atlas; + outputs.atlasTex = state.atlas; + outputs.valid = true; + return outputs; +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.h new file mode 100644 index 00000000000..4b4b625daa1 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.h @@ -0,0 +1,58 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include "Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h" +#include "Layers/xrRender/FrameGraphPasses/ShadowPassSetup.h" +#include "Layers/xrRender/RenderContext/ResourceHandle.h" +#include "Layers/xrRender/ClusteredLightManager.h" +#include "Layers/xrRender/Geometry/GeometryBatch.h" +#include + +namespace xray::render::framegraph +{ +class FrameGraph; +} + +namespace xray::render::fg +{ +class RenderDevice; +class GPUCullingManager; +} + +namespace xray::render::fg::passes +{ + +struct LocalShadowPassState +{ + xray::render::fg::TextureHandle atlasHandle; + nvrhi::ITexture* atlas = nullptr; + // Low slope-bias pipelines (CSM's 2.75 slope eats thin furniture/NPC contact shadows). + nvrhi::GraphicsPipelineHandle opaquePipeline; + nvrhi::GraphicsPipelineHandle foliagePipeline; + u32 resolution = 0; + u32 sliceCount = 0; + bool initialized = false; + bool enabled = false; +}; + +struct LocalShadowOutputs +{ + framegraph::VirtualResourceHandle atlas; + nvrhi::ITexture* atlasTex = nullptr; + bool valid = false; +}; + +void InitializeLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& state); +void ShutdownLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& state); + +LocalShadowOutputs setupLocalShadowPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + const BindlessForwardConfig& bindlessConfig, + ShadowPassState& shadowState, + LocalShadowPassState& state, + const xr_vector* worldSkinnedBatches = nullptr, + fg::GPUCullingManager* gpuCulling = nullptr); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp index ebbafed6451..c39d1df3478 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp @@ -391,12 +391,12 @@ struct ParticleBlendDesc { }; static const ParticleBlendDesc s_blendDescs[PARTICLE_BLEND_COUNT] = { - { nvrhi::BlendFactor::One, nvrhi::BlendFactor::Zero, nvrhi::BlendFactor::One, nvrhi::BlendFactor::Zero, false, true, "ParticlePass_set" }, - { nvrhi::BlendFactor::SrcAlpha, nvrhi::BlendFactor::InvSrcAlpha, nvrhi::BlendFactor::One, nvrhi::BlendFactor::InvSrcAlpha, true, false, "ParticlePass_blend" }, - { nvrhi::BlendFactor::One, nvrhi::BlendFactor::One, nvrhi::BlendFactor::One, nvrhi::BlendFactor::One, true, false, "ParticlePass_add" }, - { nvrhi::BlendFactor::DstColor, nvrhi::BlendFactor::Zero, nvrhi::BlendFactor::One, nvrhi::BlendFactor::Zero, true, false, "ParticlePass_mul" }, - { nvrhi::BlendFactor::DstColor, nvrhi::BlendFactor::SrcColor, nvrhi::BlendFactor::One, nvrhi::BlendFactor::SrcAlpha, true, false, "ParticlePass_mul2x" }, - { nvrhi::BlendFactor::SrcAlpha, nvrhi::BlendFactor::One, nvrhi::BlendFactor::One, nvrhi::BlendFactor::One, true, false, "ParticlePass_alphaAdd" }, + { nvrhi::BlendFactor::One, nvrhi::BlendFactor::Zero, nvrhi::BlendFactor::One, nvrhi::BlendFactor::Zero, false, true, "ParticlePass_set_v2" }, + { nvrhi::BlendFactor::SrcAlpha, nvrhi::BlendFactor::InvSrcAlpha, nvrhi::BlendFactor::One, nvrhi::BlendFactor::InvSrcAlpha, true, false, "ParticlePass_blend_v2" }, + { nvrhi::BlendFactor::One, nvrhi::BlendFactor::One, nvrhi::BlendFactor::One, nvrhi::BlendFactor::One, true, false, "ParticlePass_add_v2" }, + { nvrhi::BlendFactor::DstColor, nvrhi::BlendFactor::Zero, nvrhi::BlendFactor::One, nvrhi::BlendFactor::Zero, true, false, "ParticlePass_mul_v2" }, + { nvrhi::BlendFactor::DstColor, nvrhi::BlendFactor::SrcColor, nvrhi::BlendFactor::One, nvrhi::BlendFactor::SrcAlpha, true, false, "ParticlePass_mul2x_v2" }, + { nvrhi::BlendFactor::SrcAlpha, nvrhi::BlendFactor::One, nvrhi::BlendFactor::One, nvrhi::BlendFactor::One, true, false, "ParticlePass_alphaAdd_v2" }, }; void InitializeParticleResources(fg::RenderDevice* device, const nvrhi::FramebufferInfoEx& fbInfo, ParticlePassState& state) @@ -465,6 +465,12 @@ void InitializeParticleResources(fg::RenderDevice* device, const nvrhi::Framebuf pipeDesc.renderState.blendState.targets[0].destBlend = bd.destBlend; pipeDesc.renderState.blendState.targets[0].srcBlendAlpha = bd.srcBlendAlpha; pipeDesc.renderState.blendState.targets[0].destBlendAlpha = bd.destBlendAlpha; + // Transparent particles must NOT overwrite G-buffer RTs (normal / + // baseColor / worldPos). Classic particle.ps writes color only; + // leaking billboard worldPos/normal breaks soft particles + lighting + // (water splash / fountain / anomaly glow look "wrong"). + for (u32 rt = 1; rt < 4; ++rt) + pipeDesc.renderState.blendState.targets[rt].setColorWriteMask(nvrhi::ColorMask(0)); } state.pipelines[i] = cache.GetOrCreatePipeline(bd.name, pipeDesc, fbInfo, nvDevice); diff --git a/src/Layers/xrRender/FrameGraphPasses/PassCommon.cpp b/src/Layers/xrRender/FrameGraphPasses/PassCommon.cpp index b373e86a18e..5a65a7404df 100644 --- a/src/Layers/xrRender/FrameGraphPasses/PassCommon.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/PassCommon.cpp @@ -1,13 +1,32 @@ #include "stdafx.h" #include "PassCommon.h" #include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/ResourceManager/FGResourceManager.h" +#include "Layers/xrRender/ResourceManager/TextureManager.h" #include "xrEngine/IGame_Persistent.h" #include "xrEngine/Environment.h" #include "xrEngine/device.h" +#include "xrEngine/IRenderBackend.h" #include "xrCDB/Frustum.h" namespace xray::render::fg::passes { +void DrawIndexedIndirectCountOrFallback( + nvrhi::ICommandList* cmdList, + uint32_t paramOffsetBytes, + uint32_t countOffsetBytes, + uint32_t maxDrawCount) +{ + // MoltenVK / older GPUs may lack drawIndirectCount. Callers that take the + // fallback path must clear unused compact draw-arg slots (instanceCount=0). + const bool useCount = GEnv.Backend && GEnv.Backend->GetCapabilities().drawIndirectCount; + if (useCount) + cmdList->drawIndexedIndirectCount(paramOffsetBytes, countOffsetBytes, maxDrawCount); + else + cmdList->drawIndexedIndirect(paramOffsetBytes, maxDrawCount); +} + nvrhi::BufferHandle GetOrCreateDrawIndexBuffer(const char* passName, nvrhi::IDevice* device) { auto& cache = framegraph::GetPassResourceCache(); @@ -71,4 +90,55 @@ u32 ExtractFrustumPlanes(Fvector4 outPlanes[6]) return count; } +void ResolveEnvSkyCubes(fg::RenderDevice* device, nvrhi::ITexture*& outSky0, nvrhi::ITexture*& outSky1) +{ + outSky0 = nullptr; + outSky1 = nullptr; + + nvrhi::IDevice* nvDevice = device ? device->GetNVRHIDevice() : nullptr; + auto* texManager = (device && device->GetFGResourceManager()) + ? device->GetFGResourceManager()->GetTextureManager() + : nullptr; + + // Cache by sky texture name — calling LoadTexture every frame was a major hitch. + static shared_str s_cachedName0; + static shared_str s_cachedName1; + static nvrhi::ITexture* s_cachedTex0 = nullptr; + static nvrhi::ITexture* s_cachedTex1 = nullptr; + + // Water .s binds $user$sky0/$user$sky1 (full sky cubemaps), not $user$env_s0 (#small). + if (texManager && g_pGamePersistent) + { + auto& env = g_pGamePersistent->Environment(); + const shared_str name0 = (env.Current[0] && env.Current[0]->sky_texture_name.size()) + ? env.Current[0]->sky_texture_name + : shared_str(); + const shared_str name1 = (env.Current[1] && env.Current[1]->sky_texture_name.size()) + ? env.Current[1]->sky_texture_name + : shared_str(); + + if (name0.size() && (name0 != s_cachedName0 || !s_cachedTex0)) + { + s_cachedName0 = name0; + s_cachedTex0 = texManager->GetNVRHITexture(texManager->LoadTexture(name0.c_str())); + } + if (name1.size() && (name1 != s_cachedName1 || !s_cachedTex1)) + { + s_cachedName1 = name1; + s_cachedTex1 = texManager->GetNVRHITexture(texManager->LoadTexture(name1.c_str())); + } + + if (name0.size()) + outSky0 = s_cachedTex0; + if (name1.size()) + outSky1 = s_cachedTex1; + } + + auto& cache = framegraph::GetPassResourceCache(); + if (!outSky0 && nvDevice) + outSky0 = cache.GetDummyCubeMap(nvDevice); + if (!outSky1 && nvDevice) + outSky1 = cache.GetDummyCubeMap(nvDevice); +} + } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/PassCommon.h b/src/Layers/xrRender/FrameGraphPasses/PassCommon.h index 93d7be01b37..0abe13320a6 100644 --- a/src/Layers/xrRender/FrameGraphPasses/PassCommon.h +++ b/src/Layers/xrRender/FrameGraphPasses/PassCommon.h @@ -3,16 +3,17 @@ #include #include "xrCore/xrCore.h" +namespace xray::render::framegraph { +class BindingSetBuilder; +} + namespace xray::render::fg::passes { -inline void DrawIndexedIndirectCountOrFallback( +void DrawIndexedIndirectCountOrFallback( nvrhi::ICommandList* cmdList, uint32_t paramOffsetBytes, uint32_t countOffsetBytes, - uint32_t maxDrawCount) -{ - cmdList->drawIndexedIndirectCount(paramOffsetBytes, countOffsetBytes, maxDrawCount); -} + uint32_t maxDrawCount); struct LightingConstants { Fvector4 sunDirection; @@ -68,4 +69,7 @@ LightingConstants FillLightingConstants(); u32 ExtractFrustumPlanes(Fvector4 outPlanes[6]); +// Resolve $user$sky0/$user$sky1 cubemaps for water reflections (falls back to dummy cubes). +void ResolveEnvSkyCubes(fg::RenderDevice* device, nvrhi::ITexture*& outSky0, nvrhi::ITexture*& outSky1); + } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/PassVertexFormats.h b/src/Layers/xrRender/FrameGraphPasses/PassVertexFormats.h index c4c0e0d1cb4..65e939bca97 100644 --- a/src/Layers/xrRender/FrameGraphPasses/PassVertexFormats.h +++ b/src/Layers/xrRender/FrameGraphPasses/PassVertexFormats.h @@ -64,18 +64,10 @@ struct HistogramCB { }; struct AdaptCB { - float minLogLum; - float logLumRange; - float lowPercentile; - float highPercentile; - float adaptSpeedUp; - float adaptSpeedDown; - float deltaTime; - float exposureCompensation; - float minExposure; - float maxExposure; - float calibrationConstant; - float padding; + float middleGrayX; + float middleGrayY; + float middleGrayZ; + float middleGrayW; // adaptation blend }; } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/RainGPUSimManager.cpp b/src/Layers/xrRender/FrameGraphPasses/RainGPUSimManager.cpp new file mode 100644 index 00000000000..4d64f26bda6 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/RainGPUSimManager.cpp @@ -0,0 +1,342 @@ +#include "stdafx.h" +#include "RainGPUSimManager.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/FrameGraph/ShaderReflection.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "xrEngine/IGame_Persistent.h" +#include "xrEngine/Environment.h" +#include "Layers/xrRender/xrRender_console.h" +#include "xrEngine/device.h" +#include + +namespace xray::render::fg::passes +{ +namespace +{ +constexpr float kSourceRadius = 12.5f; +constexpr float kSourceOffset = 40.f; +constexpr float kMaxDistance = kSourceOffset * 1.25f; +constexpr float kSinkOffset = -(kMaxDistance - kSourceOffset); +constexpr float kDropLength = 5.f; +constexpr float kDropWidth = 0.30f; +constexpr float kDropAngle = 3.0f; +constexpr float kDropMaxAngle = deg2rad(10.f); +constexpr float kDropMaxWindVel = 20.0f; +} + +RainGPUSimManager::~RainGPUSimManager() { Shutdown(); } + +bool RainGPUSimManager::Initialize(fg::RenderDevice* device) +{ + if (m_initialized) + return true; + if (!device) + return false; + + m_device = device; + if (!CreateShaders() || !CreateBuffers() || !CreatePipelines()) + { + Shutdown(); + return false; + } + + ClearParticleBuffer(); + m_simCBSize = GetSimCBSize(); + m_initialized = true; + Msg("* [RainGPUSim] Initialized (%u max particles, CB=%u bytes)", kMaxParticles, m_simCBSize); + return true; +} + +void RainGPUSimManager::Shutdown() +{ + m_updateCS = nullptr; + m_billboardCS = nullptr; + m_updatePipeline = nullptr; + m_billboardPipeline = nullptr; + m_updateLayout = nullptr; + m_billboardLayout = nullptr; + m_updateBindingSet = nullptr; + m_billboardBindingSet = nullptr; + m_pointSampler = nullptr; + m_particleBuffer = nullptr; + m_visibleIndicesBuffer = nullptr; + m_visibleCountBuffer = nullptr; + m_vertexBuffer = nullptr; + m_drawArgsBuffer = nullptr; + m_updateParamsCB = nullptr; + m_billboardParamsCB = nullptr; + m_device = nullptr; + m_initialized = false; + m_lastVisibleCount = 0; +} + +bool RainGPUSimManager::CreateShaders() +{ + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + if (!shaderLoader) + return false; + + auto upd = shaderLoader->LoadComputeShader("rain\\rain_update", "main"); + if (!upd.handle) + { + Msg("! [RainGPUSim] Failed to load rain\\rain_update.cs"); + return false; + } + m_updateCS = upd.handle; + + auto bb = shaderLoader->LoadComputeShader("rain\\rain_billboard", "main"); + if (!bb.handle) + { + Msg("! [RainGPUSim] Failed to load rain\\rain_billboard.cs"); + return false; + } + m_billboardCS = bb.handle; + return true; +} + +bool RainGPUSimManager::CreateBuffers() +{ + nvrhi::IDevice* nv = m_device->GetNVRHIDevice(); + if (!nv) + return false; + + constexpr u32 particleStride = 48; // matches RainParticle in shader + + { + nvrhi::BufferDesc d; + d.byteSize = kMaxParticles * particleStride; + d.structStride = particleStride; + d.debugName = "RainGPUParticles"; + d.canHaveUAVs = true; + d.keepInitialState = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + m_particleBuffer = nv->createBuffer(d); + if (!m_particleBuffer) return false; + } + { + nvrhi::BufferDesc d; + d.byteSize = kMaxParticles * sizeof(u32); + d.structStride = sizeof(u32); + d.debugName = "RainGPUVisibleIndices"; + d.canHaveUAVs = true; + d.keepInitialState = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + m_visibleIndicesBuffer = nv->createBuffer(d); + if (!m_visibleIndicesBuffer) return false; + } + { + nvrhi::BufferDesc d; + d.byteSize = sizeof(u32); + d.debugName = "RainGPUVisibleCount"; + d.canHaveUAVs = true; + d.canHaveRawViews = true; + d.keepInitialState = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + m_visibleCountBuffer = nv->createBuffer(d); + if (!m_visibleCountBuffer) return false; + } + { + nvrhi::BufferDesc d; + d.byteSize = kMaxParticles * 6 * 24; + d.structStride = 24; + d.isVertexBuffer = true; + d.canHaveUAVs = true; + d.canHaveRawViews = true; + d.keepInitialState = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + d.debugName = "RainGPUVertexBuffer"; + m_vertexBuffer = nv->createBuffer(d); + if (!m_vertexBuffer) return false; + } + { + nvrhi::BufferDesc d; + d.byteSize = 5 * sizeof(u32); + d.isDrawIndirectArgs = true; + d.canHaveUAVs = true; + d.canHaveRawViews = true; + d.debugName = "RainGPUDrawArgs"; + d.keepInitialState = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + m_drawArgsBuffer = nv->createBuffer(d); + if (!m_drawArgsBuffer) return false; + } + + nvrhi::SamplerDesc sd; + sd.setAllAddressModes(nvrhi::SamplerAddressMode::Clamp); + sd.setAllFilters(false); + m_pointSampler = nv->createSampler(sd); + + auto& cache = framegraph::GetPassResourceCache(); + m_updateParamsCB = cache.GetOrCreateVolatileCB( + "RainGPUSim", "UpdateParamsV3", GetSimCBSize(), m_device); + m_billboardParamsCB = cache.GetOrCreateVolatileCB( + "RainGPUSim", "BillboardParamsV2", sizeof(RainBillboardParams), m_device); + return m_pointSampler && m_updateParamsCB && m_billboardParamsCB; +} + +u32 RainGPUSimManager::GetSimCBSize() const +{ + u32 size = sizeof(RainSimParams); + auto* shaderLoader = GEnv.Render ? GEnv.Render->GetShaderLoader() : nullptr; + auto* refl = shaderLoader ? shaderLoader->GetCachedReflection("rain\\rain_update", ".cs") : nullptr; + if (!refl) + return size; + + const auto& cbs = framegraph::ShaderReflector::GetConstantBuffers(refl); + if (const auto* cb = cbs.GetByName("RainSimParams")) + { + if (cb->size > 0) + { + if (cb->size != size) + { + Msg("! [RainGPUSim] RainSimParams size mismatch: C++=%u shader=%u (using shader)", + size, cb->size); + } + size = cb->size; + } + } + return size; +} + +bool RainGPUSimManager::CreatePipelines() +{ + nvrhi::IDevice* nv = m_device->GetNVRHIDevice(); + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* updRefl = shaderLoader->GetCachedReflection("rain\\rain_update", ".cs"); + auto* bbRefl = shaderLoader->GetCachedReflection("rain\\rain_billboard", ".cs"); + if (!nv || !updRefl || !bbRefl) + return false; + + m_updateLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("RainGPUUpdate", *updRefl, nv); + m_billboardLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("RainGPUBillboard", *bbRefl, nv); + if (!m_updateLayout || !m_billboardLayout) + return false; + + nvrhi::ComputePipelineDesc pd; + pd.CS = m_updateCS; + pd.bindingLayouts = { m_updateLayout }; + m_updatePipeline = nv->createComputePipeline(pd); + + pd.CS = m_billboardCS; + pd.bindingLayouts = { m_billboardLayout }; + m_billboardPipeline = nv->createComputePipeline(pd); + + return m_updatePipeline && m_billboardPipeline; +} + +void RainGPUSimManager::ClearParticleBuffer() +{ + nvrhi::IDevice* nv = m_device->GetNVRHIDevice(); + if (!nv || !m_particleBuffer) + return; + + constexpr u32 particleStride = 48; + xr_vector zeros(kMaxParticles * particleStride, 0); + + nvrhi::CommandListHandle cmd = nv->createCommandList(); + cmd->open(); + cmd->writeBuffer(m_particleBuffer, zeros.data(), zeros.size()); + cmd->close(); + nv->executeCommandList(cmd); +} + +void RainGPUSimManager::Dispatch( + nvrhi::ICommandList* cmdList, + nvrhi::ITexture* rainShadow, + const Fmatrix& rainSampleVP, + bool rainSMValid, + u32 particleCount, + float rainDensity, + float factorVisual, + u32 rainColor) +{ + if (!m_initialized || !cmdList || particleCount == 0) + { + m_lastVisibleCount = 0; + return; + } + + particleCount = std::min(particleCount, kMaxParticles); + nvrhi::IDevice* nv = m_device->GetNVRHIDevice(); + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* updRefl = shaderLoader->GetCachedReflection("rain\\rain_update", ".cs"); + auto* bbRefl = shaderLoader->GetCachedReflection("rain\\rain_billboard", ".cs"); + if (!nv || !updRefl || !bbRefl) + return; + + const auto& env = g_pGamePersistent->Environment().CurrentEnv; + float gust = g_pGamePersistent->Environment().wind_strength_factor / 10.f; + float k = env.wind_velocity * gust / kDropMaxWindVel; + clamp(k, 0.f, 1.f); + float pitch = kDropMaxAngle * k - PI_DIV_2; + Fvector axis(0.f, -1.f, 0.f); + axis.setHP(env.wind_direction, pitch); + if (axis.square_magnitude() < 1e-4f) + axis.set(0.f, -1.f, 0.f); + else + axis.normalize(); + + RainSimParams sim{}; + sim.cameraPos.set(Device.vCameraPosition.x, Device.vCameraPosition.y, Device.vCameraPosition.z, 0.f); + sim.windAxis.set(axis.x, axis.y, axis.z, 0.f); + sim.params.set(Device.fTimeDelta, factorVisual, 0.f, rainDensity); + sim.rainSampleVP = rainSampleVP; + sim.rainSM.set(rainSMValid ? 1.f : 0.f, float(ps_r3_dyn_wet_surf_sm_res), 0.f, 0.f); + sim.particleCount = particleCount; + sim.frameSeed = Device.dwFrame; + + cmdList->writeBuffer(m_updateParamsCB, &sim, sizeof(sim)); + + u32 zero = 0; + cmdList->writeBuffer(m_visibleCountBuffer, &zero, sizeof(zero)); + + framegraph::BindingSetBuilder updBsb(*updRefl, nv, "RainGPUUpdate"); + updBsb.ConstantBuffer("RainSimParams", m_updateParamsCB); + updBsb.BufferUAV("g_Particles", m_particleBuffer); + updBsb.BufferUAV("g_VisibleIndices", m_visibleIndicesBuffer); + updBsb.BufferUAV("g_VisibleCount", m_visibleCountBuffer); + if (rainShadow && rainSMValid) + updBsb.Texture("g_RainShadow", rainShadow); + m_updateBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(updBsb.Build(), m_updateLayout, nv); + + nvrhi::ComputeState cs; + cs.pipeline = m_updatePipeline; + cs.bindings = { m_updateBindingSet }; + cmdList->setComputeState(cs); + const u32 groups = (particleCount + kThreadGroupSize - 1) / kThreadGroupSize; + cmdList->dispatch(groups, 1, 1); + + cmdList->setBufferState(m_particleBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(m_visibleIndicesBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(m_visibleCountBuffer, nvrhi::ResourceStates::ShaderResource); + + RainBillboardParams bb{}; + bb.cameraPos.set(Device.vCameraPosition.x, Device.vCameraPosition.y, Device.vCameraPosition.z, 0.f); + float rainColorBits = 0.f; + std::memcpy(&rainColorBits, &rainColor, sizeof(u32)); + bb.params.set(factorVisual, kDropLength, kDropWidth, rainColorBits); + cmdList->writeBuffer(m_billboardParamsCB, &bb, sizeof(bb)); + + framegraph::BindingSetBuilder bbBsb(*bbRefl, nv, "RainGPUBillboard"); + bbBsb.ConstantBuffer("RainBillboardParams", m_billboardParamsCB); + bbBsb.BufferSRV("g_Particles", m_particleBuffer); + bbBsb.BufferSRV("g_VisibleIndices", m_visibleIndicesBuffer); + bbBsb.BufferSRV("g_VisibleCountBuf", m_visibleCountBuffer); + bbBsb.BufferUAV("g_Vertices", m_vertexBuffer); + bbBsb.BufferUAV("g_DrawArgs", m_drawArgsBuffer); + m_billboardBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bbBsb.Build(), m_billboardLayout, nv); + + cs.pipeline = m_billboardPipeline; + cs.bindings = { m_billboardBindingSet }; + cmdList->setComputeState(cs); + cmdList->dispatch(groups, 1, 1); + + cmdList->setBufferState(m_vertexBuffer, nvrhi::ResourceStates::VertexBuffer); + cmdList->setBufferState(m_drawArgsBuffer, nvrhi::ResourceStates::IndirectArgument); + + m_lastVisibleCount = particleCount; +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/RainGPUSimManager.h b/src/Layers/xrRender/FrameGraphPasses/RainGPUSimManager.h new file mode 100644 index 00000000000..cdd8def6d05 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/RainGPUSimManager.h @@ -0,0 +1,98 @@ +#pragma once + +#include "xrCore/xrCore.h" +#include + +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes { + +// Must match rain/rain_update.cs RainSimParams cbuffer layout exactly. +struct alignas(16) RainSimParams +{ + Fvector4 cameraPos; + Fvector4 windAxis; + Fvector4 params; // dt, factorVisual, unused, rainDensity + Fmatrix rainSampleVP; + Fvector4 rainSM; + u32 particleCount = 0; + u32 frameSeed = 0; + u32 pad[2]{}; +}; +static_assert(sizeof(RainSimParams) == 144, "RainSimParams must match rain_update.cs (144 bytes)"); + +struct alignas(16) RainBillboardParams +{ + Fvector4 cameraPos; + Fvector4 params; // factorVisual, dropLength, dropWidth, rainColorU32 as float bits +}; +static_assert(sizeof(RainBillboardParams) == 32, "RainBillboardParams must be 32 bytes"); + +struct RainHeightmapInfo +{ + nvrhi::ITexture* texture = nullptr; + float worldMinX = 0.f; + float worldMinZ = 0.f; + float texelSize = 0.f; + bool valid = false; +}; + +class RainGPUSimManager +{ +public: + static constexpr u32 kThreadGroupSize = 64; + static constexpr u32 kMaxParticles = 2500; + + RainGPUSimManager() = default; + ~RainGPUSimManager(); + + bool Initialize(fg::RenderDevice* device); + void Shutdown(); + bool IsReady() const { return m_initialized; } + + void Dispatch( + nvrhi::ICommandList* cmdList, + nvrhi::ITexture* rainShadow, + const Fmatrix& rainSampleVP, + bool rainSMValid, + u32 particleCount, + float rainDensity, + float factorVisual, + u32 rainColor); + + nvrhi::IBuffer* GetVertexBuffer() const { return m_vertexBuffer; } + nvrhi::IBuffer* GetDrawArgsBuffer() const { return m_drawArgsBuffer; } + u32 GetLastVisibleCount() const { return m_lastVisibleCount; } + +private: + bool CreateShaders(); + bool CreateBuffers(); + bool CreatePipelines(); + void ClearParticleBuffer(); + u32 GetSimCBSize() const; + + fg::RenderDevice* m_device = nullptr; + bool m_initialized = false; + u32 m_lastVisibleCount = 0; + u32 m_simCBSize = sizeof(RainSimParams); + + nvrhi::ShaderHandle m_updateCS; + nvrhi::ShaderHandle m_billboardCS; + nvrhi::ComputePipelineHandle m_updatePipeline; + nvrhi::ComputePipelineHandle m_billboardPipeline; + nvrhi::BindingLayoutHandle m_updateLayout; + nvrhi::BindingLayoutHandle m_billboardLayout; + nvrhi::BindingSetHandle m_updateBindingSet; + nvrhi::BindingSetHandle m_billboardBindingSet; + nvrhi::SamplerHandle m_pointSampler; + + nvrhi::BufferHandle m_particleBuffer; + nvrhi::BufferHandle m_visibleIndicesBuffer; + nvrhi::BufferHandle m_visibleCountBuffer; + nvrhi::BufferHandle m_vertexBuffer; + nvrhi::BufferHandle m_drawArgsBuffer; + nvrhi::BufferHandle m_updateParamsCB; + nvrhi::BufferHandle m_billboardParamsCB; +}; + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/RainPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/RainPassSetup.cpp index 1587096c884..35522164a01 100644 --- a/src/Layers/xrRender/FrameGraphPasses/RainPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/RainPassSetup.cpp @@ -2,6 +2,7 @@ #include "RainPassSetup.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" #include "Layers/xrRender/RenderContext/RenderContext.h" #include "Layers/xrRender/fgRainRender.h" @@ -11,31 +12,53 @@ framegraph::VirtualResourceHandle setupRainPass( framegraph::FrameGraph& fg, framegraph::VirtualResourceHandle inputTarget, framegraph::VirtualResourceHandle depthTarget, - FGRainRender* renderer) + framegraph::VirtualResourceHandle worldPosTarget, + framegraph::VirtualResourceHandle rainSMTarget, + FGRainRender* renderer, + const Fmatrix& rainSampleVP, + bool rainSMValid, + const RainHeightmapInfo& heightmap) { using namespace framegraph; + if (!depthTarget.is_valid() || !worldPosTarget.is_valid()) + { + Msg("! [RainPass] Missing depth/worldPos — skipping rain (would be unoccluded)"); + return inputTarget; + } + auto& passData = fg.addCallbackPass( "Rain", - [inputTarget, depthTarget, renderer](FrameGraph& builder, PassHandle passHandle, RainPassData& data) { + [&](FrameGraph& builder, PassHandle passHandle, RainPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.renderer = renderer; - data.depth = passBuilder.read(depthTarget, ResourceState::DepthStencilRead); + data.heightmap = heightmap; + data.rainSampleVP = rainSampleVP; + data.rainSMValid = rainSMValid; + data.depth = passBuilder.readWrite(depthTarget, ResourceState::DepthStencilWrite); + data.worldPos = passBuilder.read(worldPosTarget, ResourceState::ShaderResource); data.output = passBuilder.readWrite(inputTarget, ResourceState::RenderTarget); + if (rainSMTarget.is_valid()) + data.rainSM = passBuilder.read(rainSMTarget, ResourceState::ShaderResource); }, [](const RainPassData& data, const FrameGraph& fg, fg::RenderContext* ctx) { - if (!data.renderer || !data.renderer->HasWork()) return; + if (!data.renderer) return; nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + nvrhi::IDevice* nv = cmdList ? cmdList->getDevice() : nullptr; auto* outputRT = fg.GetPhysicalTexture(data.output); auto* depth = fg.GetPhysicalTexture(data.depth); - if (!cmdList || !outputRT) return; + auto* worldPos = fg.GetPhysicalTexture(data.worldPos); + auto* rainSM = data.rainSM.is_valid() ? fg.GetPhysicalTexture(data.rainSM) : nullptr; + if (!cmdList || !nv || !outputRT || !depth || !worldPos) return; nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(outputRT); - if (depth) fbDesc.setDepthAttachment(depth); - auto framebuffer = cmdList->getDevice()->createFramebuffer(fbDesc); + fbDesc.setDepthAttachment(depth); + auto framebuffer = GetPassResourceCache().GetOrCreateFramebuffer("RainPass_Soft", fbDesc, nv); + if (!framebuffer) return; - data.renderer->Draw(cmdList, framebuffer); + data.renderer->SetRainShadowInputs(rainSM, data.rainSampleVP, data.rainSMValid && rainSM); + data.renderer->Draw(cmdList, framebuffer, worldPos, data.heightmap); }); return passData.output; diff --git a/src/Layers/xrRender/FrameGraphPasses/RainPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/RainPassSetup.h index 931a3e1a05d..280ff085d95 100644 --- a/src/Layers/xrRender/FrameGraphPasses/RainPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/RainPassSetup.h @@ -2,6 +2,7 @@ #include "Layers/xrRender/FrameGraph/FGTypes.h" #include "Layers/xrRender/FrameGraph/FGResource.h" +#include "Layers/xrRender/FrameGraphPasses/RainGPUSimManager.h" namespace xray::render::framegraph { class FrameGraph; } namespace xray::render::fg { class FGRainRender; } @@ -11,13 +12,23 @@ namespace xray::render::fg::passes { struct RainPassData { framegraph::VirtualResourceHandle output; framegraph::VirtualResourceHandle depth; + framegraph::VirtualResourceHandle worldPos; + framegraph::VirtualResourceHandle rainSM; FGRainRender* renderer = nullptr; + RainHeightmapInfo heightmap{}; + Fmatrix rainSampleVP; + bool rainSMValid = false; }; framegraph::VirtualResourceHandle setupRainPass( framegraph::FrameGraph& fg, framegraph::VirtualResourceHandle inputTarget, framegraph::VirtualResourceHandle depthTarget, - FGRainRender* renderer); + framegraph::VirtualResourceHandle worldPosTarget, + framegraph::VirtualResourceHandle rainSMTarget, + FGRainRender* renderer, + const Fmatrix& rainSampleVP, + bool rainSMValid, + const RainHeightmapInfo& heightmap); } diff --git a/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.cpp new file mode 100644 index 00000000000..598ce3352f5 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.cpp @@ -0,0 +1,353 @@ +#include "stdafx.h" +#include "RainShadowPassSetup.h" +#include "PassCommon.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/Geometry/MaterialCache.h" +#include "Layers/xrRender/Bindless/MaterialBuffer.h" +#include "Layers/xrRender/Bindless/TerrainMaterialBuffer.h" +#include "Layers/xrRender/ResourceManager/FGResourceManager.h" +#include "Layers/xrRender/ResourceManager/TextureManager.h" +#include "Layers/xrRender/ResourceManager/NativeRTFactory.h" +#include "Layers/xrRender/xrRender_console.h" +#include "Layers/xrRender/GPUCullingManager.h" +#include "Layers/xrRender/r_FrameGraphRenderer.h" +#include "xrEngine/IRenderBackend.h" +#include "xrEngine/IGame_Persistent.h" +#include "xrEngine/Environment.h" +#include "xrEngine/device.h" + +namespace xray::render::fg::passes +{ + +namespace +{ + +void ComputeTopDownRainMatrices(float extent, float height, u32 smapRes, Fmatrix& outClipVP, Fmatrix& outSampleVP) +{ + const Fvector& C = Device.vCameraPosition; + Fvector eye; + eye.set(C.x, C.y + height, C.z); + Fvector dir(0.f, -1.f, 0.f); + Fvector up(0.f, 0.f, 1.f); + + Fmatrix view; + view.build_camera_dir(eye, dir, up); + + const float e = std::max(extent, 8.f); + Fmatrix proj; + proj.build_projection_ortho(e * 2.f, e * 2.f, 1.f, height + 120.f); + + outClipVP.mul(proj, view); + + // Clip → UV (same convention as CSM sample VP) + Fmatrix toUV; + toUV.identity(); + toUV._11 = 0.5f; + toUV._22 = -0.5f; + toUV._33 = 1.f; + toUV._41 = 0.5f; + toUV._42 = 0.5f; + outSampleVP.mul(toUV, outClipVP); + + (void)smapRes; +} + +} // namespace + +void InitializeRainShadowPass(fg::RenderDevice* device, RainShadowPassState& state) +{ + if (!device) + return; + + const u32 res = std::clamp((u32)ps_r3_dyn_wet_surf_sm_res, 64u, 2048u); + if (state.initialized && state.enabled && state.resolution == res && state.rainSM) + return; + + if (state.initialized) + ShutdownRainShadowPass(device, state); + + auto* resMgr = device->GetFGResourceManager(); + if (!resMgr || !resMgr->GetRTFactory() || !resMgr->GetTextureManager()) + { + state.initialized = true; + state.enabled = false; + return; + } + + state.resolution = res; + state.rainSMHandle = resMgr->GetRTFactory()->CreateShadowMap(res, true, "rt_RainShadow"); + state.rainSM = resMgr->GetTextureManager()->GetNVRHITexture(state.rainSMHandle); + state.initialized = true; + state.enabled = (state.rainSM != nullptr); + if (state.enabled) + Msg("* [RainShadow] Init: OK (%ux%u)", res, res); + else + Msg("! [RainShadow] Failed to create rain SM"); +} + +void ShutdownRainShadowPass(fg::RenderDevice* device, RainShadowPassState& state) +{ + if (device && device->GetFGResourceManager() && device->GetFGResourceManager()->GetRTFactory()) + { + if (state.rainSMHandle.IsValid()) + device->GetFGResourceManager()->GetRTFactory()->ReleaseRenderTarget(state.rainSMHandle); + } + state.rainSMHandle = {}; + state.rainSM = nullptr; + state.resolution = 0; + state.initialized = false; + state.enabled = false; +} + +RainShadowOutputs setupRainShadowPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + const BindlessForwardConfig& bindlessConfig, + ShadowPassState& shadowState, + RainShadowPassState& state) +{ + using namespace framegraph; + RainShadowOutputs outputs; + outputs.valid = false; + + if (!device) + return outputs; + + const float rainDensity = g_pGamePersistent + ? g_pGamePersistent->Environment().CurrentEnv.rain_density + : 0.f; + const bool wantRain = rainDensity >= 0.001f; + const bool wantWet = ps_r2_ls_flags.test(R3FLAG_DYN_WET_SURF); + if (!wantRain && !wantWet) + return outputs; + + InitializeRainShadowPass(device, state); + if (!state.enabled || !state.rainSM || !shadowState.enabled || !shadowState.pipeline) + return outputs; + + const float extent = std::max({ps_r3_dyn_wet_surf_far, ps_r3_dyn_wet_surf_near, 40.f}); + // Tall volume so multi-storey roofs above the camera still cast into the map. + ComputeTopDownRainMatrices(extent, 200.f, state.resolution, state.clipVP, state.sampleVP); + + ResourceDesc desc; + desc.type = ResourceDesc::Type::Texture2D; + desc.width = state.resolution; + desc.height = state.resolution; + desc.depth = 1; + desc.arraySize = 1; + desc.format = nvrhi::Format::D32; + desc.isDepthStencil = true; + desc.isImported = true; + desc.debugName = "rt_RainShadow"; + + auto rainHandle = fg.ImportTexture("rt_RainShadow", state.rainSM, desc); + + struct PassData + { + VirtualResourceHandle rainSM; + RainShadowPassState* rainState = nullptr; + ShadowPassState* shadowState = nullptr; + BindlessForwardConfig bindlessConfig; + fg::RenderDevice* device = nullptr; + }; + + auto& passData = fg.addCallbackPass( + "RainShadow", + [&, rainHandle](FrameGraph& builder, PassHandle passHandle, PassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.rainSM = pb.write(rainHandle, ResourceState::DepthStencilWrite); + data.rainState = &state; + data.shadowState = &shadowState; + data.bindlessConfig = bindlessConfig; + data.device = device; + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + if (!data.rainState || !data.shadowState) + return; + auto* rainTex = graph.GetPhysicalTexture(data.rainSM); + if (!rainTex) + rainTex = data.rainState->rainSM; + nvrhi::ICommandList* cmd = ctx->GetCommandList(); + nvrhi::IDevice* nv = cmd ? cmd->getDevice() : nullptr; + if (!cmd || !nv || !rainTex) + return; + + ShadowPassState& st = *data.shadowState; + RainShadowPassState& rs = *data.rainState; + + cmd->clearDepthStencilTexture( + rainTex, nvrhi::TextureSubresourceSet(0, 1, 0, 1), true, 1.0f, false, 0); + + if (!data.bindlessConfig.UseGPUCulling() || !data.bindlessConfig.UseMegaBuffers()) + { + static bool s_warned = false; + if (!s_warned) + { + Msg("! [RainShadow] GPU bindless culling unavailable — rain SM empty (wet cover broken)"); + s_warned = true; + } + return; + } + + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* vsRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".vs"); + auto* psRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".ps"); + auto drawIndexBuffer = GetOrCreateDrawIndexBuffer("RainShadow", nv); + if (!vsRefl || !psRefl || !drawIndexBuffer || !st.pipeline || !st.layout || !st.cascadeCB) + return; + + ShadowCascadeCB cbData{}; + cbData.lightVP = rs.clipVP; + cmd->writeBuffer(st.cascadeCB, &cbData, sizeof(cbData)); + + // Rebuild light-frustum caster lists for the rain ortho volume so roofs + // outside the camera view still occlude wet surfaces. + if (auto* fgr = dynamic_cast(GEnv.Render)) + { + if (auto* cull = fgr->GetGPUCullingManager()) + cull->BuildLightFrustumCasters(cmd, nv, rs.clipVP, 1.35f); + } + + auto& cache = GetPassResourceCache(); + nvrhi::FramebufferDesc fbDesc; + fbDesc.setDepthAttachment(rainTex); + auto fb = cache.GetOrCreateFramebuffer( + make_string("RainShadow_%u", rs.resolution).c_str(), fbDesc, nv); + if (!fb) + return; + + auto& matBuffer = bindless::MaterialBuffer::Instance(); + matBuffer.Upload(ctx); + + auto* backend = GEnv.Backend; + nvrhi::IBindingSet* bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; + + const u32 cres = rs.resolution; + nvrhi::Viewport vp(0.f, float(cres), 0.f, float(cres), 0.f, 1.f); + + auto drawSet = [&](const BindlessDrawSet& set) { + if (!set.IsValid() && !set.IsCastAllValid() && !set.IsLightCullValid()) + return; + // Rain/wet SM: prefer cast-all (full caster list) — roofs outside camera + // compact set must still cast into the top-down rain map. + const bool castAll = set.IsCastAllValid(); + const bool useLightCull = !castAll && (ps_r_shadow_light_cull != 0) && set.IsLightCullValid(); + if (!castAll && !useLightCull && !set.IsValid()) + return; + + nvrhi::IBuffer* batchIndices = useLightCull ? set.lightCullBatchIndicesBuffer + : (castAll ? set.castAllBatchIndicesBuffer : set.compactBatchIndicesBuffer); + nvrhi::IBuffer* materialIDs = useLightCull ? set.lightCullMaterialIDBuffer + : (castAll ? set.castAllMaterialIDBuffer : set.compactMaterialIDBuffer); + nvrhi::IBuffer* drawArgs = useLightCull ? set.lightCullDrawArgsBuffer + : (castAll ? set.castAllDrawArgsBuffer : set.compactDrawArgsBuffer); + nvrhi::IBuffer* countBuffer = useLightCull ? set.lightCullCountBuffer + : (castAll ? set.castAllCountBuffer : set.compactCountBuffer); + + BindingSetBuilder bsb(*vsRefl, *psRefl, nv, "RainShadow"); + bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + bsb.BufferSRV("g_InstanceData", set.instanceBuffer); + bsb.BufferSRV("g_CompactBatchIndices", batchIndices); + bsb.BufferSRV("g_CompactMaterialIDs", materialIDs); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), st.layout, nv); + if (!bindingSet) + return; + + nvrhi::GraphicsState gs; + gs.pipeline = st.pipeline; + gs.framebuffer = fb; + gs.bindings = {bindingSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {data.bindlessConfig.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = {data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + gs.viewport.addViewport(vp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.indirectParams = drawArgs; + gs.indirectCountBuffer = countBuffer; + cmd->setGraphicsState(gs); + DrawIndexedIndirectCountOrFallback(cmd, 0, 0, set.totalObjectCount); + }; + + drawSet(data.bindlessConfig.staticSet); + drawSet(data.bindlessConfig.dynamicSet); + if (st.foliagePipeline) + drawSet(data.bindlessConfig.transparentCasterSet); + + // Terrain must cast into rain SM — otherwise hills/roofs of terrain never occlude wet. + if (st.terrainPipeline && st.terrainLayout && data.bindlessConfig.HasTerrain()) + { + if (auto* matCache = GEnv.Render ? GEnv.Render->GetMaterialCache() : nullptr) + matCache->FinalizePendingTerrainMaterials(ctx); + auto& terrainMatBuffer = bindless::TerrainMaterialBuffer::Instance(); + terrainMatBuffer.Upload(ctx); + + auto* terrainPsRefl = shaderLoader->GetCachedReflection( + "shadow\\shadow_cascade_terrain", ".ps"); + + const bool useCompact = data.bindlessConfig.UseTerrainCompaction(); + nvrhi::IBuffer* tInstance = data.bindlessConfig.terrainInstanceBuffer; + nvrhi::IBuffer* tBatch = useCompact + ? data.bindlessConfig.terrainCompactBatchIndicesBuffer + : data.bindlessConfig.terrainBatchIndicesBuffer; + nvrhi::IBuffer* tMat = useCompact + ? data.bindlessConfig.terrainCompactMaterialIDBuffer + : data.bindlessConfig.terrainMaterialIDBuffer; + nvrhi::IBuffer* tArgs = useCompact + ? data.bindlessConfig.terrainCompactDrawArgsBuffer + : data.bindlessConfig.terrainDrawArgsBuffer; + nvrhi::IBuffer* tCount = useCompact + ? data.bindlessConfig.terrainCompactCountBuffer + : nullptr; + + if (terrainPsRefl && terrainMatBuffer.GetBuffer() && tInstance && tBatch && tMat && tArgs) + { + BindingSetBuilder tbsb(*vsRefl, *terrainPsRefl, nv, "RainShadow.Terrain"); + tbsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + tbsb.BufferSRV("g_InstanceData", tInstance); + tbsb.BufferSRV("g_CompactBatchIndices", tBatch); + tbsb.BufferSRV("g_CompactMaterialIDs", tMat); + auto terrainSet = cache.GetOrCreateBindingSet(tbsb.Build(), st.terrainLayout, nv); + if (terrainSet) + { + nvrhi::GraphicsState gs; + gs.pipeline = st.terrainPipeline; + gs.framebuffer = fb; + gs.bindings = {terrainSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {data.bindlessConfig.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = { + data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + gs.viewport.addViewport(vp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.indirectParams = tArgs; + if (tCount) + gs.indirectCountBuffer = tCount; + cmd->setGraphicsState(gs); + DrawIndexedIndirectCountOrFallback( + cmd, 0, 0, data.bindlessConfig.terrainObjectCount); + } + } + } + }); + + outputs.rainSM = passData.rainSM; + outputs.rainSMTex = state.rainSM; + outputs.sampleVP = state.sampleVP; + outputs.valid = true; + return outputs; +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.h new file mode 100644 index 00000000000..d68e0b86f0a --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.h @@ -0,0 +1,52 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include "Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h" +#include "Layers/xrRender/FrameGraphPasses/ShadowPassSetup.h" +#include "Layers/xrRender/RenderContext/ResourceHandle.h" +#include + +namespace xray::render::framegraph +{ +class FrameGraph; +} + +namespace xray::render::fg +{ +class RenderDevice; +} + +namespace xray::render::fg::passes +{ + +struct RainShadowPassState +{ + xray::render::fg::TextureHandle rainSMHandle; + nvrhi::ITexture* rainSM = nullptr; + u32 resolution = 0; + Fmatrix clipVP; + Fmatrix sampleVP; + bool initialized = false; + bool enabled = false; +}; + +struct RainShadowOutputs +{ + framegraph::VirtualResourceHandle rainSM; + nvrhi::ITexture* rainSMTex = nullptr; + Fmatrix sampleVP; + bool valid = false; +}; + +void InitializeRainShadowPass(fg::RenderDevice* device, RainShadowPassState& state); +void ShutdownRainShadowPass(fg::RenderDevice* device, RainShadowPassState& state); + +RainShadowOutputs setupRainShadowPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + const BindlessForwardConfig& bindlessConfig, + ShadowPassState& shadowState, + RainShadowPassState& state); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.cpp new file mode 100644 index 00000000000..0b898c58805 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.cpp @@ -0,0 +1,391 @@ +#include "stdafx.h" +#include "SSGIPassSetup.h" +#include "ShaderConstants.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" + +extern ENGINE_API int ps_r_ssgi_quality; +extern ENGINE_API float ps_r_ssgi_intensity; + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +void EnsureHistory(nvrhi::IDevice* nv, SSGIPassState& st, u32 w, u32 h) +{ + if (st.history[0] && st.historyW == w && st.historyH == h) + return; + for (int i = 0; i < 2; ++i) + { + nvrhi::TextureDesc td; + td.width = w; + td.height = h; + td.format = nvrhi::Format::RGBA16_FLOAT; + td.isRenderTarget = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = (i == 0) ? "rt_SSGI_Hist0" : "rt_SSGI_Hist1"; + st.history[i] = nv->createTexture(td); + } + st.historyW = w; + st.historyH = h; + st.hasHistory = false; + st.historyIndex = 0; +} + +void InitSSGI(nvrhi::IDevice* nv, SSGIPassState& st) +{ + if (st.initialized || !nv) + return; + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) { st.initialized = true; return; } + + auto vs = loader->LoadVertexShader("fullscreen"); + auto psTrace = loader->LoadPixelShader("ssgi_trace"); + auto psBlur = loader->LoadPixelShader("ssgi_blur"); + auto psTemporal = loader->LoadPixelShader("ssgi_temporal"); + auto psApply = loader->LoadPixelShader("ssgi_apply"); + if (!vs.handle || !psTrace.handle || !psBlur.handle || !psTemporal.handle || !psApply.handle) + { + st.initialized = true; + return; + } + + auto& cache = GetPassResourceCache(); + nvrhi::FramebufferInfoEx fb; + fb.addColorFormat(nvrhi::Format::RGBA16_FLOAT); + + auto makePipe = [&](const char* name, auto& ps, nvrhi::BindingLayoutHandle& layout, + nvrhi::GraphicsPipelineHandle& pipe) { + layout = cache.GetOrCreateBindingLayoutFromReflection( + name, *vs.reflection, *ps.reflection, nv); + if (!layout) + return; + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs.handle); + desc.setPixelShader(ps.handle); + desc.addBindingLayout(layout); + auto* backend = GEnv.Backend; + if (backend && backend->GetBindlessLayout()) + desc.addBindingLayout(backend->GetBindlessLayout()); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.blendState.targets[0].setBlendEnable(false); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + pipe = cache.GetOrCreatePipeline(name, desc, fb, nv); + }; + + makePipe("SSGI_Trace_v2", psTrace, st.traceLayout, st.tracePipeline); + makePipe("SSGI_Blur_v2", psBlur, st.blurLayout, st.blurPipeline); + makePipe("SSGI_Temporal_v2", psTemporal, st.temporalLayout, st.temporalPipeline); + makePipe("SSGI_Apply_v2", psApply, st.applyLayout, st.applyPipeline); + st.initialized = true; +} + +void DrawFullscreen( + nvrhi::ICommandList* cmd, + nvrhi::IDevice* nv, + nvrhi::GraphicsPipelineHandle pipe, + nvrhi::BindingSetHandle set, + nvrhi::ITexture* out, + u32 w, u32 h, + const char* fbName) +{ + if (!cmd || !pipe || !set || !out) + return; + auto& cache = GetPassResourceCache(); + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(out); + auto fb = cache.GetOrCreateFramebuffer(fbName, fbDesc, nv); + nvrhi::GraphicsState gs; + gs.pipeline = pipe; + gs.framebuffer = fb; + gs.bindings = { set }; + if (GEnv.Backend && GEnv.Backend->GetBindlessDescriptorTable()) + gs.addBindingSet(GEnv.Backend->GetBindlessDescriptorTable()); + gs.viewport.addViewportAndScissorRect(nvrhi::Viewport(float(w), float(h))); + cmd->setGraphicsState(gs); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); +} +} // namespace + +VirtualResourceHandle setupSSGIPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle sceneColor, + VirtualResourceHandle depth, + VirtualResourceHandle normal, + VirtualResourceHandle baseColor, + VirtualResourceHandle motionVectors, + u32 width, + u32 height, + bool hasPrevFrame, + SSGIPassState& state) +{ + if (!device || !device->GetNVRHIDevice()) + return sceneColor; + InitSSGI(device->GetNVRHIDevice(), state); + if (!state.tracePipeline || !state.blurPipeline || !state.applyPipeline) + return sceneColor; + + const int quality = std::clamp(ps_r_ssgi_quality, 1, 3); + const bool halfRes = quality < 3; + const u32 tw = halfRes ? std::max(1u, width / 2) : width; + const u32 th = halfRes ? std::max(1u, height / 2) : height; + + EnsureHistory(device->GetNVRHIDevice(), state, tw, th); + const u32 readIdx = state.historyIndex; + const u32 writeIdx = 1u - readIdx; + const bool useTemporal = state.temporalPipeline && motionVectors.is_valid() && + hasPrevFrame && state.hasHistory && state.history[0]; + + ResourceDesc giDesc; + giDesc.type = ResourceDesc::Type::Texture2D; + giDesc.width = tw; + giDesc.height = th; + giDesc.format = nvrhi::Format::RGBA16_FLOAT; + giDesc.isRenderTarget = true; + giDesc.isTransient = true; + giDesc.debugName = "rt_SSGI"; + auto giTrace = fg.CreateTexture("rt_SSGI", giDesc); + + ResourceDesc blurDesc = giDesc; + blurDesc.debugName = "rt_SSGI_Blur"; + auto giBlur = fg.CreateTexture("rt_SSGI_Blur", blurDesc); + + ResourceDesc tempDesc = giDesc; + tempDesc.debugName = "rt_SSGI_Temporal"; + auto giTemporal = fg.CreateTexture("rt_SSGI_Temporal", tempDesc); + + ResourceDesc outDesc; + outDesc.type = ResourceDesc::Type::Texture2D; + outDesc.width = width; + outDesc.height = height; + outDesc.format = nvrhi::Format::RGBA16_FLOAT; + outDesc.isRenderTarget = true; + outDesc.isTransient = true; + outDesc.debugName = "rt_SSGI_Out"; + auto output = fg.CreateTexture("rt_SSGI_Out", outDesc); + + VirtualResourceHandle histRead{}; + if (useTemporal) + { + ResourceDesc histDesc = giDesc; + histDesc.isTransient = false; + histDesc.isImported = true; + histDesc.debugName = "rt_SSGI_HistRead"; + histRead = fg.ImportTexture("rt_SSGI_HistRead", state.history[readIdx].Get(), histDesc); + } + + struct PassData + { + VirtualResourceHandle color, depth, normal, base, motion, hist; + VirtualResourceHandle giTrace, giBlur, giTemporal, output; + u32 width, height, tw, th, writeIdx; + int quality; + float intensity; + bool useTemporal; + SSGIPassState* st = nullptr; + fg::RenderDevice* device = nullptr; + }; + + auto& pd = fg.addCallbackPass( + "SSGI", + [&](FrameGraph& b, PassHandle ph, PassData& data) { + RenderPassBuilder pb(b, ph); + data.st = &state; + data.device = device; + data.width = width; + data.height = height; + data.tw = tw; + data.th = th; + data.writeIdx = writeIdx; + data.quality = quality; + data.intensity = ps_r_ssgi_intensity; + data.useTemporal = useTemporal; + data.color = pb.read(sceneColor, ResourceState::ShaderResource); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.normal = pb.read(normal, ResourceState::ShaderResource); + data.base = pb.read(baseColor, ResourceState::ShaderResource); + if (useTemporal) + { + data.motion = pb.read(motionVectors, ResourceState::ShaderResource); + data.hist = pb.read(histRead, ResourceState::ShaderResource); + } + data.giTrace = pb.write(giTrace, ResourceState::RenderTarget); + data.giBlur = pb.write(giBlur, ResourceState::RenderTarget); + data.giTemporal = pb.write(giTemporal, ResourceState::RenderTarget); + data.output = pb.write(output, ResourceState::RenderTarget); + pb.sideEffects(); + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd->getDevice(); + auto* color = graph.GetPhysicalTexture(data.color); + auto* depthTex = graph.GetPhysicalTexture(data.depth); + auto* normalTex = graph.GetPhysicalTexture(data.normal); + auto* baseTex = graph.GetPhysicalTexture(data.base); + auto* giT = graph.GetPhysicalTexture(data.giTrace); + auto* giB = graph.GetPhysicalTexture(data.giBlur); + auto* giTemp = graph.GetPhysicalTexture(data.giTemporal); + auto* out = graph.GetPhysicalTexture(data.output); + if (!cmd || !nv || !color || !depthTex || !normalTex || !baseTex || + !giT || !giB || !giTemp || !out || !data.st) + return; + + const auto& cdesc = color->getDesc(); + if (!data.st->colorCopy || + data.st->colorCopy->getDesc().width != cdesc.width || + data.st->colorCopy->getDesc().height != cdesc.height) + { + nvrhi::TextureDesc td = cdesc; + td.debugName = "SSGI_ColorCopy"; + td.isRenderTarget = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + data.st->colorCopy = nv->createTexture(td); + } + if (data.st->colorCopy) + cmd->copyTexture(data.st->colorCopy, nvrhi::TextureSlice(), color, nvrhi::TextureSlice()); + + auto& cache = GetPassResourceCache(); + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psTraceR = loader->GetCachedReflection("ssgi_trace", ".ps"); + auto* psBlurR = loader->GetCachedReflection("ssgi_blur", ".ps"); + auto* psTempR = loader->GetCachedReflection("ssgi_temporal", ".ps"); + auto* psApplyR = loader->GetCachedReflection("ssgi_apply", ".ps"); + if (!vsR || !psTraceR || !psBlurR || !psApplyR) + return; + + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + + struct alignas(16) TraceCB + { + float intensity, maxDistance, thickness, rayCount; + }; + float rays = (data.quality <= 1) ? 4.f : (data.quality == 2) ? 6.f : 8.f; + float maxDist = (data.quality <= 1) ? 8.f : (data.quality == 2) ? 12.f : 18.f; + TraceCB tcb{ data.intensity, maxDist, 0.85f, rays }; + auto* traceCB = cache.GetOrCreateVolatileCB("SSGI", "TraceParams", sizeof(TraceCB), data.device); + if (traceCB) + cmd->writeBuffer(traceCB, &tcb, sizeof(tcb)); + + { + BindingSetBuilder bsb(*vsR, *psTraceR, nv, "SSGI.Trace"); + if (staticGlobalsCB) + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + if (traceCB) + bsb.ConstantBuffer("SSGIParams", traceCB); + bsb.Texture("g_Normal", normalTex) + .Texture("g_Base", baseTex) + .Texture("g_SceneColor", data.st->colorCopy) + .Texture("g_SceneDepth", depthTex); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->traceLayout, nv); + DrawFullscreen(cmd, nv, data.st->tracePipeline, set, giT, data.tw, data.th, "SSGI_Trace"); + cmd->setTextureState(giT, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + + struct alignas(16) BlurCB { float texelX, texelY, p2, p3; }; + BlurCB bcb{ 1.f / float(data.tw), 1.f / float(data.th), 0, 0 }; + auto* blurCB = cache.GetOrCreateVolatileCB("SSGI", "BlurParams", sizeof(BlurCB), data.device); + if (blurCB) + cmd->writeBuffer(blurCB, &bcb, sizeof(bcb)); + + { + BindingSetBuilder bsb(*vsR, *psBlurR, nv, "SSGI.Blur"); + if (blurCB) + bsb.ConstantBuffer("SSGIBlurParams", blurCB); + bsb.Texture("g_SSGI", giT).Texture("g_Depth", depthTex); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->blurLayout, nv); + DrawFullscreen(cmd, nv, data.st->blurPipeline, set, giB, data.tw, data.th, "SSGI_Blur"); + cmd->setTextureState(giB, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + + nvrhi::ITexture* giForApply = giB; + if (data.useTemporal && psTempR && data.st->temporalPipeline) + { + auto* hist = graph.GetPhysicalTexture(data.hist); + auto* motionTex = graph.GetPhysicalTexture(data.motion); + if (!motionTex) + { + static nvrhi::TextureHandle s_zero; + if (!s_zero) + { + nvrhi::TextureDesc td; + td.width = 1; td.height = 1; + td.format = nvrhi::Format::RG16_FLOAT; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + s_zero = nv->createTexture(td); + float z[2] = {0, 0}; + cmd->writeTexture(s_zero, 0, 0, z, sizeof(z)); + } + motionTex = s_zero; + } + if (hist && motionTex) + { + struct alignas(16) TempCB { float sx, sy, hasHistory, pad; }; + TempCB tcb2{ float(data.tw), float(data.th), 1.f, 0.f }; + auto* tempCB = cache.GetOrCreateVolatileCB("SSGI", "TemporalParams", sizeof(TempCB), data.device); + if (tempCB) + cmd->writeBuffer(tempCB, &tcb2, sizeof(tcb2)); + BindingSetBuilder bsb(*vsR, *psTempR, nv, "SSGI.Temporal"); + if (tempCB) + bsb.ConstantBuffer("SSGITemporalParams", tempCB); + bsb.Texture("g_Current", giB) + .Texture("g_History", hist) + .Texture("g_Motion", motionTex) + .Texture("g_Depth", depthTex); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->temporalLayout, nv); + DrawFullscreen(cmd, nv, data.st->temporalPipeline, set, giTemp, data.tw, data.th, "SSGI_Temporal"); + cmd->setTextureState(giTemp, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + giForApply = giTemp; + } + } + + if (data.st->history[data.writeIdx] && giForApply) + { + cmd->copyTexture( + data.st->history[data.writeIdx], nvrhi::TextureSlice(), + giForApply, nvrhi::TextureSlice()); + data.st->historyIndex = data.writeIdx; + data.st->hasHistory = true; + } + + struct alignas(16) ApplyCB { float intensity, p1, p2, p3; }; + ApplyCB acb{ 1.0f, 0, 0, 0 }; + auto* applyCB = cache.GetOrCreateVolatileCB("SSGI", "ApplyParams", sizeof(ApplyCB), data.device); + if (applyCB) + cmd->writeBuffer(applyCB, &acb, sizeof(acb)); + + { + BindingSetBuilder bsb(*vsR, *psApplyR, nv, "SSGI.Apply"); + if (staticGlobalsCB) + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + if (applyCB) + bsb.ConstantBuffer("SSGIApplyParams", applyCB); + bsb.Texture("g_Color", data.st->colorCopy) + .Texture("g_SSGI", giForApply) + .Texture("g_Normal", normalTex) + .Texture("g_Base", baseTex) + .Texture("g_Depth", depthTex); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->applyLayout, nv); + DrawFullscreen(cmd, nv, data.st->applyPipeline, set, out, data.width, data.height, "SSGI_Apply"); + } + }); + + return pd.output; +} + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.h new file mode 100644 index 00000000000..e1da6ab0438 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.h @@ -0,0 +1,45 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes +{ + +struct SSGIPassState +{ + nvrhi::GraphicsPipelineHandle tracePipeline; + nvrhi::GraphicsPipelineHandle blurPipeline; + nvrhi::GraphicsPipelineHandle temporalPipeline; + nvrhi::GraphicsPipelineHandle applyPipeline; + nvrhi::BindingLayoutHandle traceLayout; + nvrhi::BindingLayoutHandle blurLayout; + nvrhi::BindingLayoutHandle temporalLayout; + nvrhi::BindingLayoutHandle applyLayout; + nvrhi::TextureHandle colorCopy; + nvrhi::TextureHandle history[2]; + u32 historyW = 0; + u32 historyH = 0; + u32 historyIndex = 0; + bool hasHistory = false; + bool initialized = false; +}; + +framegraph::VirtualResourceHandle setupSSGIPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle normal, + framegraph::VirtualResourceHandle baseColor, + framegraph::VirtualResourceHandle motionVectors, + u32 width, + u32 height, + bool hasPrevFrame, + SSGIPassState& state); + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.cpp new file mode 100644 index 00000000000..5248386430f --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.cpp @@ -0,0 +1,535 @@ +#include "stdafx.h" +#include "SSRPassSetup.h" +#include "ShaderConstants.h" +#include "PassCommon.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" + +extern ENGINE_API int ps_r_ssr_quality; + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +constexpr u32 kSSRPipeVersion = 43; + +struct SSRQualityParams +{ + float steps; + float distance; + float thickness; + float refine; + float blurIntensity; +}; + +SSRQualityParams QualityFromConsole() +{ + switch (std::clamp(ps_r_ssr_quality, 1, 4)) + { + case 1: return {16.f, 80.f, 1.50f, 2.f, 0.70f}; + case 2: return {24.f, 120.f, 1.25f, 4.f, 0.85f}; + case 3: return {32.f, 160.f, 1.00f, 6.f, 1.00f}; + default: return {48.f, 220.f, 0.85f, 8.f, 1.00f}; + } +} + +void EnsureSSRBuffer(nvrhi::IDevice* nv, SSRPassState& st, u32 w, u32 h) +{ + if (st.history[0] && st.history[1] && st.outputTex && st.blurTex && + st.historyW == w && st.historyH == h) + return; + nvrhi::TextureDesc td; + td.width = w; + td.height = h; + td.format = nvrhi::Format::RGBA16_FLOAT; + td.isRenderTarget = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "rt_SSR_History0"; + st.history[0] = nv->createTexture(td); + td.debugName = "rt_SSR_History1"; + st.history[1] = nv->createTexture(td); + td.debugName = "rt_SSR_Output"; + st.outputTex = nv->createTexture(td); + td.debugName = "rt_SSR_BlurPersist"; + st.blurTex = nv->createTexture(td); + st.historyW = w; + st.historyH = h; + st.historyIndex = 0; + st.historyNeedsClear = true; + st.hasHistory = false; +} + +void DrawSSRFullscreen( + nvrhi::ICommandList* cmd, + nvrhi::IDevice* nv, + nvrhi::GraphicsPipelineHandle pipe, + nvrhi::BindingSetHandle set, + nvrhi::ITexture* out, + u32 w, u32 h, + const char* fbName) +{ + if (!cmd || !pipe || !set || !out) + return; + auto& cache = GetPassResourceCache(); + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(out); + auto fb = cache.GetOrCreateFramebuffer(fbName, fbDesc, nv); + nvrhi::GraphicsState gs; + gs.pipeline = pipe; + gs.framebuffer = fb; + gs.bindings = {set}; + if (GEnv.Backend && GEnv.Backend->GetBindlessDescriptorTable()) + gs.addBindingSet(GEnv.Backend->GetBindlessDescriptorTable()); + gs.viewport.addViewportAndScissorRect(nvrhi::Viewport(float(w), float(h))); + cmd->setGraphicsState(gs); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); +} + +void InitSSR(nvrhi::IDevice* nv, SSRPassState& st) +{ + if (!nv) + return; + if (st.initialized && st.pipeVersion == kSSRPipeVersion) + return; + + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + { + st.initialized = true; + st.pipeVersion = kSSRPipeVersion; + return; + } + + BindingSetBuilder::InvalidateReflectionCache(); + + auto vs = loader->LoadVertexShader("fullscreen"); + auto ps = loader->LoadPixelShader("ssr_resolve"); + auto psBlur = loader->LoadPixelShader("ssr_blur"); + auto psApply = loader->LoadPixelShader("ssr_apply"); + if (!vs.handle || !ps.handle || !psBlur.handle || !psApply.handle || + !vs.reflection || !ps.reflection || !psBlur.reflection || !psApply.reflection) + { + Msg("! [SSR] Shader load failed (resolve=%d blur=%d apply=%d) — will retry", + !!ps.handle, !!psBlur.handle, !!psApply.handle); + st.initialized = false; + return; + } + + auto& cache = GetPassResourceCache(); + st.layout = cache.GetOrCreateBindingLayoutFromReflection( + "SSRResolve_v45", *vs.reflection, *ps.reflection, nv); + st.blurLayout = cache.GetOrCreateBindingLayoutFromReflection( + "SSRBlur_v45", *vs.reflection, *psBlur.reflection, nv); + st.applyLayout = cache.GetOrCreateBindingLayoutFromReflection( + "SSRApply_v45", *vs.reflection, *psApply.reflection, nv); + + auto makeSingle = [&](auto& psSh, nvrhi::BindingLayoutHandle layout, + nvrhi::GraphicsPipelineHandle& pipe, const char* name) { + if (!layout) + return; + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs.handle); + desc.setPixelShader(psSh.handle); + desc.addBindingLayout(layout); + auto* backend = GEnv.Backend; + if (backend && backend->GetBindlessLayout()) + desc.addBindingLayout(backend->GetBindlessLayout()); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.blendState.targets[0].setBlendEnable(false); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + nvrhi::FramebufferInfoEx fb; + fb.addColorFormat(nvrhi::Format::RGBA16_FLOAT); + pipe = cache.GetOrCreatePipeline(name, desc, fb, nv); + }; + + makeSingle(ps, st.layout, st.pipeline, "SSRResolve_v45"); + makeSingle(psBlur, st.blurLayout, st.blurPipeline, "SSRBlur_v45"); + makeSingle(psApply, st.applyLayout, st.applyPipeline, "SSRApply_v45"); + + st.initialized = true; + st.pipeVersion = kSSRPipeVersion; + if (st.pipeline && st.blurPipeline && st.applyPipeline) + Msg("* [SSR] initialized v45 (SSR dim sky + NPC face sheen)"); + else + Msg("! [SSR] Pipeline create failed (resolve=%d blur=%d apply=%d)", + !!st.pipeline, !!st.blurPipeline, !!st.applyPipeline); +} +} // namespace + +VirtualResourceHandle setupSSRPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle sceneColor, + VirtualResourceHandle reflectionColor, + VirtualResourceHandle depth, + VirtualResourceHandle normal, + VirtualResourceHandle baseColor, + VirtualResourceHandle worldPos, + VirtualResourceHandle motionVectors, + u32 width, + u32 height, + bool hasPrevFrame, + SSRPassState& state) +{ + if (!device || !device->GetNVRHIDevice()) + return sceneColor; + + InitSSR(device->GetNVRHIDevice(), state); + if (!state.pipeline || !state.layout || !state.blurPipeline || !state.blurLayout || + !state.applyPipeline || !state.applyLayout) + { + static bool s_loggedPipe = false; + if (!s_loggedPipe) + { + Msg("! [SSR] Pipelines missing — passthrough"); + s_loggedPipe = true; + } + return sceneColor; + } + + if (!reflectionColor.is_valid()) + reflectionColor = sceneColor; + if (!depth.is_valid()) + return sceneColor; + + EnsureSSRBuffer(device->GetNVRHIDevice(), state, width, height); + if (!state.outputTex || !state.blurTex || !state.history[0] || !state.history[1]) + return sceneColor; + (void)hasPrevFrame; + (void)motionVectors; + (void)worldPos; + + // Persistent imported RTs — never FG-aliased (CreateTexture non-transient was not enough). + ResourceDesc outDesc; + outDesc.type = ResourceDesc::Type::Texture2D; + outDesc.width = width; + outDesc.height = height; + outDesc.format = nvrhi::Format::RGBA16_FLOAT; + outDesc.isRenderTarget = true; + outDesc.isTransient = false; + outDesc.isImported = true; + outDesc.debugName = "rt_SSR"; + auto output = fg.ImportTexture("rt_SSR", state.outputTex.Get(), outDesc); + + ResourceDesc blurDesc = outDesc; + blurDesc.debugName = "rt_SSR_Blur"; + auto blurRT = fg.ImportTexture("rt_SSR_Blur", state.blurTex.Get(), blurDesc); + + VirtualResourceHandle ssrBuf{}; + VirtualResourceHandle ssrHist{}; + { + const u32 writeIdx = state.historyIndex & 1u; + const u32 readIdx = (writeIdx ^ 1u); + ResourceDesc ssrDesc = outDesc; + ssrDesc.debugName = "rt_SSR_Buffer"; + ssrBuf = fg.ImportTexture("rt_SSR_Buffer", state.history[writeIdx].Get(), ssrDesc); + ssrDesc.debugName = "rt_SSR_History"; + ssrHist = fg.ImportTexture("rt_SSR_History", state.history[readIdx].Get(), ssrDesc); + } + + struct PassData + { + VirtualResourceHandle color, reflection, depth, normal, base, ssr, hist, blur, output; + u32 width, height; + SSRPassState* st = nullptr; + fg::RenderDevice* device = nullptr; + bool hasHistory = false; + }; + + auto& pd = fg.addCallbackPass( + "SSR", + [&](FrameGraph& b, PassHandle ph, PassData& data) { + RenderPassBuilder pb(b, ph); + data.st = &state; + data.device = device; + data.width = width; + data.height = height; + data.hasHistory = state.hasHistory && !state.historyNeedsClear; + data.color = pb.read(sceneColor, ResourceState::ShaderResource); + data.reflection = pb.read(reflectionColor, ResourceState::ShaderResource); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.normal = pb.read(normal, ResourceState::ShaderResource); + data.base = pb.read(baseColor, ResourceState::ShaderResource); + data.hist = pb.read(ssrHist, ResourceState::ShaderResource); + data.ssr = pb.write(ssrBuf, ResourceState::RenderTarget); + data.blur = pb.write(blurRT, ResourceState::RenderTarget); + data.output = pb.write(output, ResourceState::RenderTarget); + pb.sideEffects(); + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd->getDevice(); + auto* color = graph.GetPhysicalTexture(data.color); + auto* reflection = graph.GetPhysicalTexture(data.reflection); + auto* depthTex = graph.GetPhysicalTexture(data.depth); + auto* normalTex = graph.GetPhysicalTexture(data.normal); + auto* baseTex = graph.GetPhysicalTexture(data.base); + auto* histTex = graph.GetPhysicalTexture(data.hist); + auto* out = graph.GetPhysicalTexture(data.output); + auto* ssrTex = graph.GetPhysicalTexture(data.ssr); + auto* blurTex = graph.GetPhysicalTexture(data.blur); + if (!cmd || !nv || !color || !reflection || !depthTex || !normalTex || !baseTex || + !out || !ssrTex || !blurTex || !data.st) + return; + + auto& cache = GetPassResourceCache(); + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + return; + + const auto& rdesc = reflection->getDesc(); + if (!data.st->colorCopy || + data.st->colorCopy->getDesc().width != rdesc.width || + data.st->colorCopy->getDesc().height != rdesc.height) + { + nvrhi::TextureDesc td = rdesc; + td.debugName = "SSR_ReflectionCopy"; + td.isRenderTarget = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + data.st->colorCopy = nv->createTexture(td); + } + if (data.st->colorCopy) + { + cmd->setTextureState(reflection, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(data.st->colorCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(data.st->colorCopy, nvrhi::TextureSlice(), reflection, nvrhi::TextureSlice()); + cmd->setTextureState(data.st->colorCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(reflection, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + + auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psR = loader->GetCachedReflection("ssr_resolve", ".ps"); + auto* psBlurR = loader->GetCachedReflection("ssr_blur", ".ps"); + auto* psApplyR = loader->GetCachedReflection("ssr_apply", ".ps"); + + // Lit scene copy for apply SRV (must NOT be the same texture as `out`) + const auto& cdesc = color->getDesc(); + if (!data.st->litCopy || + data.st->litCopy->getDesc().width != cdesc.width || + data.st->litCopy->getDesc().height != cdesc.height) + { + nvrhi::TextureDesc td = cdesc; + td.debugName = "SSR_LitCopy"; + td.isRenderTarget = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + data.st->litCopy = nv->createTexture(td); + } + if (data.st->litCopy) + { + cmd->setTextureState(color, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(data.st->litCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(data.st->litCopy, nvrhi::TextureSlice(), color, nvrhi::TextureSlice()); + cmd->setTextureState(data.st->litCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + + // Depth → typeless SRV copy (same as water SSR on Metal/MoltenVK) + { + const auto& ddesc = depthTex->getDesc(); + if (!data.st->depthCopy || + data.st->depthCopy->getDesc().width != ddesc.width || + data.st->depthCopy->getDesc().height != ddesc.height) + { + nvrhi::TextureDesc td{}; + td.width = ddesc.width; + td.height = ddesc.height; + td.format = nvrhi::Format::D32; + td.debugName = "SSR_DepthCopy"; + td.isShaderResource = true; + td.isTypeless = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + data.st->depthCopy = nv->createTexture(td); + } + if (data.st->depthCopy) + { + cmd->setTextureState(depthTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(data.st->depthCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(data.st->depthCopy, nvrhi::TextureSlice(), depthTex, nvrhi::TextureSlice()); + cmd->setTextureState(data.st->depthCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + } + + // Fallback: scene already in out if resolve/apply fail + cmd->setTextureState(color, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(out, nvrhi::TextureSlice(), color, nvrhi::TextureSlice()); + cmd->setTextureState(color, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(depthTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(normalTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(baseTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + + if (!vsR || !psR || !psBlurR || !psApplyR) + { + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + return; + } + + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + if (staticGlobalsCB) + { + StaticGlobals sg = BuildStaticGlobals(); + cmd->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + + const SSRQualityParams q = QualityFromConsole(); + struct alignas(16) PCB + { + float cutoff, intensity, hasHistory, frameIndex; + float marchSteps, marchDistance, marchThickness, refineSteps; + float pad0, pad1, pad2, pad3; + }; + PCB p{ + 0.65f, 0.95f, + data.hasHistory ? 1.f : 0.f, + float(Device.dwFrame & 1023), + q.steps, q.distance, q.thickness, q.refine, + 0.f, 0.f, 0.f, 0.f}; + auto* ssrCB = cache.GetOrCreateVolatileCB("SSR", "SSRParams_v45", sizeof(PCB), data.device); + if (ssrCB) + cmd->writeBuffer(ssrCB, &p, sizeof(p)); + + static nvrhi::TextureHandle s_zeroMotion; + if (!s_zeroMotion) + { + nvrhi::TextureDesc td; + td.width = 1; td.height = 1; + td.format = nvrhi::Format::RG16_FLOAT; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + s_zeroMotion = nv->createTexture(td); + } + + nvrhi::ITexture* sky0 = nullptr; + nvrhi::ITexture* sky1 = nullptr; + ResolveEnvSkyCubes(data.device, sky0, sky1); + if (!sky0) sky0 = cache.GetDummyCubeMap(nv); + if (!sky1) sky1 = sky0; + + nvrhi::ITexture* histSrv = histTex ? histTex : cache.GetDummyContactHistory(nv); + if (data.st->historyNeedsClear && histSrv) + { + cmd->clearTextureFloat(histSrv, nvrhi::AllSubresources, nvrhi::Color(0.f)); + data.st->historyNeedsClear = false; + } + + static int s_ssrLog = 0; + if ((s_ssrLog++ % 300) == 0) + Msg("* [SSR] exec v45 resolve→blur→apply"); + + // 1) Resolve + cmd->clearTextureFloat(ssrTex, nvrhi::AllSubresources, nvrhi::Color(0.f)); + { + nvrhi::ITexture* depthSrv = data.st->depthCopy ? data.st->depthCopy.Get() : depthTex; + BindingSetBuilder bsb(*vsR, *psR, nv, "SSR.Resolve"); + if (staticGlobalsCB) + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + if (ssrCB) + bsb.ConstantBuffer("SSRParams", ssrCB); + bsb.Texture("g_Color", data.st->litCopy ? data.st->litCopy.Get() : color) + .Texture("g_Normal", normalTex) + .Texture("g_Base", baseTex) + .Texture("g_History", histSrv) + .Texture("g_Motion", s_zeroMotion) + .Texture("g_SceneColor", data.st->colorCopy) + .Texture("g_SceneDepth", depthSrv) + .Texture("s_env0", sky0) + .Texture("s_env1", sky1); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->layout, nv); + if (!set) + { + static bool s_loggedBind = false; + if (!s_loggedBind) + { + Msg("! [SSR] Resolve binding failed — scene passthrough"); + s_loggedBind = true; + } + // Never leave rt_SSR unwritten (transient-era footgun). + nvrhi::ITexture* litSrc = data.st->litCopy ? data.st->litCopy.Get() : color; + if (litSrc && out) + { + cmd->setTextureState(litSrc, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(out, nvrhi::TextureSlice(), litSrc, nvrhi::TextureSlice()); + } + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + return; + } + DrawSSRFullscreen(cmd, nv, data.st->pipeline, set, ssrTex, data.width, data.height, "SSR_Resolve_v45"); + cmd->setTextureState(ssrTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + + // 2) Bilateral denoise + struct alignas(16) BlurCB { float texelX, texelY, blurIntensity, pad; }; + BlurCB bcb{1.f / float(data.width), 1.f / float(data.height), q.blurIntensity, 0.f}; + auto* blurCB = cache.GetOrCreateVolatileCB("SSR", "BlurParams_v45", sizeof(BlurCB), data.device); + if (blurCB) + cmd->writeBuffer(blurCB, &bcb, sizeof(bcb)); + { + nvrhi::ITexture* depthSrv = data.st->depthCopy ? data.st->depthCopy.Get() : depthTex; + BindingSetBuilder bsb(*vsR, *psBlurR, nv, "SSR.Blur"); + if (blurCB) + bsb.ConstantBuffer("SSRBlurParams", blurCB); + bsb.Texture("g_SSR", ssrTex) + .Texture("g_Depth", depthSrv) + .Texture("g_Normal", normalTex); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->blurLayout, nv); + if (set) + DrawSSRFullscreen(cmd, nv, data.st->blurPipeline, set, blurTex, data.width, data.height, "SSR_Blur_v45"); + else + { + cmd->setTextureState(ssrTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(blurTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(blurTex, nvrhi::TextureSlice(), ssrTex, nvrhi::TextureSlice()); + } + cmd->setTextureState(blurTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + + // Store blurred SSR into history write slot for next frame temporal hold + { + cmd->setTextureState(blurTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(ssrTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(ssrTex, nvrhi::TextureSlice(), blurTex, nvrhi::TextureSlice()); + cmd->setTextureState(ssrTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(blurTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + data.st->historyIndex ^= 1u; + data.st->hasHistory = true; + } + + // 3) Apply: read litCopy, write out — never the same texture + { + nvrhi::ITexture* litSrc = data.st->litCopy ? data.st->litCopy.Get() : color; + BindingSetBuilder bsb(*vsR, *psApplyR, nv, "SSR.Apply"); + bsb.Texture("g_Color", litSrc).Texture("g_SSR", blurTex); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->applyLayout, nv); + if (set) + DrawSSRFullscreen(cmd, nv, data.st->applyPipeline, set, out, data.width, data.height, "SSR_Apply_v45"); + else if (litSrc && out) + { + cmd->setTextureState(litSrc, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(out, nvrhi::TextureSlice(), litSrc, nvrhi::TextureSlice()); + } + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + }); + + return pd.output; +} + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.h new file mode 100644 index 00000000000..062c14ac256 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.h @@ -0,0 +1,53 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes +{ + +struct SSRPassState +{ + nvrhi::GraphicsPipelineHandle pipeline; // resolve (SSR buffer only) + nvrhi::BindingLayoutHandle layout; + nvrhi::GraphicsPipelineHandle blurPipeline; + nvrhi::BindingLayoutHandle blurLayout; + nvrhi::GraphicsPipelineHandle applyPipeline; + nvrhi::BindingLayoutHandle applyLayout; + nvrhi::TextureHandle colorCopy; // pre-wet reflection source + nvrhi::TextureHandle litCopy; // post-wet lit scene for apply (never R/W same as out) + nvrhi::TextureHandle depthCopy; // typeless D32 SRV copy (Metal-safe, like water SSR) + nvrhi::TextureHandle outputTex; // persistent composite out (never FG-aliased) + nvrhi::TextureHandle blurTex; // persistent blur RT + nvrhi::TextureHandle history[2]; + u32 historyW = 0; + u32 historyH = 0; + u32 historyIndex = 0; + u32 pipeVersion = 0; + bool hasHistory = false; + bool historyNeedsClear = false; + bool initialized = false; +}; + +// sceneColor: composite target (may be post-wet/AO) +// reflectionColor: lit scene to sample for reflections (pre-wet) +framegraph::VirtualResourceHandle setupSSRPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle reflectionColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle normal, + framegraph::VirtualResourceHandle baseColor, + framegraph::VirtualResourceHandle worldPos, + framegraph::VirtualResourceHandle motionVectors, + u32 width, + u32 height, + bool hasPrevFrame, + SSRPassState& state); + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.cpp new file mode 100644 index 00000000000..d8e754fb1b8 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.cpp @@ -0,0 +1,75 @@ +#include "stdafx.h" +#include "SceneReflectionPassSetup.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +struct SceneReflectionCaptureData +{ + VirtualResourceHandle src; + VirtualResourceHandle dst; +}; +} // namespace + +VirtualResourceHandle setupSceneReflectionCapture( + FrameGraph& fg, + VirtualResourceHandle litSceneColor, + u32 width, + u32 height) +{ + if (!litSceneColor.is_valid() || width == 0 || height == 0) + return litSceneColor; + + ResourceDesc desc; + desc.type = ResourceDesc::Type::Texture2D; + desc.width = width; + desc.height = height; + desc.format = nvrhi::Format::RGBA16_FLOAT; + desc.isRenderTarget = true; + // Must NOT be transient — aliasing reused normal/worldPos memory → green/purple SSR + desc.isTransient = false; + desc.debugName = "rt_SceneReflection"; + auto dst = fg.CreateTexture("rt_SceneReflection", desc); + + (void)fg.addCallbackPass( + "SceneReflectionCapture", + [litSceneColor, dst](FrameGraph& builder, PassHandle passHandle, SceneReflectionCaptureData& data) + { + RenderPassBuilder pb(builder, passHandle); + data.src = pb.read(litSceneColor, ResourceState::CopySource); + data.dst = pb.write(dst, ResourceState::CopyDest); + pb.sideEffects(); + }, + [](const SceneReflectionCaptureData& data, const FrameGraph& graph, fg::RenderContext* ctx) + { + if (!ctx) + return; + auto* cmd = ctx->GetCommandList(); + auto* src = graph.GetPhysicalTexture(data.src); + auto* dst = graph.GetPhysicalTexture(data.dst); + if (!cmd || !src || !dst || src == dst) + return; + + cmd->setTextureState(src, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(dst, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(dst, nvrhi::TextureSlice(), src, nvrhi::TextureSlice()); + cmd->setTextureState(dst, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + + static bool s_logged = false; + if (!s_logged) + { + Msg("* [SceneReflection] Captured lit scene → rt_SceneReflection (wet/SSR source)"); + s_logged = true; + } + }); + + return dst; +} + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.h new file mode 100644 index 00000000000..175820ad8df --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes +{ + +// Snapshot of fully-lit scene color BEFORE wet/AO darkening. +// This is the shared reflection source for: +// - wet surface specular (screen-space + sky IBL fallback) +// - fullscreen SSR +// Named RT: "rt_SceneReflection" +framegraph::VirtualResourceHandle setupSceneReflectionCapture( + framegraph::FrameGraph& fg, + framegraph::VirtualResourceHandle litSceneColor, + u32 width, + u32 height); + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.cpp b/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.cpp index 5520fc11ec6..c86586a7da7 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.cpp @@ -8,11 +8,15 @@ namespace xray::render::fg::passes { using namespace xray::render::fg; void GetSunLightData(SunLightData& outSun, float hdrIntensity) { + outSun.color.set(0.f, 0.f, 0.f); + outSun.direction.set(0.f, -1.f, 0.f); + outSun.intensity = 0.f; + auto* sun = static_cast(Lights.sun._get()); if (sun) { outSun.color.set(sun->color.r, sun->color.g, sun->color.b); outSun.direction = sun->direction; - outSun.intensity = 1.f; + outSun.intensity = (hdrIntensity > 0.f) ? hdrIntensity : 1.f; } } diff --git a/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.h b/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.h index 56040273ff1..1f7bf708dce 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.h +++ b/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.h @@ -4,15 +4,30 @@ #include "xrCore/xrCore.h" #include "xrCore/_vector3d.h" #include "xrCore/_matrix.h" +#include "Layers/xrRender/xrRender_console.h" +#include "Layers/xrRender/ClusteredLightManager.h" +#include "xrEngine/device.h" // Forward declarations of X-Ray engine globals extern ECORE_API float ps_r2_sun_lumscale_hemi; +extern ECORE_API float ps_r2_sun_lumscale_amb; +extern ECORE_API float ps_r2_sun_normal_bias; +extern ECORE_API float ps_r2_sun_soft; +extern ECORE_API float ps_r2_sun_blocker; +extern ECORE_API float ps_r2_sun_contact; extern ENGINE_API int ps_fg_pbr_diffuse_mode; extern ENGINE_API Fvector4 ps_dev_param_1; extern ENGINE_API Fvector4 ps_dev_param_2; extern ENGINE_API Fvector4 ps_dev_param_3; extern ENGINE_API Fvector4 ps_dev_param_4; extern ENGINE_API float psHUD_FOV; +extern ENGINE_API int ps_r_contact_shadows; +extern ENGINE_API float ps_r_contact_shadows_length; +extern ENGINE_API int ps_r_sky_ibl; +extern ENGINE_API float ps_r_sky_ibl_intensity; +extern ENGINE_API int ps_r_foliage_sss; +extern ENGINE_API float ps_r_foliage_sss_intensity; +extern ECORE_API u32 ps_r_sun_quality; namespace xray::render { namespace fg { extern float r__dtex_range; // Detail texture range (defined in TextureDescrManager.cpp) @@ -21,6 +36,17 @@ namespace xray::render { namespace xray::render::fg::passes { +// Filled by CascadedShadows pass; consumed by FillGlobalConstants +struct ShadowCascadeGPUData +{ + Fmatrix matrices[4]; + Fvector4 splits; + Fvector2 viewShadowProj; // far-cascade UV direction for accum_sun_far fade + float mapSize = 2048.f; + bool valid = false; +}; +extern ShadowCascadeGPUData g_ShadowCascadeGPUData; + // ══════════════════════════════════════════════════════════ // PBR TEXTURE SLOT ASSIGNMENTS (Forward+ Rendering) // ══════════════════════════════════════════════════════════ @@ -132,24 +158,78 @@ static_assert(sizeof(StaticGlobals) == 848, "StaticGlobals must be 848 bytes"); // Legacy alias for compatibility using GlobalConstants = StaticGlobals; +// Keep cluster_params / cluster_scales in sync with ClusteredLightManager. +// Every write to Frame/StaticGlobals must call this (or BuildStaticGlobals). +inline void FillClusterParams(StaticGlobals& cb) +{ + auto& clm = ::xray::render::fg::ClusteredLightManager::Instance(); + const u32 w = std::max(1u, (u32)Device.dwWidth); + const u32 h = std::max(1u, (u32)Device.dwHeight); + const float zNear = VIEWPORT_NEAR; + const float zFar = g_pGamePersistent + ? g_pGamePersistent->Environment().CurrentEnv.far_plane + : 500.f; + + if (clm.IsReady()) + { + const ::xray::render::fg::ClusterCB cluster = clm.BuildClusterCB(w, h, zNear, zFar); + cb.cluster_params = cluster.gridDims; // xyz = tilesX/Y/slices, w = numLights + cb.cluster_scales.set(zNear, zFar, cluster.depthParams.z, cluster.depthParams.w); + } + else + { + const u32 tilesX = (w + ::xray::render::fg::CLUSTER_TILE_SIZE - 1) / ::xray::render::fg::CLUSTER_TILE_SIZE; + const u32 tilesY = (h + ::xray::render::fg::CLUSTER_TILE_SIZE - 1) / ::xray::render::fg::CLUSTER_TILE_SIZE; + cb.cluster_params.set( + static_cast(tilesX), + static_cast(tilesY), + static_cast(::xray::render::fg::CLUSTER_NUM_SLICES), + 0.0f); + cb.cluster_scales.set(zNear, zFar, 1.0f, static_cast(::xray::render::fg::CLUSTER_TILE_SIZE)); + } +} + inline void FillGlobalConstants(GlobalConstants& cb) { cb.m_V = Device.mView; cb.m_P = Device.mProject; - cb.m_VP.mul(Device.mProject, Device.mView); - - // Timers - cb.timers.set( - Device.fTimeGlobal, // Game time - Device.fTimeDelta, // Frame delta - _sin(Device.fTimeGlobal), // sin(time) - _cos(Device.fTimeGlobal) // cos(time) - ); - - // Fog (use X-Ray's global fog state if available, otherwise defaults) - // TODO: Hook into X-Ray's CFogOfWar or environment system - cb.fog_plane.set(0.0f, 1.0f, 0.0f, 0.0f); // Plane equation - cb.fog_params.set(0.0f, 1000.0f, 0.001f, 0.0f); // near, far, density - cb.fog_color.set(0.5f, 0.5f, 0.6f, 1.0f); // Grayish-blue fog + // MUST match geometry + GPU cull (Device.mFullTransform), not a second mul. + // Remul(mProject,mView) can drift from mFullTransform and break depth↔UV SSR. + cb.m_VP = Device.mFullTransform; + + // Classic R2/R3 binder (Blender_Recorder_StandartBinding cl_times): + // (t, t*10, t/10, sin(t)) — watermove_tc / clouds use timers.z = t/10 + const float t = Device.fTimeGlobal; + cb.timers.set(t, t * 10.f, t / 10.f, _sin(t)); + + // Classic R2/R3 fog packing (Blender_Recorder_StandartBinding): + // fog_params: x=-n/(f-n), y=z=w=1/(f-n) → fog = saturate(d*w + x) + if (g_pGamePersistent) + { + const auto& env = g_pGamePersistent->Environment().CurrentEnv; + const float n = env.fog_near; + const float f = std::max(env.fog_far, n + 1.0f); + const float r = 1.0f / (f - n); + cb.fog_params.set(-n * r, r, r, r); + cb.fog_color.set(env.fog_color.x, env.fog_color.y, env.fog_color.z, + env.rain_density); + + Fvector4 plane; + const Fmatrix& M = Device.mFullTransform; + plane.x = -(M._14 + M._13); + plane.y = -(M._24 + M._23); + plane.z = -(M._34 + M._33); + plane.w = -(M._44 + M._43); + const float denom = -1.0f / _sqrt(_sqr(plane.x) + _sqr(plane.y) + _sqr(plane.z)); + plane.mul(denom); + const float B = r; + cb.fog_plane.set(-plane.x * B, -plane.y * B, -plane.z * B, 1.0f - (plane.w - n) * B); + } + else + { + cb.fog_plane.set(0.0f, 1.0f, 0.0f, 0.0f); + cb.fog_params.set(0.0f, 0.001f, 0.001f, 0.001f); + cb.fog_color.set(0.5f, 0.5f, 0.6f, 0.0f); + } // Lighting - defaults, will be overridden by FillSunConstants if sun is available cb.L_ambient.set(0.2f, 0.2f, 0.2f, 1.0f); // Ambient (placeholder) @@ -168,8 +248,13 @@ inline void FillGlobalConstants(GlobalConstants& cb) { cb.pos_decompression_params.set(HorzTan, VertTan, (2.0f * HorzTan) / (float)Device.dwWidth, (2.0f * VertTan) / (float)Device.dwHeight); cb.pos_decompression_params2.set((float)Device.dwWidth, (float)Device.dwHeight, 1.0f / (float)Device.dwWidth, 1.0f / (float)Device.dwHeight); - // Parallax mapping - cb.parallax.set(0.02f, -0.01f, 0.0f, 0.0f); // height scale, min samples, max samples, unused + // Parallax mapping: .x = height scale (r2_parallax_h; 0 disables object parallax), + // .y = bias, .z = foliage SSS intensity, .w = sky IBL intensity. Object steep + // parallax in bindless_forward.ps keys off .x>0 and requires r2_steep_parallax. + const float parallaxH = ps_r2_ls_flags.test(R2FLAG_STEEP_PARALLAX) ? ps_r2_df_parallax_h : 0.0f; + cb.parallax.set(parallaxH, -0.01f, + (ps_r_foliage_sss != 0) ? ps_r_foliage_sss_intensity : 0.0f, + (ps_r_sky_ibl != 0) ? ps_r_sky_ibl_intensity : 0.0f); // Screen resolution (for UI shaders and other effects) cb.screen_res.set( @@ -179,32 +264,77 @@ inline void FillGlobalConstants(GlobalConstants& cb) { 1.0f / (float)Device.dwHeight // w = 1/height ); - // Clear padding to avoid uninitialized memory warnings cb.hud_fov = psHUD_FOV; - cb.padding3 = 0.0f; + // Screen-space contact shadow length (0 = disabled); packed as StaticGlobals.padding3 + cb.padding3 = (ps_r_contact_shadows != 0) ? ps_r_contact_shadows_length : 0.0f; // ═══════════════════════════════════════════════════════ // FORWARD+ EXTENSIONS (Phase 1.3) // ═══════════════════════════════════════════════════════ - cb.m_InvVP.invert(cb.m_VP); + // Same inverse the rest of the frame / TAA / wet use — never remul+invert alone. + cb.m_InvVP = Device.mInvFullTransform; + + static bool s_loggedVp = false; + if (!s_loggedVp) + { + Fmatrix remul; + remul.mul(Device.mProject, Device.mView); + const float d = + _abs(remul._11 - Device.mFullTransform._11) + + _abs(remul._22 - Device.mFullTransform._22) + + _abs(remul._33 - Device.mFullTransform._33) + + _abs(remul._43 - Device.mFullTransform._43); + Msg("* [SSR/VP] StaticGlobals m_VP := mFullTransform (remul delta=%.6f)", d); + s_loggedVp = true; + } - for (int i = 0; i < 4; i++) - cb.shadow_matrices[i].identity(); - cb.cascade_splits.set(10.0f, 50.0f, 150.0f, 500.0f); + if (g_ShadowCascadeGPUData.valid) + { + for (int i = 0; i < 4; i++) + cb.shadow_matrices[i] = g_ShadowCascadeGPUData.matrices[i]; + cb.cascade_splits = g_ShadowCascadeGPUData.splits; + // .w = UV rim blend width; keep if zero (older path) + if (cb.cascade_splits.w <= 0.f) + cb.cascade_splits.w = 0.12f; + } + else + { + for (int i = 0; i < 4; i++) + cb.shadow_matrices[i].identity(); + cb.cascade_splits.set(10.0f, 50.0f, 150.0f, 500.0f); + } - // Cluster grid parameters (PLACEHOLDER - Phase 5: will be populated from light culling pass) - cb.cluster_params.set(16.0f, 16.0f, 24.0f, 0.0f); // 16×16×24 grid, 0 lights for now - cb.cluster_scales.set(0.1f, 500.0f, 1.0f, 1.0f); // z_near, z_far, scale_x, scale_y + // Cluster grid: filled by FillClusterParams (must run on every StaticGlobals write) + FillClusterParams(cb); - // Camera direction vector (for lighting calculations) - cb.camera_direction.set(Device.vCameraDirection.x, Device.vCameraDirection.y, - Device.vCameraDirection.z, 0.0f); + // Camera direction; .w = soft particles enable (r2_soft_particles) + cb.camera_direction.set( + Device.vCameraDirection.x, Device.vCameraDirection.y, Device.vCameraDirection.z, + ps_r2_ls_flags.test(R2FLAG_SOFT_PARTICLES) ? 1.0f : 0.0f); cb.dev_param_1 = ps_dev_param_1; cb.dev_param_2 = ps_dev_param_2; cb.dev_param_3 = ps_dev_param_3; cb.dev_param_4 = ps_dev_param_4; + + // r_shadow_debug overlay mode -> shadow_sampling.h ShadowDebugColor + cb.dev_param_3.z = float(ps_r_shadow_debug); + // r2_sun_normal_bias (meters) -> shadow_sampling.h NormalOffsetWorld + cb.dev_param_3.w = ps_r2_sun_normal_bias; + + // PCSS filtering controls -> shadow_sampling.h SampleCascadePCSS. + // (dev_param_4 was previously view_shadow_proj, which no shader reads.) + cb.dev_param_4.x = ps_r2_sun_soft; // max penumbra (texels) — softness + cb.dev_param_4.y = ps_r2_sun_blocker; // blocker-search spacing (texels) + cb.dev_param_4.z = ps_r2_sun_contact; // min penumbra (texels) — contact sharpness + + if (g_ShadowCascadeGPUData.valid) + { + cb.dev_param_3.x = g_ShadowCascadeGPUData.mapSize; + // r2_sun_quality → soft PCSS tier in shadow_sampling.h (dev_param_3.y) + cb.dev_param_3.y = float(ps_r_sun_quality); + } } inline void FillDynamicTransforms(DynamicTransforms& cb, Fmatrix m_W = Fidentity) { @@ -217,13 +347,32 @@ inline void FillDynamicTransforms(DynamicTransforms& cb, Fmatrix m_W = Fidentity cb.hemi_cube_neg_faces.set(0.19919f, 0.00392f, 0.09922f, 0.0f); } +// Soft rain / thunderbolt FX (effects_world_soft.*) +struct alignas(16) SoftFXConstants { + Fmatrix m_WVP; + Fvector4 EyePos; +}; + +inline void FillSoftFXConstants(SoftFXConstants& cb) +{ + cb.m_WVP.mul(Device.mProject, Device.mView); + cb.EyePos.set(Device.vCameraPosition.x, Device.vCameraPosition.y, Device.vCameraPosition.z, 1.f); +} + struct SunLightData { Fvector color; // Sun color (RGB) - Fvector direction; // Sun direction (world space, pointing toward light) + Fvector direction; // Sun travel direction (world space, sun → surface; env.sun_dir) float intensity; // HDR intensity multiplier (1.0 = SDR, 2.0+ = HDR) }; inline void FillSunConstants(StaticGlobals& cb, const SunLightData& sun) { + if (!g_pGamePersistent) + { + cb.L_sun_color.set(0.f, 0.f, 0.f); + cb.L_sun_dir_w.set(0.f, -1.f, 0.f); + return; + } + const auto& desc = g_pGamePersistent->Environment().CurrentEnv; cb.L_sun_color.set( @@ -232,18 +381,20 @@ inline void FillSunConstants(StaticGlobals& cb, const SunLightData& sun) { sun.color.z * sun.intensity ); - cb.L_sun_dir_w.set( - sun.direction.x, - sun.direction.y, - sun.direction.z - ); + Fvector dir = sun.direction; + if (dir.magnitude() < 1e-4f) + dir.set(0.f, -1.f, 0.f); + dir.normalize_safe(); + cb.L_sun_dir_w.set(dir.x, dir.y, dir.z); cb.L_ambient.set( - desc.ambient.x, - desc.ambient.y, - desc.ambient.z + desc.ambient.x * ps_r2_sun_lumscale_amb, + desc.ambient.y * ps_r2_sun_lumscale_amb, + desc.ambient.z * ps_r2_sun_lumscale_amb, + desc.weight ); + // 3-arg set keeps L_hemi_color.w (ps_r2_sun_lumscale_hemi from FillGlobalConstants) cb.L_hemi_color.set( desc.hemi_color.x, desc.hemi_color.y, @@ -259,6 +410,7 @@ inline StaticGlobals BuildStaticGlobals(float hdrIntensity = 2.0f) { SunLightData sunData; GetSunLightData(sunData, hdrIntensity); FillSunConstants(sg, sunData); + FillClusterParams(sg); return sg; } diff --git a/src/Layers/xrRender/FrameGraphPasses/ShadowCascadeFit.cpp b/src/Layers/xrRender/FrameGraphPasses/ShadowCascadeFit.cpp new file mode 100644 index 00000000000..6bfd27bf78f --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/ShadowCascadeFit.cpp @@ -0,0 +1,164 @@ +#include "stdafx.h" +#include "ShadowCascadeFit.h" +#include "Layers/xrRender/xrRender_console.h" +#include "xrEngine/device.h" + +namespace xray::render::fg::passes +{ + +namespace +{ + +void FrustumSliceCorners(float zNear, float zFar, Fvector outCorners[8]) +{ + const Fvector& C = Device.vCameraPosition; + const Fvector& D = Device.vCameraDirection; + const Fvector& R = Device.vCameraRight; + const Fvector& U = Device.vCameraTop; + + const float t = tanf(deg2rad(Device.fFOV * 0.5f)); + const float a = Device.fASPECT; + + auto corner = [&](float z, float sx, float sy) { + const float h = t * z; + const float w = h * a; + Fvector p; + p.mad(C, D, z); + p.mad(p, R, sx * w); + p.mad(p, U, sy * h); + return p; + }; + + outCorners[0] = corner(zNear, -1.f, -1.f); + outCorners[1] = corner(zNear, +1.f, -1.f); + outCorners[2] = corner(zNear, +1.f, +1.f); + outCorners[3] = corner(zNear, -1.f, +1.f); + outCorners[4] = corner(zFar, -1.f, -1.f); + outCorners[5] = corner(zFar, +1.f, -1.f); + outCorners[6] = corner(zFar, +1.f, +1.f); + outCorners[7] = corner(zFar, -1.f, +1.f); +} + +} // namespace + +void ComputeFrustumFitCascadeMatrices( + const Fvector& sunDirTowardLight, + float mapSize, + float viewZNear, + float viewZFar, + u32 cascadeIndex, + u32 smapResolution, + Fmatrix& outClipVP, + Fmatrix& outSampleVP) +{ + Fvector L_dir; + L_dir.set(-sunDirTowardLight.x, -sunDirTowardLight.y, -sunDirTowardLight.z); + L_dir.normalize_safe(); + + Fvector L_right, L_up; + L_right.set(1.f, 0.f, 0.f); + if (_abs(L_right.dotproduct(L_dir)) > 0.99f) + L_right.set(0.f, 0.f, 1.f); + L_up.crossproduct(L_dir, L_right); + L_up.normalize_safe(); + L_right.crossproduct(L_up, L_dir); + L_right.normalize_safe(); + + const float z0 = std::max(viewZNear, VIEWPORT_NEAR); + const float z1 = std::max(viewZFar, z0 + 1.f); + + Fvector corners[8]; + FrustumSliceCorners(z0, z1, corners); + + Fvector L_pos; + L_pos.mad(Device.vCameraPosition, L_dir, -(mapSize * 3.f + 400.f)); + + Fmatrix lightView0; + lightView0.build_camera_dir(L_pos, L_dir, L_up); + + float minZ = FLT_MAX, maxZ = -FLT_MAX; + for (int i = 0; i < 8; ++i) + { + Fvector ls; + lightView0.transform_tiny(ls, corners[i]); + minZ = std::min(minZ, ls.z); + maxZ = std::max(maxZ, ls.z); + } + + // View-independent fit: fixed ortho size per cascade, centered slightly ahead + // of the camera using ONLY the horizontal facing (pitch ignored). Frustum-slice + // fitting shrank/shifted the ortho when looking down, so off-screen tree casters + // dropped out and shadows popped with no smooth fade. A stable, pitch-independent + // footprint keeps casters in the map regardless of where you look. + float orthoSize = mapSize; + + Fvector fwdH = Device.vCameraDirection; + fwdH.y = 0.f; + if (fwdH.magnitude() > 1e-3f) + fwdH.normalize(); + else + fwdH.set(0.f, 0.f, 1.f); + Fvector centerW; + centerW.mad(Device.vCameraPosition, fwdH, orthoSize * 0.25f); + + Fvector centerLS; + lightView0.transform_tiny(centerLS, centerW); + + const float texelWorld = orthoSize / float(std::max(smapResolution, 1u)); + const float cxSnap = floorf(centerLS.x / texelWorld) * texelWorld; + const float cySnap = floorf(centerLS.y / texelWorld) * texelWorld; + + Fvector shiftWorld; + shiftWorld.mul(L_right, cxSnap); + shiftWorld.mad(shiftWorld, L_up, cySnap); + Fvector L_posFit; + L_posFit.add(L_pos, shiftWorld); + + Fmatrix lightView; + lightView.build_camera_dir(L_posFit, L_dir, L_up); + + minZ = FLT_MAX; + maxZ = -FLT_MAX; + for (int i = 0; i < 8; ++i) + { + Fvector ls; + lightView.transform_tiny(ls, corners[i]); + minZ = std::min(minZ, ls.z); + maxZ = std::max(maxZ, ls.z); + } + + const float zn = 0.1f; + const float zPad = 1.41421f * orthoSize; + float zf = (maxZ - minZ) + zPad + 50.f; + const float zPull = std::max(0.f, -minZ) + orthoSize * 0.5f + 50.f; + zf += zPull; + zf = std::max(zf, orthoSize + 100.f); + + Fmatrix zOff; + zOff.identity(); + zOff.translate(0.f, 0.f, -(minZ - zn - zPull * 0.5f)); + + Fmatrix viewOff; + viewOff.mul(zOff, lightView); + + Fmatrix lightProj; + lightProj.build_projection_ortho(orthoSize, orthoSize, zn, zn + zf); + outClipVP.mul(lightProj, viewOff); + + const float fRange = (cascadeIndex == 0) ? ps_r2_sun_depth_near_scale : ps_r2_sun_depth_far_scale; + // Small constant depth bias. Acne is handled mainly by world normal-offset + // (shadow_sampling.h NormalOffsetWorld), so keep this low to avoid detaching + // object shadows (peter-panning) that made cast shadows vanish. + const float fBias = -(0.0004f + orthoSize * 0.000006f); + Fmatrix texScaleBias; + texScaleBias.identity(); + texScaleBias._11 = 0.5f; + texScaleBias._22 = -0.5f; + texScaleBias._33 = fRange; + texScaleBias._41 = 0.5f; + texScaleBias._42 = 0.5f; + texScaleBias._43 = fBias; + outSampleVP.mul(texScaleBias, outClipVP); +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/ShadowCascadeFit.h b/src/Layers/xrRender/FrameGraphPasses/ShadowCascadeFit.h new file mode 100644 index 00000000000..6d4abe0e427 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/ShadowCascadeFit.h @@ -0,0 +1,22 @@ +#pragma once + +#include "xrCore/xrCore.h" +#include "xrCore/_matrix.h" +#include "xrCore/_vector3d.h" + +namespace xray::render::fg::passes +{ + +// Classic-style frustum-fit cascade (fixes pitch: cascades follow view frustum). +// Fits a fixed ortho of mapSize around the camera frustum slice [viewZNear, viewZFar]. +void ComputeFrustumFitCascadeMatrices( + const Fvector& sunDirTowardLight, // direction toward the sun (env.sun_dir style) + float mapSize, + float viewZNear, + float viewZFar, + u32 cascadeIndex, + u32 smapResolution, + Fmatrix& outClipVP, + Fmatrix& outSampleVP); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.cpp new file mode 100644 index 00000000000..ea156459ab6 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.cpp @@ -0,0 +1,1380 @@ +#include "stdafx.h" +#include "ShadowPassSetup.h" +#include "ShadowCascadeFit.h" +#include "PassCommon.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/Geometry/MaterialCache.h" +#include "Layers/xrRender/Bindless/MaterialBuffer.h" +#include "Layers/xrRender/Bindless/TerrainMaterialBuffer.h" +#include "Layers/xrRender/ResourceManager/FGResourceManager.h" +#include "Layers/xrRender/ResourceManager/TextureManager.h" +#include "Layers/xrRender/ResourceManager/NativeRTFactory.h" +#include "Layers/xrRender/xrRender_console.h" +#include "Layers/xrRender/FGDetailManager.h" +#include "Layers/xrRender/SkeletonCustom.h" +#include "Layers/xrRender/FSkinned.h" +#include "Layers/xrRender/SkeletonX.h" +#include "xrEngine/IGame_Persistent.h" +#include "xrEngine/Environment.h" +#include "xrEngine/device.h" +#include "xrEngine/IRenderBackend.h" +#include "Layers/xrRender/r__sector.h" +#include "Layers/xrRender/GPUCullingManager.h" + +extern ENGINE_API float ps_r3_grass_blade_height; +extern ENGINE_API float psHUD_FOV; + +namespace xray::render::fg +{ +extern int ps_r__detail_gpu; +} + +namespace xray::render::fg::passes +{ + +ShadowCascadeGPUData g_ShadowCascadeGPUData; + +u32 GetCSMResolution() +{ + u32 s = ps_r2_smapsize; + if (s < 512) + s = 512; + if (s > 8192) + s = 8192; + return s; +} + +u32 GetCSMCascadeResolution(u32 cascade) +{ + u32 base = GetCSMResolution(); + u32 div = 1u << std::min(cascade, 2u); // 1, 2, 4 + u32 s = base / div; + if (s < 512) + s = 512; + return s; +} + +namespace +{ + +// Original r2 sun cascades (render_phase_sun.cpp): 20 / 40 / 160. +// Cascade ortho footprints are derived from r2_sun_near / r2_sun_far at setup time. + +} // namespace + +void InitializeShadowPass(fg::RenderDevice* device, ShadowPassState& state) +{ + if (!device) + return; + + // Hot-upgrade: uniform Texture2DArray CSM → per-cascade Texture2D ladder + if (state.initialized && state.enabled) + { + const bool sharedArray = + state.shadowCascades[0] && + state.shadowCascades[0] == state.shadowCascades[1]; + const bool flatRes = + state.cascadeResolution[0] > 0 && + state.cascadeResolution[0] == state.cascadeResolution[1] && + GetCSMCascadeResolution(0) != GetCSMCascadeResolution(1); + if (sharedArray || flatRes) + ShutdownShadowPass(device, state); + } + + // Hot-upgrade: local shadow tiles need more volatile CB versions than CSM-only (16). + constexpr u32 kCascadeCBVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; + if (state.initialized && state.enabled && state.cascadeCB && + state.cascadeCBMaxVersions < kCascadeCBVersions) + { + nvrhi::IDevice* nv = device->GetNVRHIDevice(); + if (nv) + { + nvrhi::BufferDesc cbDesc; + cbDesc.byteSize = sizeof(ShadowCascadeCB); + cbDesc.isConstantBuffer = true; + cbDesc.isVolatile = true; + cbDesc.maxVersions = kCascadeCBVersions; + cbDesc.debugName = "ShadowCascadeCB"; + state.cascadeCB = nv->createBuffer(cbDesc); + state.cascadeCBMaxVersions = state.cascadeCB ? kCascadeCBVersions : 0; + if (state.cascadeCB) + Msg("* [ShadowPass] Upgraded ShadowCascadeCB maxVersions → %u", kCascadeCBVersions); + } + } + + // Hot-upgrade: older sessions may have CSM without billboard grass caster + if (state.initialized && state.enabled && !state.billboardGrassPipeline) + { + nvrhi::IDevice* nv = device->GetNVRHIDevice(); + auto* loader = GEnv.Render->GetShaderLoader(); + if (nv && loader) + { + auto& cache = framegraph::GetPassResourceCache(); + nvrhi::IBindingLayout* bindlessLayout = nullptr; + if (auto* backend = GEnv.Backend) + bindlessLayout = backend->GetBindlessLayout(); + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.depthFormat = nvrhi::Format::D32; + fbInfo.sampleCount = 1; + auto bbVs = loader->LoadVertexShader("detail_billboard_shadow", "main"); + auto bbPs = loader->LoadPixelShader("detail_billboard_shadow", "main"); + if (bbVs.handle && bbVs.reflection && bbPs.handle && bbPs.reflection) + { + state.billboardGrassVs = bbVs.handle; + state.billboardGrassPs = bbPs.handle; + state.billboardGrassLayout = cache.GetOrCreateBindingLayoutFromReflection( + "ShadowCascadeBillboardGrass_v2", *bbVs.reflection, *bbPs.reflection, nv); + if (state.billboardGrassLayout) + { + nvrhi::GraphicsPipelineDesc bbDesc; + bbDesc.VS = state.billboardGrassVs; + bbDesc.PS = state.billboardGrassPs; + bbDesc.primType = nvrhi::PrimitiveType::TriangleList; + bbDesc.bindingLayouts = {state.billboardGrassLayout}; + if (bindlessLayout) + bbDesc.bindingLayouts.push_back(bindlessLayout); + bbDesc.renderState.depthStencilState.setDepthTestEnable(true); + bbDesc.renderState.depthStencilState.setDepthWriteEnable(true); + bbDesc.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + bbDesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + bbDesc.renderState.rasterState.depthBias = 4; + bbDesc.renderState.rasterState.slopeScaledDepthBias = 3.0f; + state.billboardGrassPipeline = cache.GetOrCreatePipeline( + "ShadowCascadeBillboardGrass_v2", bbDesc, fbInfo, nv); + } + bbVs.reflection = nullptr; + bbPs.reflection = nullptr; + } + } + return; + } + if (state.initialized) + return; + + // Always mark initialized so a failed init never retries every frame + // (previously leaked a new 1024²×4 D32 CSM each frame → load hang / OOM). + auto fail = [&](const char* reason) { + Msg("! [ShadowPass] %s", reason); + state.initialized = true; + state.enabled = false; + }; + + nvrhi::IDevice* nv = device->GetNVRHIDevice(); + if (!nv) + { + fail("NVRHI device unavailable"); + return; + } + + auto* resMgr = device->GetFGResourceManager(); + if (!resMgr || !resMgr->GetRTFactory() || !resMgr->GetTextureManager()) + { + fail("Resource manager unavailable"); + return; + } + + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + { + fail("ShaderLoader unavailable"); + return; + } + + auto vs = loader->LoadVertexShader("shadow\\shadow_cascade", "main"); + auto ps = loader->LoadPixelShader("shadow\\shadow_cascade", "main"); + if (!vs.handle || !ps.handle || !vs.reflection || !ps.reflection) + { + fail("Failed to load shadow_cascade shaders"); + return; + } + + state.vs = vs.handle; + state.ps = ps.handle; + + auto& cache = framegraph::GetPassResourceCache(); + state.layout = cache.GetOrCreateBindingLayoutFromReflection( + "ShadowCascade_v2", *vs.reflection, *ps.reflection, nv); + if (!state.layout) + { + fail("Failed to create binding layout"); + vs.reflection = nullptr; + ps.reflection = nullptr; + return; + } + + u32 attrCount = 0; + auto* attrs = GetUnifiedVertexAttributes(attrCount); + state.inputLayout = nv->createInputLayout(attrs, attrCount, state.vs); + + nvrhi::GraphicsPipelineDesc pipeDesc; + pipeDesc.VS = state.vs; + pipeDesc.PS = state.ps; + pipeDesc.inputLayout = state.inputLayout; + pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; + pipeDesc.renderState.depthStencilState.setDepthTestEnable(true); + pipeDesc.renderState.depthStencilState.setDepthWriteEnable(true); + pipeDesc.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + pipeDesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + // Normal-offset (shadow_sampling.h) removes most acne, but on large flat surfaces + // at grazing sun angles (building faces, seabed) it is not enough and depth + // quantization shows up as parallel stripes. A moderate slope-scaled raster bias + // kills that banding at render time; kept moderate so object shadows do not detach + // (peter-panning). depthBiasClamp caps the worst-case slope contribution. + pipeDesc.renderState.rasterState.depthBias = 2; + pipeDesc.renderState.rasterState.slopeScaledDepthBias = 2.75f; + pipeDesc.renderState.rasterState.depthBiasClamp = 0.0025f; + + auto* backend = device->GetBackend(); + nvrhi::IBindingLayout* bindlessLayout = backend ? backend->GetBindlessLayout() : nullptr; + if (bindlessLayout) + pipeDesc.bindingLayouts = {state.layout, bindlessLayout}; + else + pipeDesc.bindingLayouts = {state.layout}; + + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.depthFormat = nvrhi::Format::D32; + + state.pipeline = cache.GetOrCreatePipeline("ShadowCascade_v2", pipeDesc, fbInfo, nv); + if (!state.pipeline) + { + fail("Failed to create pipeline"); + vs.reflection = nullptr; + ps.reflection = nullptr; + return; + } + QueryBindingLayoutFromPipeline(state.pipeline, state.layout); + + // Foliage CSM caster — same VS/layout, alpha-clip PS. Trees/bushes are alpha-blended + // (transparent set) so they never reach the opaque PS; this pipeline casts them as + // alpha-tested cutouts (and skips non-foliage transparents like glass/water). + { + auto foliagePs = loader->LoadPixelShader("shadow\\shadow_cascade_foliage", "main"); + if (foliagePs.handle && foliagePs.reflection) + { + state.foliagePs = foliagePs.handle; + nvrhi::GraphicsPipelineDesc foliageDesc = pipeDesc; + foliageDesc.PS = state.foliagePs; + state.foliagePipeline = cache.GetOrCreatePipeline( + "ShadowCascadeFoliage_v2", foliageDesc, fbInfo, nv); + foliagePs.reflection = nullptr; + } + if (!state.foliagePipeline) + Msg("! [ShadowPass] Foliage CSM pipeline unavailable — trees will not cast"); + } + + // Terrain CSM caster — same VS, opaque PS (terrain mat IDs ≠ g_Materials) + { + auto terrainPs = loader->LoadPixelShader("shadow\\shadow_cascade_terrain", "main"); + if (terrainPs.handle && terrainPs.reflection) + { + state.terrainPs = terrainPs.handle; + state.terrainLayout = cache.GetOrCreateBindingLayoutFromReflection( + "ShadowCascadeTerrain_v2", *vs.reflection, *terrainPs.reflection, nv); + if (state.terrainLayout) + { + nvrhi::GraphicsPipelineDesc terrainDesc = pipeDesc; + terrainDesc.PS = state.terrainPs; + if (bindlessLayout) + terrainDesc.bindingLayouts = {state.terrainLayout, bindlessLayout}; + else + terrainDesc.bindingLayouts = {state.terrainLayout}; + state.terrainPipeline = cache.GetOrCreatePipeline( + "ShadowCascadeTerrain_v2", terrainDesc, fbInfo, nv); + } + terrainPs.reflection = nullptr; + } + if (!state.terrainPipeline) + Msg("! [ShadowPass] Terrain CSM pipeline unavailable — terrain will not cast"); + } + + // Grass CSM caster (r2_sun_details) — blade geometry + alpha clip + { + auto grassVs = loader->LoadVertexShader("detail_gpu_shadow", "main"); + auto grassPs = loader->LoadPixelShader("detail_gpu_shadow", "main"); + if (grassVs.handle && grassVs.reflection && grassPs.handle && grassPs.reflection) + { + state.grassVs = grassVs.handle; + state.grassPs = grassPs.handle; + state.grassLayout = cache.GetOrCreateBindingLayoutFromReflection( + "ShadowCascadeGrass_v2", *grassVs.reflection, *grassPs.reflection, nv); + if (state.grassLayout) + { + nvrhi::VertexAttributeDesc grassAttrs[] = { + nvrhi::VertexAttributeDesc() + .setName("POSITION") + .setFormat(nvrhi::Format::RGB32_FLOAT) + .setOffset(0) + .setElementStride(sizeof(FGDetailManager::BladeVertex)), + nvrhi::VertexAttributeDesc() + .setName("TEXCOORD") + .setFormat(nvrhi::Format::RG32_FLOAT) + .setOffset(12) + .setElementStride(sizeof(FGDetailManager::BladeVertex)), + nvrhi::VertexAttributeDesc() + .setName("COLOR") + .setFormat(nvrhi::Format::R32_FLOAT) + .setArraySize(2) + .setOffset(20) + .setElementStride(sizeof(FGDetailManager::BladeVertex)), + }; + state.grassInputLayout = nv->createInputLayout(grassAttrs, 3, state.grassVs); + + nvrhi::GraphicsPipelineDesc grassDesc; + grassDesc.VS = state.grassVs; + grassDesc.PS = state.grassPs; + grassDesc.inputLayout = state.grassInputLayout; + grassDesc.primType = nvrhi::PrimitiveType::TriangleList; + grassDesc.bindingLayouts = {state.grassLayout}; + if (bindlessLayout) + grassDesc.bindingLayouts.push_back(bindlessLayout); + grassDesc.renderState.depthStencilState.setDepthTestEnable(true); + grassDesc.renderState.depthStencilState.setDepthWriteEnable(true); + grassDesc.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + grassDesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + grassDesc.renderState.rasterState.depthBias = 4; + grassDesc.renderState.rasterState.slopeScaledDepthBias = 3.0f; + + state.grassPipeline = cache.GetOrCreatePipeline( + "ShadowCascadeGrass_v2", grassDesc, fbInfo, nv); + } + grassVs.reflection = nullptr; + grassPs.reflection = nullptr; + } + if (!state.grassPipeline) + Msg("! [ShadowPass] Grass CSM pipeline unavailable — grass will not cast"); + } + + // CoP billboard detail caster (r__detail_gpu 0 + r2_sun_details) + { + auto bbVs = loader->LoadVertexShader("detail_billboard_shadow", "main"); + auto bbPs = loader->LoadPixelShader("detail_billboard_shadow", "main"); + if (bbVs.handle && bbVs.reflection && bbPs.handle && bbPs.reflection) + { + state.billboardGrassVs = bbVs.handle; + state.billboardGrassPs = bbPs.handle; + state.billboardGrassLayout = cache.GetOrCreateBindingLayoutFromReflection( + "ShadowCascadeBillboardGrass_v2", *bbVs.reflection, *bbPs.reflection, nv); + if (state.billboardGrassLayout) + { + nvrhi::GraphicsPipelineDesc bbDesc; + bbDesc.VS = state.billboardGrassVs; + bbDesc.PS = state.billboardGrassPs; + bbDesc.primType = nvrhi::PrimitiveType::TriangleList; + bbDesc.bindingLayouts = {state.billboardGrassLayout}; + if (bindlessLayout) + bbDesc.bindingLayouts.push_back(bindlessLayout); + bbDesc.renderState.depthStencilState.setDepthTestEnable(true); + bbDesc.renderState.depthStencilState.setDepthWriteEnable(true); + bbDesc.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + bbDesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + bbDesc.renderState.rasterState.depthBias = 4; + bbDesc.renderState.rasterState.slopeScaledDepthBias = 3.0f; + state.billboardGrassPipeline = cache.GetOrCreatePipeline( + "ShadowCascadeBillboardGrass_v2", bbDesc, fbInfo, nv); + } + bbVs.reflection = nullptr; + bbPs.reflection = nullptr; + } + if (!state.billboardGrassPipeline) + Msg("! [ShadowPass] Billboard grass CSM pipeline unavailable"); + } + + // Actor/HUD skinned casters → near cascade (r__actor_shadow) + { + auto skVs = loader->LoadVertexShader("shadow\\shadow_cascade_skinned", "main"); + auto sk4Vs = loader->LoadVertexShader("shadow\\shadow_cascade_skinned_4w", "main"); + if (skVs.handle && skVs.reflection && ps.handle && ps.reflection) + { + state.skinnedVs = skVs.handle; + state.skinnedLayout = cache.GetOrCreateBindingLayoutFromReflection( + "ShadowCascadeSkinned_v2", *skVs.reflection, *ps.reflection, nv); + if (state.skinnedLayout) + { + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA16_SNORM).setOffset(0).setElementStride(24), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(8).setElementStride(24), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(12).setElementStride(24), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(24), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG16_SNORM).setOffset(20).setElementStride(24), + }; + // Match SkinningPass nonHQ layout via shader reflection — createInputLayout from VS + state.skinnedInputLayout = nv->createInputLayout(attribs, 5, state.skinnedVs); + + nvrhi::GraphicsPipelineDesc skDesc; + skDesc.VS = state.skinnedVs; + skDesc.PS = state.ps; + skDesc.inputLayout = state.skinnedInputLayout; + skDesc.primType = nvrhi::PrimitiveType::TriangleList; + skDesc.bindingLayouts = {state.skinnedLayout}; + if (bindlessLayout) + skDesc.bindingLayouts.push_back(bindlessLayout); + skDesc.renderState.depthStencilState.setDepthTestEnable(true); + skDesc.renderState.depthStencilState.setDepthWriteEnable(true); + skDesc.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + skDesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + skDesc.renderState.rasterState.depthBias = 4; + skDesc.renderState.rasterState.slopeScaledDepthBias = 3.0f; + state.skinnedPipeline = cache.GetOrCreatePipeline("ShadowCascadeSkinned_v2", skDesc, fbInfo, nv); + } + skVs.reflection = nullptr; + } + if (sk4Vs.handle && sk4Vs.reflection && ps.handle && ps.reflection) + { + state.skinned4wVs = sk4Vs.handle; + state.skinned4wLayout = cache.GetOrCreateBindingLayoutFromReflection( + "ShadowCascadeSkinned4W_v2", *sk4Vs.reflection, *ps.reflection, nv); + if (state.skinned4wLayout) + { + constexpr u32 stride4 = 40; + nvrhi::VertexAttributeDesc attribs4[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(0).setElementStride(stride4), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride4), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(20).setElementStride(stride4), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride4), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride4), + nvrhi::VertexAttributeDesc().setName("BLENDINDICES").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(36).setElementStride(stride4), + }; + state.skinned4wInputLayout = nv->createInputLayout(attribs4, 6, state.skinned4wVs); + + nvrhi::GraphicsPipelineDesc sk4Desc; + sk4Desc.VS = state.skinned4wVs; + sk4Desc.PS = state.ps; + sk4Desc.inputLayout = state.skinned4wInputLayout; + sk4Desc.primType = nvrhi::PrimitiveType::TriangleList; + sk4Desc.bindingLayouts = {state.skinned4wLayout}; + if (bindlessLayout) + sk4Desc.bindingLayouts.push_back(bindlessLayout); + sk4Desc.renderState.depthStencilState.setDepthTestEnable(true); + sk4Desc.renderState.depthStencilState.setDepthWriteEnable(true); + sk4Desc.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + sk4Desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + sk4Desc.renderState.rasterState.depthBias = 4; + sk4Desc.renderState.rasterState.slopeScaledDepthBias = 3.0f; + state.skinned4wPipeline = cache.GetOrCreatePipeline("ShadowCascadeSkinned4W_v2", sk4Desc, fbInfo, nv); + } + sk4Vs.reflection = nullptr; + } + + // Additional world-skinned formats so NPCs/mutants (which rarely use the + // nonHQ/4W layouts) cast into every cascade. Layouts mirror SkinningPass. + auto makeSkinnedShadowPipe = [&](const char* vsName, const char* dbgName, + u32 stride, nvrhi::Format tcFormat, + nvrhi::ShaderHandle& outVs, nvrhi::BindingLayoutHandle& outLayout, + nvrhi::InputLayoutHandle& outIL, nvrhi::GraphicsPipelineHandle& outPipe) { + auto v = loader->LoadVertexShader(vsName, "main"); + if (!v.handle || !v.reflection || !ps.handle || !ps.reflection) + return; + outVs = v.handle; + outLayout = cache.GetOrCreateBindingLayoutFromReflection(dbgName, *v.reflection, *ps.reflection, nv); + if (outLayout) + { + nvrhi::VertexAttributeDesc a[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(20).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(tcFormat).setOffset(28).setElementStride(stride), + }; + outIL = nv->createInputLayout(a, 5, outVs); + nvrhi::GraphicsPipelineDesc d; + d.VS = outVs; + d.PS = state.ps; + d.inputLayout = outIL; + d.primType = nvrhi::PrimitiveType::TriangleList; + d.bindingLayouts = {outLayout}; + if (bindlessLayout) + d.bindingLayouts.push_back(bindlessLayout); + d.renderState.depthStencilState.setDepthTestEnable(true); + d.renderState.depthStencilState.setDepthWriteEnable(true); + d.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + d.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + d.renderState.rasterState.depthBias = 4; + d.renderState.rasterState.slopeScaledDepthBias = 3.0f; + outPipe = cache.GetOrCreatePipeline(dbgName, d, fbInfo, nv); + } + v.reflection = nullptr; + }; + + makeSkinnedShadowPipe("shadow\\shadow_cascade_skinned_hq", "ShadowCascadeSkinnedHQ_v2", + 36, nvrhi::Format::RG32_FLOAT, state.skinnedHqVs, state.skinnedHqLayout, + state.skinnedHqInputLayout, state.skinnedHqPipeline); + makeSkinnedShadowPipe("shadow\\shadow_cascade_skinned_2w", "ShadowCascadeSkinned2W_v2", + 44, nvrhi::Format::RGBA32_FLOAT, state.skinned2wVs, state.skinned2wLayout, + state.skinned2wInputLayout, state.skinned2wPipeline); + makeSkinnedShadowPipe("shadow\\shadow_cascade_skinned_3w", "ShadowCascadeSkinned3W_v2", + 44, nvrhi::Format::RGBA32_FLOAT, state.skinned3wVs, state.skinned3wLayout, + state.skinned3wInputLayout, state.skinned3wPipeline); + + if (!state.skinnedPipeline) + Msg("! [ShadowPass] Actor skinned CSM pipeline unavailable"); + } + + nvrhi::BufferDesc cbDesc; + cbDesc.byteSize = sizeof(ShadowCascadeCB); + cbDesc.isConstantBuffer = true; + cbDesc.isVolatile = true; + // CSM (3) + HUD + rain + up to MAX_LOCAL_SHADOW_TILES writes/frame + cbDesc.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; + cbDesc.debugName = "ShadowCascadeCB"; + state.cascadeCB = nv->createBuffer(cbDesc); + if (!state.cascadeCB) + { + fail("Failed to create cascade CB"); + vs.reflection = nullptr; + ps.reflection = nullptr; + return; + } + state.cascadeCBMaxVersions = cbDesc.maxVersions; + + { + nvrhi::BufferDesc grassCbDesc; + grassCbDesc.byteSize = sizeof(GrassShadowCB); + grassCbDesc.isConstantBuffer = true; + grassCbDesc.isVolatile = true; + grassCbDesc.maxVersions = 16; + grassCbDesc.debugName = "GrassShadowCB"; + state.grassCB = nv->createBuffer(grassCbDesc); + } + + // Resolution ladder: separate Texture2D maps (c0 full, c1 ½, c2 ¼). + // Bind as g_ShadowMap0/1/2 — not a shared array — so each cascade keeps + // its own resolution without layout/slot mismatches. + static const char* kCascadeNames[kCSMCascadeCount] = { + "rt_CascadedShadow0", "rt_CascadedShadow1", "rt_CascadedShadow2"}; + for (u32 i = 0; i < kCSMCascadeCount; ++i) + { + const u32 cres = GetCSMCascadeResolution(i); + state.cascadeResolution[i] = cres; + state.shadowCascadeHandle[i] = resMgr->GetRTFactory()->CreateShadowMap( + cres, true, kCascadeNames[i]); + state.shadowCascades[i] = + resMgr->GetTextureManager()->GetNVRHITexture(state.shadowCascadeHandle[i]); + if (!state.shadowCascades[i]) + { + fail(make_string("Failed to create CSM cascade %u (%ux%u)", i, cres, cres).c_str()); + vs.reflection = nullptr; + ps.reflection = nullptr; + return; + } + } + state.shadowArrayHandle = state.shadowCascadeHandle[0]; + state.shadowArray = state.shadowCascades[0]; + + state.hudShadowHandle = resMgr->GetRTFactory()->CreateCascadedShadowMap( + ShadowPassState::kHUDShadowResolution, 1, true, "rt_HUDShadow"); + state.hudShadowMap = resMgr->GetTextureManager()->GetNVRHITexture(state.hudShadowHandle); + if (!state.hudShadowMap) + Msg("! [ShadowPass] HUD shadow map create failed — HUD self-shadow disabled"); + + state.initialized = true; + state.enabled = true; + Msg("* [ShadowPass] Init: OK (CSM ladder %u/%u/%u, HUD %ux%u)", + state.cascadeResolution[0], state.cascadeResolution[1], state.cascadeResolution[2], + ShadowPassState::kHUDShadowResolution, ShadowPassState::kHUDShadowResolution); + + vs.reflection = nullptr; + ps.reflection = nullptr; +} + +void ShutdownShadowPass(fg::RenderDevice* device, ShadowPassState& state) +{ + if (device && device->GetFGResourceManager() && device->GetFGResourceManager()->GetRTFactory()) + { + auto* factory = device->GetFGResourceManager()->GetRTFactory(); + bool releasedAlias = false; + for (u32 i = 0; i < kCSMCascadeCount; ++i) + { + if (state.shadowCascadeHandle[i].IsValid()) + { + if (state.shadowCascadeHandle[i] == state.shadowArrayHandle) + releasedAlias = true; + factory->ReleaseRenderTarget(state.shadowCascadeHandle[i]); + } + state.shadowCascadeHandle[i] = {}; + state.shadowCascades[i] = nullptr; + state.cascadeResolution[i] = 0; + } + // Legacy uniform-array path owned only shadowArrayHandle + if (state.shadowArrayHandle.IsValid() && !releasedAlias) + factory->ReleaseRenderTarget(state.shadowArrayHandle); + state.shadowArrayHandle = {}; + if (state.hudShadowHandle.IsValid()) + factory->ReleaseRenderTarget(state.hudShadowHandle); + } + state.pipeline = nullptr; + state.layout = nullptr; + state.foliagePipeline = nullptr; + state.terrainPipeline = nullptr; + state.terrainLayout = nullptr; + state.grassPipeline = nullptr; + state.grassLayout = nullptr; + state.billboardGrassPipeline = nullptr; + state.billboardGrassLayout = nullptr; + state.skinnedPipeline = nullptr; + state.skinnedLayout = nullptr; + state.skinned4wPipeline = nullptr; + state.skinned4wLayout = nullptr; + state.inputLayout = nullptr; + state.grassInputLayout = nullptr; + state.skinnedInputLayout = nullptr; + state.skinned4wInputLayout = nullptr; + state.vs = nullptr; + state.ps = nullptr; + state.terrainPs = nullptr; + state.grassVs = nullptr; + state.grassPs = nullptr; + state.billboardGrassVs = nullptr; + state.billboardGrassPs = nullptr; + state.skinnedVs = nullptr; + state.skinned4wVs = nullptr; + state.cascadeCB = nullptr; + state.cascadeCBMaxVersions = 0; + state.grassCB = nullptr; + state.shadowArray = nullptr; + state.hudShadowMap = nullptr; + state.initialized = false; + state.enabled = false; + g_ShadowCascadeGPUData.valid = false; +} + +ShadowCascadeOutputs setupCascadedShadowPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + const BindlessForwardConfig& bindlessConfig, + MaterialCache* materialCache, + fg::FGDetailManager* detailManager, + const Fvector& sunDirection, + const xr_vector* hudBatches, + const xr_vector* worldSkinnedBatches, + fg::GPUCullingManager* gpuCulling, + ShadowPassState& state) +{ + using namespace framegraph; + + ShadowCascadeOutputs outputs; + outputs.valid = false; + + if (!device) + return outputs; + + InitializeShadowPass(device, state); + if (!state.enabled || !state.shadowArray) + return outputs; + + const bool sunOn = ps_r2_ls_flags.test(R2FLAG_SUN); + + // Cascade split distances driven by r2_sun_near / r2_sun_far (not hardcoded 20/40/160). + // Mid sits ~35% of the way from near→far so raising sun_far actually extends coverage. + const float nearDist = std::max(ps_r2_sun_near, 5.f); + const float farDist = std::max(ps_r2_sun_far, nearDist + 20.f); + const float midDist = std::clamp( + nearDist + (farDist - nearDist) * 0.35f, + nearDist + 5.f, + farDist - 5.f); + + Fvector4 splits; + // .w = UV rim blend width for fine→coarse handoff + splits.set(nearDist, midDist, farDist, 0.15f); + if (ps_r2_sun_near_border > 0.5f && ps_r2_sun_near_border < 1.f) + splits.w = std::max(0.08f, 0.55f * (1.f - ps_r2_sun_near_border)); + else + splits.w = 0.18f; + + Fvector sunDir = sunDirection; + if (sunDir.magnitude() < 1e-4f) + sunDir.set(0.3f, 0.8f, 0.2f); + sunDir.normalize_safe(); + + // Nested frustum coverage: each coarser cascade includes the near volume so + // UV handoff (fine→coarse) always has a valid parent map (classic stencil defer). + const float viewNear = VIEWPORT_NEAR; + const float zFar[kCSMCascadeCount] = {splits.x, splits.y, splits.z}; + // Ortho footprint ≈ split distance so r2_sun_* scales both selection AND map coverage. + const float mapSize[kCSMCascadeCount] = {splits.x, splits.y, splits.z}; + + const u32 smapRes = GetCSMResolution(); + for (u32 i = 0; i < kCSMCascadeCount; ++i) + { + const float sliceNear = viewNear; + const float sliceFar = zFar[i]; + ComputeFrustumFitCascadeMatrices( + sunDir, mapSize[i], sliceNear, sliceFar, i, state.cascadeResolution[i], + state.cascadeClipVP[i], state.cascadeSampleVP[i]); + } + state.cascadeSplits = splits; + + // Tight HUD/weapon light frustum (~2.5m ortho around camera near volume) + ComputeFrustumFitCascadeMatrices( + sunDir, 2.5f, VIEWPORT_NEAR, 4.0f, 0, + ShadowPassState::kHUDShadowResolution, + state.hudClipVP, state.hudSampleVP); + + g_ShadowCascadeGPUData.valid = sunOn; + g_ShadowCascadeGPUData.splits = splits; + g_ShadowCascadeGPUData.mapSize = float(smapRes); + for (u32 i = 0; i < 4; ++i) + g_ShadowCascadeGPUData.matrices[i].identity(); + for (u32 i = 0; i < kCSMCascadeCount; ++i) + g_ShadowCascadeGPUData.matrices[i] = state.cascadeSampleVP[i]; + // Slot 3 = dedicated HUD shadow sample matrix + g_ShadowCascadeGPUData.matrices[3] = state.hudSampleVP; + + // view_shadow_proj: camera forward in far-cascade UV (accum_sun_far). + // When looking steeply up/down the UV delta is unreliable — disable directional + // gate so only radial rim fade applies (otherwise shadows wash out). + { + const Fmatrix& farM = state.cascadeSampleVP[kCSMCascadeCount - 1]; + Fvector2 vsp; + vsp.set(0.f, 0.f); + if (_abs(Device.vCameraDirection.y) < 0.85f) + { + Fvector p0 = Device.vCameraPosition; + Fvector p1; + p1.mad(p0, Device.vCameraDirection, 50.f); + Fvector4 u0, u1; + farM.transform(u0, p0); + farM.transform(u1, p1); + vsp.set(u1.x - u0.x, u1.y - u0.y); + float len = _sqrt(vsp.x * vsp.x + vsp.y * vsp.y); + if (len > 1e-5f) + { + vsp.x /= len; + vsp.y /= len; + } + else + vsp.set(0.f, 0.f); + } + g_ShadowCascadeGPUData.viewShadowProj = vsp; + } + + ResourceDesc desc[kCSMCascadeCount]; + VirtualResourceHandle shadowHandles[kCSMCascadeCount]; + static const char* kImportNames[kCSMCascadeCount] = { + "rt_CascadedShadow0", "rt_CascadedShadow1", "rt_CascadedShadow2"}; + for (u32 i = 0; i < kCSMCascadeCount; ++i) + { + desc[i].type = ResourceDesc::Type::Texture2D; + desc[i].width = state.cascadeResolution[i]; + desc[i].height = state.cascadeResolution[i]; + desc[i].depth = 1; + desc[i].arraySize = 1; + desc[i].format = nvrhi::Format::D32; + desc[i].isDepthStencil = true; + desc[i].isImported = true; + desc[i].debugName = kImportNames[i]; + shadowHandles[i] = fg.ImportTexture(kImportNames[i], state.shadowCascades[i], desc[i]); + } + + struct PassData + { + VirtualResourceHandle shadowCascades[kCSMCascadeCount]; + ShadowPassState* passState = nullptr; + BindlessForwardConfig bindlessConfig; + MaterialCache* materialCache = nullptr; + FGDetailManager* detailManager = nullptr; + const xr_vector* hudBatches = nullptr; + const xr_vector* worldSkinnedBatches = nullptr; + GPUCullingManager* gpuCulling = nullptr; + fg::RenderDevice* device = nullptr; + bool sunOn = false; + bool sunDetails = false; + bool actorShadow = false; + bool hudShadow = false; + }; + + const bool sunDetails = sunOn && ps_r2_ls_flags.test(R2FLAG_SUN_DETAILS); + // World actor→CSM0 stays off (camera-attached disc). HUD casts into dedicated map. + const bool actorShadow = false; + const bool hudShadow = sunOn && state.hudShadowMap != nullptr; + + auto& passData = fg.addCallbackPass( + "CascadedShadows", + [&, sunOn, sunDetails, actorShadow, hudShadow, detailManager, hudBatches, worldSkinnedBatches, gpuCulling](FrameGraph& builder, PassHandle passHandle, PassData& data) { + RenderPassBuilder pb(builder, passHandle); + for (u32 i = 0; i < kCSMCascadeCount; ++i) + data.shadowCascades[i] = pb.write(shadowHandles[i], ResourceState::DepthStencilWrite); + data.passState = &state; + data.bindlessConfig = bindlessConfig; + data.materialCache = materialCache; + data.detailManager = detailManager; + data.hudBatches = hudBatches; + data.worldSkinnedBatches = worldSkinnedBatches; + data.gpuCulling = gpuCulling; + data.device = device; + data.sunOn = sunOn; + data.sunDetails = sunDetails; + data.actorShadow = actorShadow; + data.hudShadow = hudShadow; + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + if (!data.passState || !data.passState->enabled) + return; + + nvrhi::ITexture* shadowTex[kCSMCascadeCount] = {}; + for (u32 i = 0; i < kCSMCascadeCount; ++i) + { + shadowTex[i] = graph.GetPhysicalTexture(data.shadowCascades[i]); + if (!shadowTex[i]) + shadowTex[i] = data.passState->shadowCascades[i]; + } + nvrhi::ICommandList* cmd = ctx->GetCommandList(); + nvrhi::IDevice* nv = cmd ? cmd->getDevice() : nullptr; + if (!cmd || !nv || !shadowTex[0]) + return; + + ShadowPassState& st = *data.passState; + + for (u32 c = 0; c < kCSMCascadeCount; ++c) + { + if (!shadowTex[c]) + continue; + cmd->clearDepthStencilTexture( + shadowTex[c], nvrhi::TextureSubresourceSet(0, 1, 0, 1), true, 1.0f, false, 0); + } + + if (!data.sunOn) + return; + + if (data.materialCache) + data.materialCache->FinalizePendingMaterials(ctx); + + auto& matBuffer = bindless::MaterialBuffer::Instance(); + matBuffer.Upload(ctx); + + auto& cache = framegraph::GetPassResourceCache(); + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* backend = GEnv.Backend; + nvrhi::IBindingSet* bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; + + // HUD uses dedicated map (viewport set per-HUD res below) + // World cascades use per-cascade resolution ladder viewports. + + // Dedicated HUD/weapon shadow map (FOV-adjusted, matches HUD forward VS) + if (data.hudShadow && data.hudBatches && data.gpuCulling && st.hudShadowMap && + (st.skinnedPipeline || st.skinned4wPipeline)) + { + auto* boneBuf = data.gpuCulling->GetGlobalBoneBuffer(); + auto* skVsRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned", ".vs"); + auto* sk4VsRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_4w", ".vs"); + auto* psReflHud = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".ps"); + if (boneBuf && psReflHud) + { + cmd->clearDepthStencilTexture(st.hudShadowMap, nvrhi::TextureSubresourceSet(0, 1, 0, 1), true, 1.0f, false, 0); + + ShadowCascadeCB cbData{}; + cbData.lightVP = st.hudClipVP; + cmd->writeBuffer(st.cascadeCB, &cbData, sizeof(cbData)); + + nvrhi::FramebufferDesc fbDesc; + nvrhi::TextureSubresourceSet depthSub; + depthSub.baseArraySlice = 0; + depthSub.numArraySlices = 1; + fbDesc.setDepthAttachment(st.hudShadowMap, depthSub); + auto fb = cache.GetOrCreateFramebuffer("ShadowHUD", fbDesc, nv); + + auto dynCB = cache.GetOrCreateVolatileCB( + "ShadowCascade_v2", "HUDDyn", sizeof(DynamicTransforms), data.device, 256); + auto matCB = cache.GetOrCreateVolatileCB( + "ShadowCascade_v2", "HUDMat", sizeof(SkinnedMaterialCB), data.device, 256); + + const u32 hudRes = ShadowPassState::kHUDShadowResolution; + nvrhi::Viewport hudVp(0.f, float(hudRes), 0.f, float(hudRes), 0.f, 1.f); + + // Match SkinningPassSetup HUD FOV so cast/receive share the same space + auto applyHudFov = [](const Fmatrix& worldMatrix) -> Fmatrix { + const float fovScale = 1.0f / psHUD_FOV; + Fmatrix viewMatrix = Device.mView; + Fmatrix invView; + invView.invert(viewMatrix); + Fmatrix fovScaleMatrix; + fovScaleMatrix.identity(); + fovScaleMatrix._11 = fovScale; + fovScaleMatrix._22 = fovScale; + fovScaleMatrix._33 = 1.0f; + Fmatrix t1, t2, result; + t1.mul(viewMatrix, worldMatrix); + t2.mul(fovScaleMatrix, t1); + result.mul(invView, t2); + return result; + }; + + if (fb && dynCB && matCB) + { + for (const auto& batch : *data.hudBatches) + { + if (!batch.isSkinned || !batch.vertexBuffer || !batch.indexBuffer) + continue; + + CKinematics* parent = nullptr; + const u32 visualType = batch.visual ? batch.visual->getType() : 0; + if (visualType == MT_SKELETON_GEOMDEF_ST) + parent = static_cast(batch.visual)->GetParent(); + else if (visualType == MT_SKELETON_GEOMDEF_PM) + parent = static_cast(batch.visual)->GetParent(); + if (!parent) + continue; + + const u32 boneOffset = data.gpuCulling->GetOrUploadSkeleton(cmd, parent); + + const bool use4w = (batch.vertexStride == 40) || + (batch.skinningRenderMode == 9) || (batch.skinningRenderMode == 10); + nvrhi::IGraphicsPipeline* pipe = + use4w ? st.skinned4wPipeline.Get() : st.skinnedPipeline.Get(); + nvrhi::IBindingLayout* layout = + use4w ? st.skinned4wLayout.Get() : st.skinnedLayout.Get(); + nvrhi::IInputLayout* il = + use4w ? st.skinned4wInputLayout.Get() : st.skinnedInputLayout.Get(); + auto* vsRefl = use4w ? sk4VsRefl : skVsRefl; + if (!pipe || !layout || !il || !vsRefl) + continue; + + DynamicTransforms dyn{}; + FillDynamicTransforms(dyn, applyHudFov(batch.worldMatrix)); + cmd->writeBuffer(dynCB, &dyn, sizeof(dyn)); + + SkinnedMaterialCB matId{}; + matId.materialID = batch.bindlessMaterialID; + matId.skeletonBoneOffset = boneOffset; + cmd->writeBuffer(matCB, &matId, sizeof(matId)); + + framegraph::BindingSetBuilder bsb(*vsRefl, *psReflHud, nv, "ShadowHUD"); + bsb.ConstantBuffer("dynamic_transforms", dynCB); + bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + bsb.ConstantBuffer("SkinnedMaterialCB", matCB); + bsb.BufferSRV("g_BoneMatrices", boneBuf); + bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), layout, nv); + if (!set) + continue; + + nvrhi::GraphicsState gs; + gs.pipeline = pipe; + gs.framebuffer = fb; + gs.bindings = {set}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = {{batch.vertexBuffer, 0, 0}}; + gs.indexBuffer = {batch.indexBuffer, nvrhi::Format::R16_UINT, 0}; + gs.viewport.addViewport(hudVp); + gs.viewport.addScissorRect(nvrhi::Rect(hudRes, hudRes)); + cmd->setGraphicsState(gs); + cmd->drawIndexed( + nvrhi::DrawArguments() + .setVertexCount(batch.indexCount) + .setStartIndexLocation(batch.startIndex) + .setStartVertexLocation(batch.baseVertex)); + } + } + } + } + + // World actor→CSM0 remains disabled (camera-attached disc). + + if (!data.bindlessConfig.UseGPUCulling() || !data.bindlessConfig.UseMegaBuffers()) + return; + + auto drawIndexBuffer = GetOrCreateDrawIndexBuffer("ShadowCascade_v2", nv); + auto* vsRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".vs"); + auto* psRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".ps"); + if (!vsRefl || !psRefl || !drawIndexBuffer || !st.pipeline) + return; + + const bool drawGrass = + data.sunDetails && + data.detailManager && + psDeviceFlags.is(rsDrawDetails) && + data.detailManager->generatedInstancesBuffer; + + if (drawGrass && st.grassCB) + { + GrassShadowCB gcb{}; + gcb.grassBladeHeight = ps_r3_grass_blade_height; + gcb.buildDetailsIndex = data.detailManager->buildDetailsBindlessIndex; + cmd->writeBuffer(st.grassCB, &gcb, sizeof(gcb)); + } + + // ── World skinned casters (NPCs, mutants, corpses) → every cascade ── + // Reuses the forward skeleton bone upload (deduped per frame) so the CSM + // matches the animated pose. Drawn per-cascade so distant characters keep + // casting; count is small so per-draw overhead is negligible. + const bool drawWorldSkinned = + (ps_r_skinned_shadows != 0) && data.worldSkinnedBatches && data.gpuCulling && + (st.skinnedPipeline || st.skinned4wPipeline || st.skinnedHqPipeline || + st.skinned2wPipeline || st.skinned3wPipeline); + nvrhi::IBuffer* skBoneBuf = + drawWorldSkinned ? data.gpuCulling->GetGlobalBoneBuffer() : nullptr; + auto* skPsRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".ps"); + auto* skVsReflNonHQ = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned", ".vs"); + auto* skVsRefl4W = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_4w", ".vs"); + auto* skVsReflHQ = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_hq", ".vs"); + auto* skVsRefl2W = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_2w", ".vs"); + auto* skVsRefl3W = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_3w", ".vs"); + nvrhi::IBuffer* skDynCB = nullptr; + nvrhi::IBuffer* skMatCB = nullptr; + const bool worldSkinnedReady = drawWorldSkinned && skBoneBuf && skPsRefl; + if (worldSkinnedReady) + { + skDynCB = cache.GetOrCreateVolatileCB( + "ShadowCascade_v2", "WorldSkinDyn", sizeof(DynamicTransforms), data.device, 4096); + skMatCB = cache.GetOrCreateVolatileCB( + "ShadowCascade_v2", "WorldSkinMat", sizeof(SkinnedMaterialCB), data.device, 4096); + } + + for (u32 c = 0; c < kCSMCascadeCount; ++c) + { + // Indoor: few portal-visible sectors → only near cascade + if (ps_r_portal_cull != 0 && ps_r_shadow_indoor_near_only != 0 && c > 0 && + PortalTraverser.r_sectors.size() > 0 && PortalTraverser.r_sectors.size() <= 8) + continue; + + if (!shadowTex[c]) + continue; + + const u32 cres = st.cascadeResolution[c] ? st.cascadeResolution[c] : GetCSMCascadeResolution(c); + nvrhi::Viewport cascadeVp(0.f, float(cres), 0.f, float(cres), 0.f, 1.f); + + // Per-cascade light-frustum caster cull (replaces cast_all for near/mid) + const bool lightCull = (ps_r_shadow_light_cull != 0) && data.gpuCulling; + if (lightCull) + { + const float inflate = (c == 0) ? 1.35f : 1.15f; + data.gpuCulling->BuildLightFrustumCasters(cmd, nv, st.cascadeClipVP[c], inflate); + } + + ShadowCascadeCB cbData{}; + cbData.lightVP = st.cascadeClipVP[c]; + cmd->writeBuffer(st.cascadeCB, &cbData, sizeof(cbData)); + + nvrhi::FramebufferDesc fbDesc; + fbDesc.setDepthAttachment(shadowTex[c]); + auto fb = cache.GetOrCreateFramebuffer( + make_string("ShadowCascade_%u_%u", c, cres).c_str(), fbDesc, nv); + if (!fb) + continue; + + auto drawSet = [&](const BindlessDrawSet& set, nvrhi::IGraphicsPipeline* pipeline) { + if (!pipeline) + return; + + // Resolve light-cull buffers from manager (created during BuildLightFrustumCasters) + nvrhi::IBuffer* lcArgs = nullptr; + nvrhi::IBuffer* lcIdx = nullptr; + nvrhi::IBuffer* lcMat = nullptr; + nvrhi::IBuffer* lcCnt = nullptr; + if (lightCull && data.gpuCulling) + { + if (set.instanceBuffer == data.gpuCulling->GetStaticInstanceBuffer()) + { + lcArgs = data.gpuCulling->GetStaticShadowCompactDrawArgsBuffer(); + lcIdx = data.gpuCulling->GetStaticShadowIndicesBuffer(); + lcMat = data.gpuCulling->GetStaticShadowCompactMaterialIDBuffer(); + lcCnt = data.gpuCulling->GetStaticShadowCountBuffer(); + } + else if (set.instanceBuffer == data.gpuCulling->GetDynamicInstanceBuffer()) + { + lcArgs = data.gpuCulling->GetDynamicShadowCompactDrawArgsBuffer(); + lcIdx = data.gpuCulling->GetDynamicShadowIndicesBuffer(); + lcMat = data.gpuCulling->GetDynamicShadowCompactMaterialIDBuffer(); + lcCnt = data.gpuCulling->GetDynamicShadowCountBuffer(); + } + else if (set.instanceBuffer == data.gpuCulling->GetTransparentInstanceBuffer()) + { + lcArgs = data.gpuCulling->GetTransparentShadowCompactDrawArgsBuffer(); + lcIdx = data.gpuCulling->GetTransparentShadowIndicesBuffer(); + lcMat = data.gpuCulling->GetTransparentShadowCompactMaterialIDBuffer(); + lcCnt = data.gpuCulling->GetTransparentShadowCountBuffer(); + } + } + const bool useLightCull = lightCull && lcArgs && lcIdx && lcMat && lcCnt; + const bool castAll = !useLightCull && (ps_r_shadow_cast_all != 0) && + set.IsCastAllValid() && (c < 1); + if (!useLightCull && !castAll && !set.IsValid()) + return; + + nvrhi::IBuffer* batchIndices = useLightCull ? lcIdx + : (castAll ? set.castAllBatchIndicesBuffer : set.compactBatchIndicesBuffer); + nvrhi::IBuffer* materialIDs = useLightCull ? lcMat + : (castAll ? set.castAllMaterialIDBuffer : set.compactMaterialIDBuffer); + nvrhi::IBuffer* drawArgs = useLightCull ? lcArgs + : (castAll ? set.castAllDrawArgsBuffer : set.compactDrawArgsBuffer); + nvrhi::IBuffer* countBuffer = useLightCull ? lcCnt + : (castAll ? set.castAllCountBuffer : set.compactCountBuffer); + + framegraph::BindingSetBuilder bsb(*vsRefl, *psRefl, nv, "ShadowCascade_v2"); + bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + bsb.BufferSRV("g_InstanceData", set.instanceBuffer); + bsb.BufferSRV("g_CompactBatchIndices", batchIndices); + bsb.BufferSRV("g_CompactMaterialIDs", materialIDs); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), st.layout, nv); + if (!bindingSet) + return; + + nvrhi::GraphicsState gs; + gs.pipeline = pipeline; + gs.framebuffer = fb; + gs.bindings = {bindingSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {data.bindlessConfig.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = {data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + gs.viewport.addViewport(cascadeVp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.indirectParams = drawArgs; + gs.indirectCountBuffer = countBuffer; + + cmd->setGraphicsState(gs); + DrawIndexedIndirectCountOrFallback(cmd, 0, 0, set.totalObjectCount); + }; + + { + static bool s_drawDbgDone = false; + if (!s_drawDbgDone) { + const auto& sS = data.bindlessConfig.staticSet; + const auto& dS = data.bindlessConfig.dynamicSet; + const auto& tS = data.bindlessConfig.transparentCasterSet; + Msg("* [ShadowDbg] draw cast_all=%d | static: obj=%u castAll=%d | dyn: obj=%u castAll=%d | trans: obj=%u castAll=%d foliagePipe=%d", + ps_r_shadow_cast_all, + sS.totalObjectCount, sS.IsCastAllValid() ? 1 : 0, + dS.totalObjectCount, dS.IsCastAllValid() ? 1 : 0, + tS.totalObjectCount, tS.IsCastAllValid() ? 1 : 0, + st.foliagePipeline ? 1 : 0); + s_drawDbgDone = true; + } + } + + drawSet(data.bindlessConfig.staticSet, st.pipeline); + drawSet(data.bindlessConfig.dynamicSet, st.pipeline); + // Trees/bushes (alpha-blended → transparent set) cast as alpha-tested foliage — near+mid only + if (c < 2) + drawSet(data.bindlessConfig.transparentCasterSet, st.foliagePipeline); + + // Terrain cast into near+mid (far cascade too coarse / expensive) + if (st.terrainPipeline && st.terrainLayout && + data.bindlessConfig.HasTerrain() && + data.bindlessConfig.UseTerrainCompaction() && + c < 2) + { + if (data.materialCache) + data.materialCache->FinalizePendingTerrainMaterials(ctx); + auto& terrainMatBuffer = bindless::TerrainMaterialBuffer::Instance(); + terrainMatBuffer.Upload(ctx); + + auto* terrainPsRefl = shaderLoader->GetCachedReflection( + "shadow\\shadow_cascade_terrain", ".ps"); + if (terrainPsRefl && terrainMatBuffer.GetBuffer()) + { + framegraph::BindingSetBuilder tbsb(*vsRefl, *terrainPsRefl, nv, "ShadowCascade.Terrain"); + tbsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + tbsb.BufferSRV("g_InstanceData", data.bindlessConfig.terrainInstanceBuffer); + tbsb.BufferSRV("g_CompactBatchIndices", data.bindlessConfig.terrainCompactBatchIndicesBuffer); + tbsb.BufferSRV("g_CompactMaterialIDs", data.bindlessConfig.terrainCompactMaterialIDBuffer); + auto terrainSet = cache.GetOrCreateBindingSet(tbsb.Build(), st.terrainLayout, nv); + if (terrainSet) + { + nvrhi::GraphicsState gs; + gs.pipeline = st.terrainPipeline; + gs.framebuffer = fb; + gs.bindings = {terrainSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {data.bindlessConfig.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = { + data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + gs.viewport.addViewport(cascadeVp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.indirectParams = data.bindlessConfig.terrainCompactDrawArgsBuffer; + gs.indirectCountBuffer = data.bindlessConfig.terrainCompactCountBuffer; + cmd->setGraphicsState(gs); + DrawIndexedIndirectCountOrFallback( + cmd, 0, 0, data.bindlessConfig.terrainObjectCount); + } + } + } + + // Grass cast into near cascade only (r2_sun_details) — mid/far too coarse + if (drawGrass && c < 1 && st.grassCB) + { + auto* dm = data.detailManager; + const bool billboardMode = !ps_r__detail_gpu; + + if (billboardMode && + st.billboardGrassPipeline && st.billboardGrassLayout && + dm->visibleBillboardInstancesBuffer && + dm->billboardDrawArgsBuffer && + dm->pulledIndexBuffer && dm->maxPulledIndexCount > 0 && + dm->detailModelsBuffer && dm->pulledVertexBuffer) + { + auto* bbVsRefl = shaderLoader->GetCachedReflection("detail_billboard_shadow", ".vs"); + auto* bbPsRefl = shaderLoader->GetCachedReflection("detail_billboard_shadow", ".ps"); + if (bbVsRefl && bbPsRefl) + { + framegraph::BindingSetBuilder gsb( + *bbVsRefl, *bbPsRefl, nv, "ShadowCascade.BillboardGrass"); + gsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + gsb.ConstantBuffer("GrassShadowCB", st.grassCB); + gsb.BufferSRV("visible_indices", dm->visibleBillboardInstancesBuffer); + gsb.BufferSRV("detail_models", dm->detailModelsBuffer); + gsb.BufferSRV("pulled_vertices", dm->pulledVertexBuffer); + gsb.BufferSRV("all_instances", dm->generatedInstancesBuffer); + auto grassSet = cache.GetOrCreateBindingSet(gsb.Build(), st.billboardGrassLayout, nv); + if (grassSet) + { + nvrhi::GraphicsState gs; + gs.pipeline = st.billboardGrassPipeline; + gs.framebuffer = fb; + gs.bindings = {grassSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.indexBuffer = {dm->pulledIndexBuffer, nvrhi::Format::R16_UINT, 0}; + gs.viewport.addViewport(cascadeVp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.indirectParams = dm->billboardDrawArgsBuffer; + cmd->setGraphicsState(gs); + cmd->drawIndexedIndirect(0); + } + } + } + else if (!billboardMode && st.grassPipeline && st.grassLayout) + { + auto* grassVsRefl = shaderLoader->GetCachedReflection("detail_gpu_shadow", ".vs"); + auto* grassPsRefl = shaderLoader->GetCachedReflection("detail_gpu_shadow", ".ps"); + if (grassVsRefl && grassPsRefl) + { + for (u32 lod = 0; lod < 2 && lod < FGDetailManager::LOD_COUNT; ++lod) + { + if (!dm->visibleInstancesBuffer[lod] || !dm->drawArgsBuffer[lod] || + !dm->bladeVertexBuffer[lod] || !dm->bladeIndexBuffer[lod]) + continue; + + framegraph::BindingSetBuilder gsb( + *grassVsRefl, *grassPsRefl, nv, "ShadowCascade.Grass"); + gsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + gsb.ConstantBuffer("GrassShadowCB", st.grassCB); + gsb.BufferSRV("visible_indices", dm->visibleInstancesBuffer[lod]); + gsb.BufferSRV("all_instances", dm->generatedInstancesBuffer); + auto grassSet = cache.GetOrCreateBindingSet(gsb.Build(), st.grassLayout, nv); + if (!grassSet) + continue; + + nvrhi::GraphicsState gs; + gs.pipeline = st.grassPipeline; + gs.framebuffer = fb; + gs.bindings = {grassSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = {{dm->bladeVertexBuffer[lod], 0, 0}}; + gs.indexBuffer = {dm->bladeIndexBuffer[lod], nvrhi::Format::R16_UINT, 0}; + gs.viewport.addViewport(cascadeVp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.indirectParams = dm->drawArgsBuffer[lod]; + cmd->setGraphicsState(gs); + cmd->drawIndexedIndirect(0); + } + } + } + } + + // World skinned: near + mid only (far cascade barely resolves NPC scale) + if (worldSkinnedReady && skDynCB && skMatCB && c < 1) + { + for (const auto& batch : *data.worldSkinnedBatches) + { + if (!batch.isSkinned || !batch.vertexBuffer || !batch.indexBuffer) + continue; + + CKinematics* parent = nullptr; + const u32 visualType = batch.visual ? batch.visual->getType() : 0; + if (visualType == MT_SKELETON_GEOMDEF_ST) + parent = static_cast(batch.visual)->GetParent(); + else if (visualType == MT_SKELETON_GEOMDEF_PM) + parent = static_cast(batch.visual)->GetParent(); + if (!parent) + continue; + + // Match the shadow pipeline to the vertex format, mirroring + // SkinningPass::SelectSkinnedPipelineFromVariants. RM_* from + // CSkeletonX: 1 SINGLE,2 SINGLE_HQ,3 1B,4 1B_HQ,5 2B,6 2B_HQ, + // 7 3B,8 3B_HQ,9 4B,10 4B_HQ. + const u16 rm = batch.skinningRenderMode; + const u32 stride = batch.vertexStride; + nvrhi::IGraphicsPipeline* pipe = nullptr; + nvrhi::IBindingLayout* layout = nullptr; + nvrhi::IInputLayout* il = nullptr; + auto* vsRefl = skVsReflNonHQ; + auto pick3w = [&] { pipe = st.skinned3wPipeline.Get(); layout = st.skinned3wLayout.Get(); il = st.skinned3wInputLayout.Get(); vsRefl = skVsRefl3W; }; + auto pick2w = [&] { pipe = st.skinned2wPipeline.Get(); layout = st.skinned2wLayout.Get(); il = st.skinned2wInputLayout.Get(); vsRefl = skVsRefl2W; }; + auto pick4w = [&] { pipe = st.skinned4wPipeline.Get(); layout = st.skinned4wLayout.Get(); il = st.skinned4wInputLayout.Get(); vsRefl = skVsRefl4W; }; + auto pickHq = [&] { pipe = st.skinnedHqPipeline.Get(); layout = st.skinnedHqLayout.Get(); il = st.skinnedHqInputLayout.Get(); vsRefl = skVsReflHQ; }; + auto pickNonHq = [&] { pipe = st.skinnedPipeline.Get(); layout = st.skinnedLayout.Get(); il = st.skinnedInputLayout.Get(); vsRefl = skVsReflNonHQ; }; + if (rm == 7 || rm == 8) pick3w(); + else if (rm == 5 || rm == 6) pick2w(); + else if (rm == 9 || rm == 10) pick4w(); + else if (rm == 4 || rm == 2) pickHq(); + else if (rm == 3 || rm == 1) pickNonHq(); + else if (stride == 36) pickHq(); + else if (stride == 40) pick4w(); + else if (stride == 44) pick2w(); + else pickNonHq(); + + if (!pipe || !layout || !il || !vsRefl) + continue; + + const u32 boneOffset = data.gpuCulling->GetOrUploadSkeleton(cmd, parent); + + DynamicTransforms dyn{}; + FillDynamicTransforms(dyn, batch.worldMatrix); + cmd->writeBuffer(skDynCB, &dyn, sizeof(dyn)); + + SkinnedMaterialCB matId{}; + matId.materialID = batch.bindlessMaterialID; + matId.skeletonBoneOffset = boneOffset; + cmd->writeBuffer(skMatCB, &matId, sizeof(matId)); + + framegraph::BindingSetBuilder bsb(*vsRefl, *skPsRefl, nv, "ShadowCascade.WorldSkin"); + bsb.ConstantBuffer("dynamic_transforms", skDynCB); + bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + bsb.ConstantBuffer("SkinnedMaterialCB", skMatCB); + bsb.BufferSRV("g_BoneMatrices", skBoneBuf); + bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), layout, nv); + if (!set) + continue; + + nvrhi::GraphicsState gs; + gs.pipeline = pipe; + gs.framebuffer = fb; + gs.bindings = {set}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = {{batch.vertexBuffer, 0, 0}}; + gs.indexBuffer = {batch.indexBuffer, nvrhi::Format::R16_UINT, 0}; + gs.viewport.addViewport(cascadeVp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + cmd->setGraphicsState(gs); + cmd->drawIndexed( + nvrhi::DrawArguments() + .setVertexCount(batch.indexCount) + .setStartIndexLocation(batch.startIndex) + .setStartVertexLocation(batch.baseVertex)); + } + } + } + }); + + outputs.shadowArray = passData.shadowCascades[0]; + outputs.valid = true; + return outputs; +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.h new file mode 100644 index 00000000000..ac1df9c34cc --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.h @@ -0,0 +1,142 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include "Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h" +#include "Layers/xrRender/FrameGraphPasses/ShaderConstants.h" +#include "Layers/xrRender/Geometry/GeometryBatch.h" +#include "Layers/xrRender/GPUCullingManager.h" +#include "Layers/xrRender/RenderContext/ResourceHandle.h" +#include + +namespace xray::render::framegraph +{ +class FrameGraph; +} + +namespace xray::render::fg +{ +class RenderDevice; +} + +namespace xray::render +{ +class MaterialCache; +} + +namespace xray::render::fg +{ +class FGDetailManager; +} + +namespace xray::render::fg::passes +{ + +static constexpr u32 kCSMCascadeCount = 3; + +// Defined in ShadowPassSetup.cpp — reads r2_smap_size (classic default 2048). +u32 GetCSMResolution(); +// Per-cascade ladder: c0=full, c1=half, c2=quarter (clamped ≥512). +u32 GetCSMCascadeResolution(u32 cascade); + +struct alignas(16) ShadowCascadeCB +{ + Fmatrix lightVP; +}; +static_assert(sizeof(ShadowCascadeCB) % 16 == 0); + +struct alignas(16) GrassShadowCB +{ + float grassBladeHeight; + u32 buildDetailsIndex; // bindless atlas for billboard alpha clip + float pad0, pad1; +}; +static_assert(sizeof(GrassShadowCB) % 16 == 0); + +struct ShadowPassState +{ + nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::BindingLayoutHandle layout; + nvrhi::GraphicsPipelineHandle foliagePipeline; // transparent-set foliage caster (reuses layout) + nvrhi::GraphicsPipelineHandle terrainPipeline; + nvrhi::BindingLayoutHandle terrainLayout; + nvrhi::GraphicsPipelineHandle grassPipeline; + nvrhi::BindingLayoutHandle grassLayout; + nvrhi::GraphicsPipelineHandle billboardGrassPipeline; + nvrhi::BindingLayoutHandle billboardGrassLayout; + nvrhi::GraphicsPipelineHandle skinnedPipeline; + nvrhi::BindingLayoutHandle skinnedLayout; + nvrhi::GraphicsPipelineHandle skinned4wPipeline; + nvrhi::BindingLayoutHandle skinned4wLayout; + nvrhi::GraphicsPipelineHandle skinnedHqPipeline; // 1-bone HQ (36B) + nvrhi::BindingLayoutHandle skinnedHqLayout; + nvrhi::GraphicsPipelineHandle skinned2wPipeline; // 2-bone HQ (44B) + nvrhi::BindingLayoutHandle skinned2wLayout; + nvrhi::GraphicsPipelineHandle skinned3wPipeline; // 3-bone HQ (44B) + nvrhi::BindingLayoutHandle skinned3wLayout; + nvrhi::InputLayoutHandle inputLayout; + nvrhi::InputLayoutHandle grassInputLayout; + nvrhi::InputLayoutHandle skinnedInputLayout; + nvrhi::InputLayoutHandle skinned4wInputLayout; + nvrhi::InputLayoutHandle skinnedHqInputLayout; + nvrhi::InputLayoutHandle skinned2wInputLayout; + nvrhi::InputLayoutHandle skinned3wInputLayout; + nvrhi::ShaderHandle vs; + nvrhi::ShaderHandle ps; + nvrhi::ShaderHandle foliagePs; + nvrhi::ShaderHandle terrainPs; + nvrhi::ShaderHandle grassVs; + nvrhi::ShaderHandle grassPs; + nvrhi::ShaderHandle billboardGrassVs; + nvrhi::ShaderHandle billboardGrassPs; + nvrhi::ShaderHandle skinnedVs; + nvrhi::ShaderHandle skinned4wVs; + nvrhi::ShaderHandle skinnedHqVs; + nvrhi::ShaderHandle skinned2wVs; + nvrhi::ShaderHandle skinned3wVs; + nvrhi::BufferHandle cascadeCB; + nvrhi::BufferHandle grassCB; + u32 cascadeCBMaxVersions = 0; // tracks volatile version budget (CSM+local tiles) + + xray::render::fg::TextureHandle shadowArrayHandle; // legacy alias → cascade 0 + nvrhi::ITexture* shadowArray = nullptr; // cascade 0 (bind slot t23) + xray::render::fg::TextureHandle shadowCascadeHandle[kCSMCascadeCount]; + nvrhi::ITexture* shadowCascades[kCSMCascadeCount] = {}; + u32 cascadeResolution[kCSMCascadeCount] = {}; + + // Dedicated HUD/weapon shadow map (not world cascade 0) + xray::render::fg::TextureHandle hudShadowHandle; + nvrhi::ITexture* hudShadowMap = nullptr; + Fmatrix hudClipVP; + Fmatrix hudSampleVP; + static constexpr u32 kHUDShadowResolution = 1024; + + Fmatrix cascadeClipVP[kCSMCascadeCount]; // light VP for depth raster + Fmatrix cascadeSampleVP[kCSMCascadeCount]; // clip→UV for sampling + Fvector4 cascadeSplits; + bool initialized = false; + bool enabled = false; +}; + +struct ShadowCascadeOutputs +{ + framegraph::VirtualResourceHandle shadowArray; + bool valid = false; +}; + +ShadowCascadeOutputs setupCascadedShadowPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + const BindlessForwardConfig& bindlessConfig, + MaterialCache* materialCache, + fg::FGDetailManager* detailManager, + const Fvector& sunDirection, + const xr_vector* hudBatches, + const xr_vector* worldSkinnedBatches, + fg::GPUCullingManager* gpuCulling, + ShadowPassState& state); + +void InitializeShadowPass(fg::RenderDevice* device, ShadowPassState& state); +void ShutdownShadowPass(fg::RenderDevice* device, ShadowPassState& state); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp index 8a01681ffa7..00fdef67b80 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp @@ -67,6 +67,9 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf if (state.initialized) return; + // Drop stale variant PSOs so NPC materials remapped to bindless_skinned pick up IBL/CSM. + VariantPSOCache::Instance().Shutdown(); + nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); if (!nvDevice) return; @@ -87,12 +90,12 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf state.ps = skinnedPsResult.handle; auto skinnedVsForReflection = shaderLoader->LoadVertexShader("bindless_skinned", "main"); - state.layout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass", *skinnedVsForReflection.reflection, *skinnedPsResult.reflection, nvDevice); + state.layout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_IBL_CSMLadder", *skinnedVsForReflection.reflection, *skinnedPsResult.reflection, nvDevice); auto hudPsResult = shaderLoader->LoadPixelShader("bindless_skinned_hud", "main"); if (hudPsResult.handle) { state.hudPS = hudPsResult.handle; - state.hudLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_HUD", *skinnedVsForReflection.reflection, *hudPsResult.reflection, nvDevice); + state.hudLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_HUD_IBL_CSMLadder_HUDShadow", *skinnedVsForReflection.reflection, *hudPsResult.reflection, nvDevice); } if (!state.hudLayout) state.hudLayout = state.layout; @@ -142,7 +145,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf auto mdiVsForReflection = shaderLoader->LoadVertexShader("bindless_skinned_mdi", "main"); if (mdiPsResult.handle && mdiVsForReflection.handle) { state.mdiPS = mdiPsResult.handle; - state.mdiLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_MDI", + state.mdiLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_MDI_IBL_CSMLadder", *mdiVsForReflection.reflection, *mdiPsResult.reflection, nvDevice); } @@ -177,8 +180,8 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG16_SNORM).setOffset(20).setElementStride(stride), }; - initVariant(state.nonHQ, "bindless_skinned", "SkinningPass_nonHQ", attribs, 5); - initMDIVariant(state.mdiNonHQ, "bindless_skinned_mdi", "SkinningPass_mdi_nonHQ", attribs, 5); + initVariant(state.nonHQ, "bindless_skinned", "SkinningPass_nonHQ_IBL_CSMLadder", attribs, 5); + initMDIVariant(state.mdiNonHQ, "bindless_skinned_mdi", "SkinningPass_mdi_nonHQ_IBL_CSMLadder", attribs, 5); } { @@ -190,8 +193,8 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), }; - initVariant(state.hq1w, "bindless_skinned_hq", "SkinningPass_hq1w", attribs, 5); - initMDIVariant(state.mdiHQ1w, "bindless_skinned_hq_mdi", "SkinningPass_mdi_hq1w", attribs, 5); + initVariant(state.hq1w, "bindless_skinned_hq", "SkinningPass_hq1w_IBL_CSMLadder", attribs, 5); + initMDIVariant(state.mdiHQ1w, "bindless_skinned_hq_mdi", "SkinningPass_mdi_hq1w_IBL_CSMLadder", attribs, 5); } { @@ -204,8 +207,8 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("BLENDINDICES").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(36).setElementStride(stride), }; - initVariant(state.hq4w, "bindless_skinned_4w", "SkinningPass_hq4w", attribs, 6); - initMDIVariant(state.mdiHQ4w, "bindless_skinned_4w_mdi", "SkinningPass_mdi_hq4w", attribs, 6); + initVariant(state.hq4w, "bindless_skinned_4w", "SkinningPass_hq4w_IBL_CSMLadder", attribs, 6); + initMDIVariant(state.mdiHQ4w, "bindless_skinned_4w_mdi", "SkinningPass_mdi_hq4w_IBL_CSMLadder", attribs, 6); } { @@ -217,8 +220,8 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), }; - initVariant(state.hq2w, "bindless_skinned_2w", "SkinningPass_hq2w", attribs, 5); - initMDIVariant(state.mdiHQ2w, "bindless_skinned_2w_mdi", "SkinningPass_mdi_hq2w", attribs, 5); + initVariant(state.hq2w, "bindless_skinned_2w", "SkinningPass_hq2w_IBL_CSMLadder", attribs, 5); + initMDIVariant(state.mdiHQ2w, "bindless_skinned_2w_mdi", "SkinningPass_mdi_hq2w_IBL_CSMLadder", attribs, 5); } { @@ -230,15 +233,64 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), }; - initVariant(state.hq3w, "bindless_skinned_3w", "SkinningPass_hq3w", attribs, 5); - initMDIVariant(state.mdiHQ3w, "bindless_skinned_3w_mdi", "SkinningPass_mdi_hq3w", attribs, 5); + initVariant(state.hq3w, "bindless_skinned_3w", "SkinningPass_hq3w_IBL_CSMLadder", attribs, 5); + initMDIVariant(state.mdiHQ3w, "bindless_skinned_3w_mdi", "SkinningPass_mdi_hq3w_IBL_CSMLadder", attribs, 5); } - initHudVariant(state.hudNonHQ, state.nonHQ, "SkinningPass_hud_nonHQ"); - initHudVariant(state.hudHQ1w, state.hq1w, "SkinningPass_hud_hq1w"); - initHudVariant(state.hudHQ2w, state.hq2w, "SkinningPass_hud_hq2w"); - initHudVariant(state.hudHQ3w, state.hq3w, "SkinningPass_hud_hq3w"); - initHudVariant(state.hudHQ4w, state.hq4w, "SkinningPass_hud_hq4w"); + initHudVariant(state.hudNonHQ, state.nonHQ, "SkinningPass_hud_nonHQ_IBL_CSMLadder"); + initHudVariant(state.hudHQ1w, state.hq1w, "SkinningPass_hud_hq1w_IBL_CSMLadder"); + initHudVariant(state.hudHQ2w, state.hq2w, "SkinningPass_hud_hq2w_IBL_CSMLadder"); + initHudVariant(state.hudHQ3w, state.hq3w, "SkinningPass_hud_hq3w_IBL_CSMLadder"); + initHudVariant(state.hudHQ4w, state.hq4w, "SkinningPass_hud_hq4w_IBL_CSMLadder"); + + // Skinned PatchList tessellation is NOT created on Apple/MoltenVK: Metal's + // tessellation temp buffers have caused IOGPU kernel panics + // (IOGPUGroupMemory::remove_memory_object). Keep the variant slots empty so + // SelectSkinnedTessPipeline always returns null. +#if !defined(XR_PLATFORM_APPLE) + { + auto hs = shaderLoader->LoadHullShader("bindless_skinned_tess", "main"); + auto ds = shaderLoader->LoadDomainShader("bindless_skinned_tess", "main"); + if (hs.handle && ds.handle) + { + state.tessHS = hs.handle; + state.tessDS = ds.handle; + + auto initTessVariant = [&](SkinningPipelineVariant& tessVar, + const SkinningPipelineVariant& src, + const char* cacheName) { + if (!src.pipeline || !src.vs || !src.inputLayout) + return; + tessVar.vs = src.vs; + tessVar.inputLayout = src.inputLayout; + auto pipeDesc = buildPipelineDesc(src.vs, src.inputLayout); + pipeDesc.HS = state.tessHS; + pipeDesc.DS = state.tessDS; + pipeDesc.primType = nvrhi::PrimitiveType::PatchList; + pipeDesc.patchControlPoints = 3; + pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + tessVar.pipeline = cache.GetOrCreatePipeline(cacheName, pipeDesc, fbInfo, nvDevice); + }; + + initTessVariant(state.tessNonHQ, state.nonHQ, "SkinningPass_tess_nonHQ_v1"); + initTessVariant(state.tessHQ1w, state.hq1w, "SkinningPass_tess_hq1w_v1"); + initTessVariant(state.tessHQ2w, state.hq2w, "SkinningPass_tess_hq2w_v1"); + initTessVariant(state.tessHQ3w, state.hq3w, "SkinningPass_tess_hq3w_v1"); + initTessVariant(state.tessHQ4w, state.hq4w, "SkinningPass_tess_hq4w_v1"); + + if (state.tessNonHQ.pipeline || state.tessHQ1w.pipeline || state.tessHQ4w.pipeline) + Msg("* [SkinningPass] Skinned tessellation pipelines ready"); + else + Msg("! [SkinningPass] Skinned tess HS/DS loaded but PSO create failed"); + } + else + { + Msg("! [SkinningPass] bindless_skinned_tess HS/DS missing — skinned tess disabled"); + } + } +#else + Msg("* [SkinningPass] Skinned tessellation disabled on Apple/MoltenVK (IOGPU safety)"); +#endif state.initialized = true; Msg("* [SkinningPass] Pipeline initialization complete"); @@ -297,6 +349,11 @@ static nvrhi::IGraphicsPipeline* SelectSkinnedPipeline(const SkinningPassState& return SelectSkinnedPipelineFromVariants(state.nonHQ, state.hq1w, state.hq2w, state.hq3w, state.hq4w, vertexStride, renderMode); } +static nvrhi::IGraphicsPipeline* SelectSkinnedTessPipeline(const SkinningPassState& state, u32 vertexStride, u16 renderMode) +{ + return SelectSkinnedPipelineFromVariants(state.tessNonHQ, state.tessHQ1w, state.tessHQ2w, state.tessHQ3w, state.tessHQ4w, vertexStride, renderMode); +} + static nvrhi::IGraphicsPipeline* SelectHUDSkinnedPipeline(const SkinningPassState& state, u32 vertexStride, u16 renderMode) { auto* hudPipe = SelectSkinnedPipelineFromVariants(state.hudNonHQ, state.hudHQ1w, state.hudHQ2w, state.hudHQ3w, state.hudHQ4w, vertexStride, renderMode); @@ -397,6 +454,49 @@ struct SkinnedPhaseContext { bool isHUD = false; }; +static void BindSkinnedLightingTextures( + framegraph::BindingSetBuilder& bsb, + nvrhi::IDevice* nvDevice, + nvrhi::ITexture* const* shadowCascades, + nvrhi::ITexture* hudShadowMap, + nvrhi::ITexture* contactDepth, + nvrhi::ITexture* contactHistory, + nvrhi::ITexture* envSky0, + nvrhi::ITexture* envSky1, + bool isHUD, + nvrhi::ITexture* localShadowAtlas = nullptr) +{ + auto& cache = framegraph::GetPassResourceCache(); + nvrhi::ITexture* dummy2D = cache.GetDummyShadowMap2D(nvDevice); + nvrhi::ITexture* dummyArray = cache.GetDummyShadowMap(nvDevice); + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = (shadowCascades && shadowCascades[i]) ? shadowCascades[i] : dummy2D; + if (t) + bsb.Texture(kNames[i], t); + } + if (isHUD) + { + nvrhi::ITexture* hudTex = hudShadowMap ? hudShadowMap : dummyArray; + if (hudTex) + bsb.TextureSlot(24, hudTex); + } + (void)contactDepth; // history-only contact; depth no longer in forward layouts + nvrhi::ITexture* hist = contactHistory ? contactHistory : cache.GetDummyContactHistory(nvDevice); + if (hist) + bsb.Texture("g_ContactHistory", hist); + nvrhi::ITexture* localAtlas = localShadowAtlas ? localShadowAtlas : dummyArray; + if (localAtlas) + bsb.Texture("g_LocalShadowAtlas", localAtlas); + nvrhi::ITexture* sky0 = envSky0 ? envSky0 : cache.GetDummyCubeMap(nvDevice); + nvrhi::ITexture* sky1 = envSky1 ? envSky1 : cache.GetDummyCubeMap(nvDevice); + if (sky0) + bsb.Texture("s_env0", sky0); + if (sky1) + bsb.Texture("s_env1", sky1); +} + static SkinnedPhaseContext BuildSkinnedPhaseContext( const SkinningPassState& state, nvrhi::IDevice* nvDevice, @@ -410,7 +510,14 @@ static SkinnedPhaseContext BuildSkinnedPhaseContext( nvrhi::IBuffer* splatBuffer, const nvrhi::Viewport& viewport, const nvrhi::Rect& scissor, - bool isHUD) + bool isHUD, + nvrhi::ITexture* const* shadowCascades, + nvrhi::ITexture* hudShadowMap, + nvrhi::ITexture* contactDepth, + nvrhi::ITexture* contactHistory, + nvrhi::ITexture* envSky0, + nvrhi::ITexture* envSky1, + nvrhi::ITexture* localShadowAtlas = nullptr) { using namespace fg; using namespace fg::bindless; @@ -447,6 +554,7 @@ static SkinnedPhaseContext BuildSkinnedPhaseContext( bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); + BindSkinnedLightingTextures(bsb, nvDevice, shadowCascades, hudShadowMap, contactDepth, contactHistory, envSky0, envSky1, isHUD, localShadowAtlas); ctx.bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), activeLayout, nvDevice); return ctx; @@ -498,9 +606,34 @@ static void DrawSkinnedBatch( nvDevice, ctx.framebuffer, variantIdx, *variant, p, fmt, GetSkinnedInputLayout(state, fmt), state.layout, ctx.bindlessLayout); } else { - pipeline = ctx.isHUD - ? SelectHUDSkinnedPipeline(state, batch.vertexStride, batch.skinningRenderMode) - : SelectSkinnedPipeline(state, batch.vertexStride, batch.skinningRenderMode); + // Skinned PatchList tess is disabled on Apple (IOGPU kernel panic). + // On other platforms it still requires r4_enable_tessellation + tessMethod. + nvrhi::IGraphicsPipeline* tessPipe = nullptr; +#if !defined(XR_PLATFORM_APPLE) + constexpr u32 kMaxSkinnedTessIndices = 8192; + const bool tessOn = !ctx.isHUD && + ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION) && + batch.indexCount > 0 && batch.indexCount <= kMaxSkinnedTessIndices; + if (tessOn) + { + const auto* mat = MaterialBuffer::Instance().GetMaterial(batch.bindlessMaterialID); + if (mat && mat->tessMethod != 0) + tessPipe = SelectSkinnedTessPipeline(state, batch.vertexStride, batch.skinningRenderMode); + } +#endif + if (tessPipe) + { + pipeline = tessPipe; + indirectArgs = nullptr; + } + else if (ctx.isHUD) + { + pipeline = SelectHUDSkinnedPipeline(state, batch.vertexStride, batch.skinningRenderMode); + } + else + { + pipeline = SelectSkinnedPipeline(state, batch.vertexStride, batch.skinningRenderMode); + } } if (!pipeline) continue; @@ -546,7 +679,16 @@ framegraph::DefaultOutputLayout setupSkinningPass( fg::GPUCullingManager* gpuCulling, framegraph::VirtualResourceHandle skinnedDrawArgs, SkinningPassState* state, - decals::OverlayManager* overlayMgr) + decals::OverlayManager* overlayMgr, + nvrhi::ITexture* shadowMapArray, + framegraph::VirtualResourceHandle shadowMapHandle, + nvrhi::ITexture* contactDepth, + nvrhi::ITexture* contactHistory, + nvrhi::ITexture* envSky0, + nvrhi::ITexture* envSky1, + nvrhi::ITexture* hudShadowMap, + nvrhi::ITexture* const* shadowCascadesIn, + nvrhi::ITexture* localShadowAtlas) { using namespace framegraph; @@ -566,7 +708,8 @@ framegraph::DefaultOutputLayout setupSkinningPass( // ═══════════════════════════════════════════════════════ // SETUP LAMBDA // ═══════════════════════════════════════════════════════ - [&, width, height, gpuCulling, skinnedDrawArgs, state, overlayMgr](FrameGraph& builder, PassHandle passHandle, SkinningPassData& data) { + [&, width, height, gpuCulling, skinnedDrawArgs, state, overlayMgr, + shadowMapArray, shadowMapHandle, contactDepth, contactHistory, envSky0, envSky1, hudShadowMap, shadowCascadesIn, localShadowAtlas](FrameGraph& builder, PassHandle passHandle, SkinningPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.width = width; @@ -578,9 +721,20 @@ framegraph::DefaultOutputLayout setupSkinningPass( data.gpuCulling = gpuCulling; data.passState = state; data.overlayMgr = overlayMgr; + data.shadowMapArray = shadowMapArray; + for (int i = 0; i < 3; ++i) + data.shadowCascades[i] = (shadowCascadesIn && shadowCascadesIn[i]) ? shadowCascadesIn[i] : shadowMapArray; + data.hudShadowMap = hudShadowMap; + data.localShadowAtlas = localShadowAtlas; + data.contactDepth = contactDepth; + data.contactHistory = contactHistory; + data.envSky0 = envSky0; + data.envSky1 = envSky1; if (skinnedDrawArgs.is_valid()) data.skinnedDrawArgs = passBuilder.read(skinnedDrawArgs, ResourceState::IndirectArgument); + if (shadowMapHandle.is_valid()) + data.shadowMap = passBuilder.read(shadowMapHandle, ResourceState::ShaderResource); data.color = passBuilder.readWrite(inputs.albedo, ResourceState::RenderTarget); data.normal = passBuilder.readWrite(inputs.normal, ResourceState::RenderTarget); @@ -716,7 +870,8 @@ framegraph::DefaultOutputLayout setupSkinningPass( *data.passState, nvDevice, framebuffer, dynTransformsCB, staticGlobalsCB, materialIdCB, globalBoneBuffer, bindlessTable, bindlessLayout, splatBuffer, - worldViewport, scissor, false); + worldViewport, scissor, false, + data.shadowCascades, data.hudShadowMap, data.contactDepth, data.contactHistory, data.envSky0, data.envSky1, data.localShadowAtlas); const bool cullActive = data.skinnedDrawArgs.is_valid() && gpuCullMgr->IsSkinnedCullingEnabled() @@ -756,6 +911,7 @@ framegraph::DefaultOutputLayout setupSkinningPass( bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); + BindSkinnedLightingTextures(bsb, nvDevice, data.shadowCascades, data.hudShadowMap, data.contactDepth, data.contactHistory, data.envSky0, data.envSky1, false, data.localShadowAtlas); auto& cache = framegraph::GetPassResourceCache(); nvrhi::BindingSetHandle mdiBindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.passState->mdiLayout, nvDevice); @@ -818,7 +974,8 @@ framegraph::DefaultOutputLayout setupSkinningPass( *data.passState, nvDevice, framebuffer, dynTransformsCB, staticGlobalsCB, materialIdCB, globalBoneBuffer, bindlessTable, bindlessLayout, splatBuffer, - hudViewport, scissor, true); + hudViewport, scissor, true, + data.shadowCascades, data.hudShadowMap, data.contactDepth, data.contactHistory, data.envSky0, data.envSky1, data.localShadowAtlas); for (const auto& batch : *data.hudBatches) { Fmatrix adjustedWorldMatrix = ApplyHUDFOVAdjustment(batch.worldMatrix); diff --git a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h index 1ec4287a04f..be953d09308 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h @@ -49,10 +49,18 @@ struct SkinningPassState { SkinningPipelineVariant hq2w; SkinningPipelineVariant hq3w; SkinningPipelineVariant hq4w; + // World-only PatchList variants (same VS/PS, + bindless_skinned_tess HS/DS). + SkinningPipelineVariant tessNonHQ; + SkinningPipelineVariant tessHQ1w; + SkinningPipelineVariant tessHQ2w; + SkinningPipelineVariant tessHQ3w; + SkinningPipelineVariant tessHQ4w; nvrhi::BindingLayoutHandle layout; nvrhi::BindingLayoutHandle hudLayout; nvrhi::ShaderHandle ps; nvrhi::ShaderHandle hudPS; + nvrhi::ShaderHandle tessHS; + nvrhi::ShaderHandle tessDS; SkinningPipelineVariant mdiNonHQ; SkinningPipelineVariant mdiHQ1w; SkinningPipelineVariant mdiHQ2w; @@ -78,6 +86,7 @@ struct SkinningPassData { framegraph::VirtualResourceHandle worldPos; framegraph::VirtualResourceHandle depth; framegraph::VirtualResourceHandle skinnedDrawArgs; + framegraph::VirtualResourceHandle shadowMap; fg::RenderDevice* device; const GeometryCollector* geometry; const xr_vector* hudBatches; @@ -87,6 +96,14 @@ struct SkinningPassData { framegraph::DefaultOutputLayout outputs; SkinningPassState* passState; decals::OverlayManager* overlayMgr; + nvrhi::ITexture* shadowMapArray = nullptr; + nvrhi::ITexture* shadowCascades[3] = {}; + nvrhi::ITexture* hudShadowMap = nullptr; + nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* contactDepth = nullptr; + nvrhi::ITexture* contactHistory = nullptr; + nvrhi::ITexture* envSky0 = nullptr; + nvrhi::ITexture* envSky1 = nullptr; }; // Main skinning pass setup function @@ -103,7 +120,16 @@ framegraph::DefaultOutputLayout setupSkinningPass( fg::GPUCullingManager* gpuCulling = nullptr, framegraph::VirtualResourceHandle skinnedDrawArgs = {}, SkinningPassState* state = nullptr, - decals::OverlayManager* overlayMgr = nullptr + decals::OverlayManager* overlayMgr = nullptr, + nvrhi::ITexture* shadowMapArray = nullptr, + framegraph::VirtualResourceHandle shadowMapHandle = {}, + nvrhi::ITexture* contactDepth = nullptr, + nvrhi::ITexture* contactHistory = nullptr, + nvrhi::ITexture* envSky0 = nullptr, + nvrhi::ITexture* envSky1 = nullptr, + nvrhi::ITexture* hudShadowMap = nullptr, + nvrhi::ITexture* const* shadowCascades = nullptr, + nvrhi::ITexture* localShadowAtlas = nullptr ); } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/SmokeTrailPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SmokeTrailPassSetup.cpp index 5bedfca7851..3ede1d80c59 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SmokeTrailPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SmokeTrailPassSetup.cpp @@ -473,6 +473,7 @@ DefaultOutputLayout setupSmokeTrailPass( BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "SmokeTrail.Draw"); bsb.ConstantBuffer("static_globals", staticGlobalsCB) .ConstantBuffer("TrailParams", trailParamsCB) + .BufferSRV("g_Materials", bindless::MaterialBuffer::Instance().GetBuffer()) .BufferSRV("g_ControlPoints", mgr->GetCompactBuffer()) .BufferSRV("g_TrailState", mgr->GetStateBuffer()) .Texture("g_Perlin4D", data.perlin4dVolume); diff --git a/src/Layers/xrRender/FrameGraphPasses/SunPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SunPassSetup.cpp index 101797522cc..3543e2b6581 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SunPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SunPassSetup.cpp @@ -11,6 +11,7 @@ namespace xray::render::fg::passes { framegraph::VirtualResourceHandle setupSunPass( framegraph::FrameGraph& fg, framegraph::VirtualResourceHandle colorInput, + framegraph::VirtualResourceHandle depthInput, FGEnvironmentRender* renderer, u32 width, u32 height) @@ -19,22 +20,26 @@ framegraph::VirtualResourceHandle setupSunPass( auto& passData = fg.addCallbackPass( "Sun", - [colorInput, renderer, width, height](FrameGraph& builder, PassHandle passHandle, SunPassData& data) { + [colorInput, depthInput, renderer, width, height](FrameGraph& builder, PassHandle passHandle, SunPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.renderer = renderer; data.width = width; data.height = height; data.colorOutput = passBuilder.readWrite(colorInput, ResourceState::RenderTarget); + data.depth = passBuilder.read(depthInput, ResourceState::DepthStencilRead); }, [](const SunPassData& data, const FrameGraph& fg, fg::RenderContext* ctx) { if (!data.renderer) return; nvrhi::ICommandList* cmdList = ctx->GetCommandList(); auto* colorRT = fg.GetPhysicalTexture(data.colorOutput); + auto* depthRT = fg.GetPhysicalTexture(data.depth); if (!cmdList || !colorRT) return; nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(colorRT); + if (depthRT) + fbDesc.setDepthAttachment(depthRT); auto framebuffer = cmdList->getDevice()->createFramebuffer(fbDesc); CEnvironment* environment = g_pGamePersistent ? &g_pGamePersistent->Environment() : nullptr; diff --git a/src/Layers/xrRender/FrameGraphPasses/SunPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SunPassSetup.h index 7a447629eef..ad7374de007 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SunPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/SunPassSetup.h @@ -15,14 +15,17 @@ namespace xray::render::fg::passes { struct SunPassData { framegraph::VirtualResourceHandle colorOutput; + framegraph::VirtualResourceHandle depth; FGEnvironmentRender* renderer; u32 width; u32 height; }; +// Draw sun disc with depth test so geometry occludes it (classic behaviour). framegraph::VirtualResourceHandle setupSunPass( framegraph::FrameGraph& fg, framegraph::VirtualResourceHandle colorInput, + framegraph::VirtualResourceHandle depthInput, FGEnvironmentRender* renderer, u32 width, u32 height diff --git a/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.cpp new file mode 100644 index 00000000000..c42ab1dba2f --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.cpp @@ -0,0 +1,346 @@ +#include "stdafx.h" +#include "SunShaftsPassSetup.h" +#include "ShaderConstants.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/xrRender_console.h" +#include "xrEngine/IGame_Persistent.h" +#include "xrEngine/Environment.h" +#include "xrEngine/device.h" + +namespace xray::render::fg::passes +{ + +using namespace framegraph; + +struct alignas(16) SunShaftsCBGPU +{ + Fvector4 shaft_params; // intensity, quality, samples, scale +}; +static_assert(sizeof(SunShaftsCBGPU) == 16); + +struct alignas(16) SunShaftsCombineCBGPU +{ + Fvector4 combine_params; // xy = half-res texel size, zw = unused +}; +static_assert(sizeof(SunShaftsCombineCBGPU) == 16); + +struct SunShaftsPassData +{ + VirtualResourceHandle depth; + VirtualResourceHandle worldPos; + VirtualResourceHandle shadowMap; + VirtualResourceHandle output; // half-res shafts + nvrhi::ITexture* shadowMapArray = nullptr; + nvrhi::ITexture* shadowCascades[3] = {}; + u32 width = 0; // march (half) resolution + u32 height = 0; + float intensity = 0.f; + u32 quality = 0; + SunShaftsPassState* passState = nullptr; +}; + +struct SunShaftsCombineData +{ + VirtualResourceHandle sceneColor; + VirtualResourceHandle shafts; // half-res + VirtualResourceHandle output; // full-res scene + shafts + u32 width = 0; // full resolution + u32 height = 0; + u32 halfWidth = 0; + u32 halfHeight = 0; + SunShaftsPassState* passState = nullptr; +}; + +void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state) +{ + if (state.initialized || !device) + return; + + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + { + state.initialized = true; + return; + } + + auto vs = loader->LoadVertexShader("fullscreen"); + auto ps = loader->LoadPixelShader("volumetric/sunshafts"); + if (!vs.handle || !ps.handle || !ps.reflection) + { + Msg("! [SunShafts] Failed to load shaders"); + state.initialized = true; + return; + } + + auto& cache = GetPassResourceCache(); + state.layout = cache.GetOrCreateBindingLayoutFromReflection( + "SunShafts_CSMLadder", *vs.reflection, *ps.reflection, device); + + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs.handle); + desc.setPixelShader(ps.handle); + desc.addBindingLayout(state.layout); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.blendState.targets[0].setBlendEnable(false); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); + state.pipeline = cache.GetOrCreatePipeline("SunShafts", desc, fbInfo, device); + + // Full-res combine pipeline: upsample + blur half-res shafts, add to scene. + auto combinePs = loader->LoadPixelShader("volumetric/sunshafts_combine"); + if (combinePs.handle && combinePs.reflection) + { + state.combineLayout = cache.GetOrCreateBindingLayoutFromReflection( + "SunShaftsCombine", *vs.reflection, *combinePs.reflection, device); + + nvrhi::GraphicsPipelineDesc cdesc; + cdesc.setVertexShader(vs.handle); + cdesc.setPixelShader(combinePs.handle); + cdesc.addBindingLayout(state.combineLayout); + cdesc.setPrimType(nvrhi::PrimitiveType::TriangleList); + cdesc.renderState.blendState.targets[0].setBlendEnable(false); + cdesc.renderState.depthStencilState.setDepthTestEnable(false); + cdesc.renderState.depthStencilState.setDepthWriteEnable(false); + cdesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + + nvrhi::FramebufferInfoEx cfbInfo; + cfbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); + state.combinePipeline = cache.GetOrCreatePipeline("SunShaftsCombine", cdesc, cfbInfo, device); + } + else + { + Msg("! [SunShafts] Failed to load combine shader"); + } + + state.initialized = true; + Msg("* [SunShafts] Pass initialized (march=%d combine=%d)", + state.pipeline ? 1 : 0, state.combinePipeline ? 1 : 0); +} + +static bool NeedSunShafts(float& outIntensity, u32& outQuality) +{ + outIntensity = 0.f; + outQuality = ps_r_sun_shafts; + if (!ps_r_sun_shafts) + return false; + if (!g_pGamePersistent) + return false; + const auto& env = g_pGamePersistent->Environment().CurrentEnv; + outIntensity = env.m_fSunShaftsIntensity; + if (outIntensity < 0.0001f) + return false; + return true; +} + +VirtualResourceHandle setupSunShaftsPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle sceneColor, + VirtualResourceHandle depth, + VirtualResourceHandle worldPos, + nvrhi::ITexture* shadowMapArray, + VirtualResourceHandle shadowMapHandle, + u32 width, + u32 height, + SunShaftsPassState& passState, + nvrhi::ITexture* const* shadowCascades) +{ + if (device && device->GetNVRHIDevice()) + InitializeSunShaftsPass(device->GetNVRHIDevice(), passState); + + float intensity = 0.f; + u32 quality = 0; + if (!NeedSunShafts(intensity, quality) || !passState.pipeline || + !passState.combinePipeline || !shadowMapArray) + return sceneColor; + + // Original renders the volumetric sun into a reduced-res buffer, then combines. + const u32 halfW = (width > 1) ? (width / 2) : 1; + const u32 halfH = (height > 1) ? (height / 2) : 1; + + ResourceDesc halfDesc; + halfDesc.type = ResourceDesc::Type::Texture2D; + halfDesc.width = halfW; + halfDesc.height = halfH; + halfDesc.format = nvrhi::Format::RGBA16_FLOAT; + halfDesc.isRenderTarget = true; + halfDesc.isTransient = true; + halfDesc.debugName = "rt_SunShafts_half"; + auto shaftsHalf = fg.CreateTexture("rt_SunShafts_half", halfDesc); + + ResourceDesc outDesc; + outDesc.type = ResourceDesc::Type::Texture2D; + outDesc.width = width; + outDesc.height = height; + outDesc.format = nvrhi::Format::RGBA16_FLOAT; + outDesc.isRenderTarget = true; + outDesc.isTransient = true; + outDesc.debugName = "rt_SunShafts"; + auto output = fg.CreateTexture("rt_SunShafts", outDesc); + + // ── Pass 1: half-res ray march (shafts term only) ───────────────────────── + fg.addCallbackPass( + "SunShaftsMarch", + [=, &passState](FrameGraph& builder, PassHandle passHandle, SunShaftsPassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.width = halfW; + data.height = halfH; + data.intensity = intensity; + data.quality = quality; + data.passState = &passState; + data.shadowMapArray = shadowMapArray; + for (int i = 0; i < 3; ++i) + data.shadowCascades[i] = (shadowCascades && shadowCascades[i]) ? shadowCascades[i] : shadowMapArray; + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); + if (shadowMapHandle.is_valid()) + data.shadowMap = pb.read(shadowMapHandle, ResourceState::ShaderResource); + data.output = pb.write(shaftsHalf, ResourceState::RenderTarget); + }, + [](const SunShaftsPassData& data, const FrameGraph& fg, fg::RenderContext* ctx) { + auto* ps = data.passState; + if (!ps || !ps->pipeline || !ps->layout) + return; + + nvrhi::ICommandList* cmd = ctx->GetCommandList(); + nvrhi::IDevice* nv = cmd->getDevice(); + auto* depthTex = fg.GetPhysicalTexture(data.depth); + auto* worldPosTex = fg.GetPhysicalTexture(data.worldPos); + auto* outTex = fg.GetPhysicalTexture(data.output); + if (!depthTex || !worldPosTex || !outTex || !data.shadowMapArray) + return; + + auto& cache = GetPassResourceCache(); + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(StaticGlobals), ctx->GetDevice()); + auto shaftCB = cache.GetOrCreateVolatileCB( + "SunShafts", "SunShaftsCB", sizeof(SunShaftsCBGPU), ctx->GetDevice()); + + StaticGlobals sg = BuildStaticGlobals(); + cmd->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + + SunShaftsCBGPU cb{}; + const float samples = (data.quality >= 3) ? 40.f : 20.f; + const float scale = ps_r_sun_shafts_scale > 0.f ? ps_r_sun_shafts_scale : 1.f; + cb.shaft_params.set(data.intensity, float(data.quality), samples, scale); + cmd->writeBuffer(shaftCB, &cb, sizeof(cb)); + + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsRefl = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psRefl = loader->GetCachedReflection("volumetric/sunshafts", ".ps"); + if (!vsRefl || !psRefl) + return; + + BindingSetBuilder bsb(*vsRefl, *psRefl, nv, "SunShaftsMarch"); + bsb.ConstantBuffer("static_globals", staticGlobalsCB) + .ConstantBuffer("SunShaftsCB", shaftCB) + .Texture("g_Depth", depthTex) + .Texture("g_WorldPos", worldPosTex); + { + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + auto& smpCache = GetPassResourceCache(); + nvrhi::ITexture* dummy2D = smpCache.GetDummyShadowMap2D(nv); + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = data.shadowCascades[i] ? data.shadowCascades[i] + : (i == 0 && data.shadowMapArray ? data.shadowMapArray : dummy2D); + if (t) + bsb.Texture(kNames[i], t); + } + } + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->layout, nv); + if (!bindingSet) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(outTex); + auto fb = cache.GetOrCreateFramebuffer("SunShaftsMarch", fbDesc, nv); + + nvrhi::Viewport vp(0.f, float(data.width), 0.f, float(data.height), 0.f, 1.f); + nvrhi::GraphicsState state; + state.pipeline = ps->pipeline; + state.framebuffer = fb; + state.viewport.addViewportAndScissorRect(vp); + state.addBindingSet(bindingSet); + cmd->setGraphicsState(state); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); + }); + + // ── Pass 2: full-res combine (upsample + blur + add to scene) ───────────── + auto& combineData = fg.addCallbackPass( + "SunShaftsCombine", + [=, &passState](FrameGraph& builder, PassHandle passHandle, SunShaftsCombineData& data) { + RenderPassBuilder pb(builder, passHandle); + data.width = width; + data.height = height; + data.halfWidth = halfW; + data.halfHeight = halfH; + data.passState = &passState; + data.sceneColor = pb.read(sceneColor, ResourceState::ShaderResource); + data.shafts = pb.read(shaftsHalf, ResourceState::ShaderResource); + data.output = pb.write(output, ResourceState::RenderTarget); + }, + [](const SunShaftsCombineData& data, const FrameGraph& fg, fg::RenderContext* ctx) { + auto* ps = data.passState; + if (!ps || !ps->combinePipeline || !ps->combineLayout) + return; + + nvrhi::ICommandList* cmd = ctx->GetCommandList(); + nvrhi::IDevice* nv = cmd->getDevice(); + auto* colorTex = fg.GetPhysicalTexture(data.sceneColor); + auto* shaftsTex = fg.GetPhysicalTexture(data.shafts); + auto* outTex = fg.GetPhysicalTexture(data.output); + if (!colorTex || !shaftsTex || !outTex) + return; + + auto& cache = GetPassResourceCache(); + auto combineCB = cache.GetOrCreateVolatileCB( + "SunShafts", "SunShaftsCombineCB", sizeof(SunShaftsCombineCBGPU), ctx->GetDevice()); + + SunShaftsCombineCBGPU cb{}; + cb.combine_params.set( + 1.f / float(data.halfWidth), 1.f / float(data.halfHeight), 0.f, 0.f); + cmd->writeBuffer(combineCB, &cb, sizeof(cb)); + + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsRefl = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psRefl = loader->GetCachedReflection("volumetric/sunshafts_combine", ".ps"); + if (!vsRefl || !psRefl) + return; + + BindingSetBuilder bsb(*vsRefl, *psRefl, nv, "SunShaftsCombine"); + bsb.ConstantBuffer("SunShaftsCombineCB", combineCB) + .Texture("g_Color", colorTex) + .Texture("g_Shafts", shaftsTex); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->combineLayout, nv); + if (!bindingSet) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(outTex); + auto fb = cache.GetOrCreateFramebuffer("SunShaftsCombine", fbDesc, nv); + + nvrhi::Viewport vp(0.f, float(data.width), 0.f, float(data.height), 0.f, 1.f); + nvrhi::GraphicsState state; + state.pipeline = ps->combinePipeline; + state.framebuffer = fb; + state.viewport.addViewportAndScissorRect(vp); + state.addBindingSet(bindingSet); + cmd->setGraphicsState(state); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); + }); + + return combineData.output; +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h new file mode 100644 index 00000000000..291cfc2e98b --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h @@ -0,0 +1,44 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include + +namespace xray::render::framegraph +{ +class FrameGraph; +} + +namespace xray::render::fg +{ +class RenderDevice; +} + +namespace xray::render::fg::passes +{ + +struct SunShaftsPassState +{ + nvrhi::GraphicsPipelineHandle pipeline; // half-res march (shafts-only) + nvrhi::BindingLayoutHandle layout; + nvrhi::GraphicsPipelineHandle combinePipeline; // full-res upsample + add to scene + nvrhi::BindingLayoutHandle combineLayout; + bool initialized = false; +}; + +framegraph::VirtualResourceHandle setupSunShaftsPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle worldPos, + nvrhi::ITexture* shadowMapArray, + framegraph::VirtualResourceHandle shadowMapHandle, + u32 width, + u32 height, + SunShaftsPassState& state, + nvrhi::ITexture* const* shadowCascades = nullptr); + +void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.cpp new file mode 100644 index 00000000000..e884b73bf4c --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.cpp @@ -0,0 +1,300 @@ +#include "stdafx.h" +#include "TAAPassSetup.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" + +extern ENGINE_API float ps_r_taa_sharpness; +extern ENGINE_API int ps_r_taa; + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +float g_taa_jitter_px = 0.f; +float g_taa_jitter_py = 0.f; +float g_taa_jitter_prev_px = 0.f; +float g_taa_jitter_prev_py = 0.f; +Fmatrix g_taa_unjittered_full_transform; +Fmatrix g_taa_unjittered_inv_full_transform; + +void ApplyTAAJitter() +{ + // Keep temporal reprojection in the unjittered camera space. + g_taa_unjittered_full_transform.mul(Device.mProject, Device.mView); + g_taa_unjittered_inv_full_transform.invert_44(g_taa_unjittered_full_transform); + + g_taa_jitter_prev_px = g_taa_jitter_px; + g_taa_jitter_prev_py = g_taa_jitter_py; + g_taa_jitter_px = 0.f; + g_taa_jitter_py = 0.f; + // Full projection jitter currently shifts the whole frame in this pipeline. + // Keep TAA temporal accumulation enabled, but leave the camera projection stable. + + Device.mFullTransform = g_taa_unjittered_full_transform; + Device.mInvFullTransform = g_taa_unjittered_inv_full_transform; + if (GEnv.Render) + GEnv.Render->SetCacheXform(Device.mView, Device.mProject); +} + +namespace +{ +void EnsureHistory(nvrhi::IDevice* nv, TAAPassState& state, u32 w, u32 h) +{ + if (state.history[0] && state.historyW == w && state.historyH == h) + return; + for (int i = 0; i < 2; ++i) + { + nvrhi::TextureDesc td; + td.width = w; + td.height = h; + td.format = nvrhi::Format::RGBA16_FLOAT; + td.isRenderTarget = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = (i == 0) ? "rt_TAA_History0" : "rt_TAA_History1"; + state.history[i] = nv->createTexture(td); + } + state.historyW = w; + state.historyH = h; + state.hasHistory = false; + state.historyIndex = 0; + + // Clear so first temporal frame never blends garbage (nested-frame look) + if (state.history[0] && state.history[1]) + { + nvrhi::CommandListHandle cmd = nv->createCommandList(); + cmd->open(); + cmd->clearTextureFloat(state.history[0], nvrhi::AllSubresources, nvrhi::Color(0.f)); + cmd->clearTextureFloat(state.history[1], nvrhi::AllSubresources, nvrhi::Color(0.f)); + cmd->close(); + nv->executeCommandList(cmd); + } +} + +void InitializeTAA(nvrhi::IDevice* device, TAAPassState& state) +{ + // Hot-reload path: bump layout/PSO name when shader binding changes + if (state.initialized && state.pipeline && state.layout) + return; + if (state.initialized && (!state.pipeline || !state.layout)) + state.initialized = false; + if (state.initialized || !device) + return; + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + { + state.initialized = true; + return; + } + auto vs = loader->LoadVertexShader("fullscreen"); + auto ps = loader->LoadPixelShader("taa"); + if (!vs.handle || !ps.handle) + { + state.initialized = true; + return; + } + auto& cache = GetPassResourceCache(); + state.layout = cache.GetOrCreateBindingLayoutFromReflection( + "TAA_v4", *vs.reflection, *ps.reflection, device); + if (state.layout) + { + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs.handle); + desc.setPixelShader(ps.handle); + desc.addBindingLayout(state.layout); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.blendState.targets[0].setBlendEnable(false); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + nvrhi::FramebufferInfoEx fb; + fb.addColorFormat(nvrhi::Format::RGBA16_FLOAT); + state.pipeline = cache.GetOrCreatePipeline("TAA_v4", desc, fb, device); + } + state.initialized = true; +} +} // namespace + +void ShutdownTAAPass(TAAPassState& state) +{ + state.pipeline = nullptr; + state.layout = nullptr; + state.history[0] = nullptr; + state.history[1] = nullptr; + state.hasHistory = false; + state.initialized = false; +} + +framegraph::VirtualResourceHandle setupTAAPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle sceneColor, + VirtualResourceHandle depth, + VirtualResourceHandle motionVectors, + u32 width, + u32 height, + bool hasPrevFrame, + TAAPassState& state) +{ + if (!device || !device->GetNVRHIDevice()) + return sceneColor; + + InitializeTAA(device->GetNVRHIDevice(), state); + if (!state.pipeline || !state.layout) + return sceneColor; + + EnsureHistory(device->GetNVRHIDevice(), state, width, height); + if (!state.history[0] || !state.history[1]) + return sceneColor; + + const u32 readIdx = state.historyIndex; + const u32 writeIdx = 1u - readIdx; + + ResourceDesc outDesc; + outDesc.type = ResourceDesc::Type::Texture2D; + outDesc.width = width; + outDesc.height = height; + outDesc.format = nvrhi::Format::RGBA16_FLOAT; + outDesc.isRenderTarget = true; + outDesc.isTransient = true; + outDesc.debugName = "rt_TAA"; + auto output = fg.CreateTexture("rt_TAA", outDesc); + + ResourceDesc histDesc; + histDesc.type = ResourceDesc::Type::Texture2D; + histDesc.width = width; + histDesc.height = height; + histDesc.format = nvrhi::Format::RGBA16_FLOAT; + histDesc.isImported = true; + histDesc.debugName = "rt_TAA_HistoryRead"; + auto historyRead = fg.ImportTexture("rt_TAA_HistoryRead", state.history[readIdx].Get(), histDesc); + + const bool useTemporal = motionVectors.is_valid() && hasPrevFrame && state.hasHistory; + + struct PassData + { + VirtualResourceHandle current, history, motion, depth, output; + TAAPassState* passState = nullptr; + fg::RenderDevice* device = nullptr; + u32 width = 0, height = 0; + u32 writeIdx = 0; + bool useTemporal = false; + }; + + auto& passData = fg.addCallbackPass( + "TAA", + [&, output, historyRead, useTemporal, writeIdx](FrameGraph& b, PassHandle ph, PassData& data) { + RenderPassBuilder pb(b, ph); + data.passState = &state; + data.device = device; + data.width = width; + data.height = height; + data.writeIdx = writeIdx; + data.useTemporal = useTemporal; + data.current = pb.read(sceneColor, ResourceState::ShaderResource); + data.history = pb.read(historyRead, ResourceState::ShaderResource); + data.depth = pb.read(depth, ResourceState::ShaderResource); + if (useTemporal) + data.motion = pb.read(motionVectors, ResourceState::ShaderResource); + data.output = pb.write(output, ResourceState::RenderTarget); + pb.sideEffects(); // history copy after resolve + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd ? cmd->getDevice() : nullptr; + auto* cur = graph.GetPhysicalTexture(data.current); + auto* hist = graph.GetPhysicalTexture(data.history); + auto* depthTex = graph.GetPhysicalTexture(data.depth); + auto* out = graph.GetPhysicalTexture(data.output); + if (!cmd || !nv || !cur || !hist || !depthTex || !out || !data.passState) + return; + + TAAPassState& st = *data.passState; + auto& cache = GetPassResourceCache(); + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psR = loader->GetCachedReflection("taa", ".ps"); + if (!vsR || !psR || !st.pipeline || !st.layout) + return; + + nvrhi::ITexture* motionTex = nullptr; + if (data.useTemporal && data.motion.is_valid()) + motionTex = graph.GetPhysicalTexture(data.motion); + if (!motionTex) + { + // Zero motion → treat as first frame (history unused effectively via high blend) + static nvrhi::TextureHandle s_zeroMotion; + if (!s_zeroMotion) + { + nvrhi::TextureDesc td; + td.width = 1; + td.height = 1; + td.format = nvrhi::Format::RG16_FLOAT; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "DummyZeroMotion"; + s_zeroMotion = nv->createTexture(td); + float zeros[2] = {0.f, 0.f}; + cmd->writeTexture(s_zeroMotion, 0, 0, zeros, sizeof(zeros)); + } + motionTex = s_zeroMotion; + } + + auto* cb = cache.GetOrCreateVolatileCB("TAA", "TAAParams_v3", sizeof(TAAParamsCB), data.device); + TAAParamsCB params{}; + params.screenSizeX = float(data.width); + params.screenSizeY = float(data.height); + params.blendAlpha = data.useTemporal ? 0.38f : 1.0f; + params.sharpness = ps_r_taa_sharpness; + params.jitterX = g_taa_jitter_px; + params.jitterY = g_taa_jitter_py; + params.prevJitterX = g_taa_jitter_prev_px; + params.prevJitterY = g_taa_jitter_prev_py; + if (cb) + cmd->writeBuffer(cb, ¶ms, sizeof(params)); + + BindingSetBuilder bsb(*vsR, *psR, nv, "TAA"); + if (cb) + bsb.ConstantBuffer("TAAParams", cb); + bsb.Texture("g_Current", cur) + .Texture("g_History", hist) + .Texture("g_Motion", motionTex) + .Texture("g_Depth", depthTex); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), st.layout, nv); + if (!set) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(out); + auto fb = cache.GetOrCreateFramebuffer("TAA", fbDesc, nv); + + nvrhi::GraphicsState gs; + gs.pipeline = st.pipeline; + gs.framebuffer = fb; + gs.bindings = {set}; + gs.viewport.addViewportAndScissorRect( + nvrhi::Viewport(float(data.width), float(data.height))); + cmd->setGraphicsState(gs); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); + + // Persist resolved frame into history[writeIdx] + if (st.history[data.writeIdx]) + { + cmd->copyTexture( + st.history[data.writeIdx], nvrhi::TextureSlice(), + out, nvrhi::TextureSlice()); + st.historyIndex = data.writeIdx; + st.hasHistory = true; + } + }); + + return passData.output; +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.h new file mode 100644 index 00000000000..41c5d30f053 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.h @@ -0,0 +1,59 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes +{ + +struct TAAPassState +{ + nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::BindingLayoutHandle layout; + nvrhi::TextureHandle history[2]; + u32 historyW = 0; + u32 historyH = 0; + u32 historyIndex = 0; + bool hasHistory = false; + bool initialized = false; +}; + +struct alignas(16) TAAParamsCB +{ + float screenSizeX, screenSizeY; + float blendAlpha; + float sharpness; + float jitterX, jitterY; // current frame Halton offset in pixels [-0.5, +0.5] + float prevJitterX, prevJitterY; // previous frame (for history reprojection) +}; + +// Pixel-space Halton jitter (filled by ApplyTAAJitter) +extern float g_taa_jitter_px; +extern float g_taa_jitter_py; +extern float g_taa_jitter_prev_px; +extern float g_taa_jitter_prev_py; + +// View-proj without subpixel jitter — use for motion vectors / prev-frame storage +extern Fmatrix g_taa_unjittered_full_transform; +extern Fmatrix g_taa_unjittered_inv_full_transform; + +void ApplyTAAJitter(); + +framegraph::VirtualResourceHandle setupTAAPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle motionVectors, // may be invalid + u32 width, + u32 height, + bool hasPrevFrame, + TAAPassState& state); + +void ShutdownTAAPass(TAAPassState& state); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/ThunderboltPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ThunderboltPassSetup.cpp index 582a37b3fe6..32a4b6d0d82 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ThunderboltPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ThunderboltPassSetup.cpp @@ -4,23 +4,35 @@ #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" #include "Layers/xrRender/RenderContext/RenderContext.h" #include "Layers/xrRender/fgThunderboltRender.h" namespace xray::render::fg::passes { -framegraph::VirtualResourceHandle setupThunderboltPass(framegraph::FrameGraph& fg, framegraph::VirtualResourceHandle inputTarget, - framegraph::VirtualResourceHandle depthTarget, FGThunderboltRender* renderer) +framegraph::VirtualResourceHandle setupThunderboltPass( + framegraph::FrameGraph& fg, + framegraph::VirtualResourceHandle inputTarget, + framegraph::VirtualResourceHandle depthTarget, + framegraph::VirtualResourceHandle worldPosTarget, + FGThunderboltRender* renderer) { using namespace framegraph; + if (!depthTarget.is_valid() || !worldPosTarget.is_valid()) + { + Msg("! [ThunderboltPass] Missing depth/worldPos — skipping bolt (would be unoccluded)"); + return inputTarget; + } + auto& passData = fg.addCallbackPass( "Thunderbolt", - [inputTarget, depthTarget, renderer](FrameGraph& builder, PassHandle passHandle, ThunderboltPassData& data) + [inputTarget, depthTarget, worldPosTarget, renderer](FrameGraph& builder, PassHandle passHandle, ThunderboltPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.renderer = renderer; - data.depth = passBuilder.read(depthTarget, ResourceState::DepthStencilRead); + data.depth = passBuilder.readWrite(depthTarget, ResourceState::DepthStencilWrite); + data.worldPos = passBuilder.read(worldPosTarget, ResourceState::ShaderResource); data.output = passBuilder.readWrite(inputTarget, ResourceState::RenderTarget); }, [](const ThunderboltPassData& data, const FrameGraph& fg, fg::RenderContext* ctx) @@ -28,18 +40,21 @@ framegraph::VirtualResourceHandle setupThunderboltPass(framegraph::FrameGraph& f if (!data.renderer || !data.renderer->HasWork()) return; nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + nvrhi::IDevice* nv = cmdList ? cmdList->getDevice() : nullptr; auto* outputRT = fg.GetPhysicalTexture(data.output); auto* depth = fg.GetPhysicalTexture(data.depth); - if (!cmdList || !outputRT) + auto* worldPos = fg.GetPhysicalTexture(data.worldPos); + if (!cmdList || !nv || !outputRT || !depth || !worldPos) return; nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(outputRT); - if (depth) - fbDesc.setDepthAttachment(depth); - auto framebuffer = cmdList->getDevice()->createFramebuffer(fbDesc); + fbDesc.setDepthAttachment(depth); + auto framebuffer = GetPassResourceCache().GetOrCreateFramebuffer("ThunderboltPass_Soft", fbDesc, nv); + if (!framebuffer) + return; - data.renderer->Draw(cmdList, framebuffer); + data.renderer->Draw(cmdList, framebuffer, worldPos); }); return passData.output; diff --git a/src/Layers/xrRender/FrameGraphPasses/ThunderboltPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ThunderboltPassSetup.h index 1c42a5f2df4..438bc5674e6 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ThunderboltPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/ThunderboltPassSetup.h @@ -19,9 +19,14 @@ struct ThunderboltPassData { framegraph::VirtualResourceHandle output; framegraph::VirtualResourceHandle depth; + framegraph::VirtualResourceHandle worldPos; FGThunderboltRender* renderer = nullptr; }; -framegraph::VirtualResourceHandle setupThunderboltPass(framegraph::FrameGraph& fg, framegraph::VirtualResourceHandle inputTarget, - framegraph::VirtualResourceHandle depthTarget, FGThunderboltRender* renderer); +framegraph::VirtualResourceHandle setupThunderboltPass( + framegraph::FrameGraph& fg, + framegraph::VirtualResourceHandle inputTarget, + framegraph::VirtualResourceHandle depthTarget, + framegraph::VirtualResourceHandle worldPosTarget, + FGThunderboltRender* renderer); } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.cpp index 3a85e7dfe6c..c4290ab48e0 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.cpp @@ -44,7 +44,7 @@ void InitializeTonemapPass(nvrhi::IDevice* device, TonemapPassState& state) { auto& cache = framegraph::GetPassResourceCache(); state.bindingLayout = cache.GetOrCreateBindingLayoutFromReflection( - "TonemapPass", *vsResult.reflection, *psResult.reflection, device); + "TonemapPass_ACES", *vsResult.reflection, *psResult.reflection, device); if (state.bindingLayout) { nvrhi::GraphicsPipelineDesc pipeDesc; @@ -63,7 +63,7 @@ void InitializeTonemapPass(nvrhi::IDevice* device, TonemapPassState& state) { fbFmt = GEnv.Backend->GetBackBuffer()->getDesc().format; fbInfo.addColorFormat(fbFmt); - state.pipeline = cache.GetOrCreatePipeline("TonemapPass", pipeDesc, fbInfo, device); + state.pipeline = cache.GetOrCreatePipeline("TonemapPass_ACES", pipeDesc, fbInfo, device); } } } @@ -118,11 +118,16 @@ framegraph::VirtualResourceHandle setupTonemapPass( if (hasOutputTarget) { data.ldrOutput = passBuilder.write(outputTarget, ResourceState::RenderTarget); } else { + // Must match tonemap PSO format (backbuffer), otherwise NVRHI rejects setGraphicsState + nvrhi::Format ldrFmt = nvrhi::Format::RGBA8_UNORM; + if (GEnv.Backend && GEnv.Backend->GetBackBuffer()) + ldrFmt = GEnv.Backend->GetBackBuffer()->getDesc().format; + framegraph::ResourceDesc ldrDesc; ldrDesc.type = framegraph::ResourceDesc::Type::Texture2D; ldrDesc.width = width; ldrDesc.height = height; - ldrDesc.format = nvrhi::Format::RGBA8_UNORM; + ldrDesc.format = ldrFmt; ldrDesc.isRenderTarget = true; ldrDesc.isTransient = false; ldrDesc.debugName = "rt_Final"; @@ -147,6 +152,19 @@ framegraph::VirtualResourceHandle setupTonemapPass( if (!hdrTexture || !ldrTexture) return; + nvrhi::ITexture* exposureTex = ps->fallbackExposureTexture; + if (data.hasExposure && data.exposureInput.is_valid()) + { + if (auto* e = fg.GetPhysicalTexture(data.exposureInput)) + exposureTex = e; + } + if (!exposureTex && data.exposurePassState) + exposureTex = GetExposureTexture(*data.exposurePassState); + if (!exposureTex) + exposureTex = ps->fallbackExposureTexture; + if (!exposureTex) + return; + auto& cache = framegraph::GetPassResourceCache(); auto* vsRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("tonemap", ".vs"); @@ -156,13 +174,14 @@ framegraph::VirtualResourceHandle setupTonemapPass( framegraph::BindingSetBuilder bsb(*vsRefl, *psRefl, device, "Tonemap"); bsb.Texture("t_hdr", hdrTexture); + bsb.Texture("t_exposure", exposureTex); auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->bindingLayout, device); if (!bindingSet) return; nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(ldrTexture); - auto framebuffer = cache.GetOrCreateFramebuffer("TonemapPass", fbDesc, device); + auto framebuffer = cache.GetOrCreateFramebuffer("TonemapPass_ACES", fbDesc, device); nvrhi::Viewport viewport; viewport.minX = 0; diff --git a/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.h index a9b662b666e..07674234f2c 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.h @@ -36,8 +36,7 @@ struct TonemapPassData { }; // Lambda-based tonemap pass setup -// Converts HDR scene color (RGBA16_FLOAT) to LDR output (RGBA8_UNORM) using ACES filmic tonemap -// Now accepts exposure texture from ExposurePass for auto-exposure +// Converts HDR → LDR via classic CoP modified Reinhard (white=1.7) + MiddleGray exposure // If outputTarget is valid, writes directly to it (e.g., imported backbuffer) // If outputTarget is invalid, creates internal rt_Final texture framegraph::VirtualResourceHandle setupTonemapPass( diff --git a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp index 2cc419db1e7..26308202c73 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp @@ -15,6 +15,10 @@ #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" #include "PassCommon.h" #include "Layers/xrRender/ClusteredLightManager.h" +#include "Layers/xrRender/ResourceManager/FGResourceManager.h" +#include "Layers/xrRender/ResourceManager/TextureManager.h" +#include "xrEngine/Environment.h" +#include "xrEngine/IGame_Persistent.h" namespace xray::render::fg::passes { @@ -40,10 +44,12 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame state.ps = psResult.handle; auto& cache = framegraph::GetPassResourceCache(); - state.layout = cache.GetOrCreateBindingLayoutFromReflection("TransparentPass", *vsResult.reflection, *psResult.reflection, nvDevice); + state.layout = cache.GetOrCreateBindingLayoutFromReflection("TransparentPass_CSMLadder", *vsResult.reflection, *psResult.reflection, nvDevice); if (!state.layout) return; + state.sampler = cache.GetLinearWrapSampler(nvDevice); + u32 attrCount = 0; auto* attrs = GetUnifiedVertexAttributes(attrCount); state.inputLayout = nvDevice->createInputLayout(attrs, attrCount, state.vs); @@ -80,14 +86,53 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame rt0.destBlendAlpha = nvrhi::BlendFactor::InvSrcAlpha; rt0.blendOpAlpha = nvrhi::BlendOp::Add; - state.pipeline = cache.GetOrCreatePipeline("TransparentPass", pipeDesc, fbInfo, nvDevice); + state.pipeline = cache.GetOrCreatePipeline("TransparentPass_CSMLadder", pipeDesc, fbInfo, nvDevice); if (!state.pipeline) return; QueryBindingLayoutFromPipeline(state.pipeline, state.layout); + // Dedicated water PSO (soft + foam + SSR) + { + auto waterVs = shaderLoader->LoadVertexShader("water", "main"); + auto waterPs = shaderLoader->LoadPixelShader("water", "main"); + if (waterVs.handle && waterPs.handle && waterVs.reflection && waterPs.reflection) + { + state.waterVs = waterVs.handle; + state.waterPs = waterPs.handle; + state.waterLayout = cache.GetOrCreateBindingLayoutFromReflection( + "TransparentWater_v3", *waterVs.reflection, *waterPs.reflection, nvDevice); + if (state.waterLayout) + { + nvrhi::GraphicsPipelineDesc waterDesc = pipeDesc; + waterDesc.VS = state.waterVs; + waterDesc.PS = state.waterPs; + waterDesc.inputLayout = nvDevice->createInputLayout(attrs, attrCount, state.waterVs); + if (bindlessLayout) + waterDesc.bindingLayouts = {state.waterLayout, bindlessLayout}; + else + waterDesc.bindingLayouts = {state.waterLayout}; + waterDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + state.waterPipeline = cache.GetOrCreatePipeline("TransparentWater_v3", waterDesc, fbInfo, nvDevice); + } + waterVs.reflection = nullptr; + waterPs.reflection = nullptr; + } + if (!state.waterPipeline) + Msg("! [TransparentPass] Water PSO unavailable — water clipped from forward"); + } + + if (auto* resMgr = device->GetFGResourceManager()) + { + if (auto* texMgr = resMgr->GetTextureManager()) + { + auto foam = texMgr->LoadTexture("water" DELIMITER "water_foam"); + state.foamTexture = texMgr->GetNVRHITexture(foam); + } + } + state.initialized = true; - Msg("* [TransparentPass] Pipeline initialized"); + Msg("* [TransparentPass] Pipeline initialized (water=%d)", state.waterPipeline ? 1 : 0); } framegraph::DefaultOutputLayout setupTransparentPass( @@ -180,10 +225,22 @@ framegraph::DefaultOutputLayout setupTransparentPass( const auto& cfg = data.config; auto& variantTexBuffer = bindless::VariantTextureBuffer::Instance(); + auto& clm = ClusteredLightManager::Instance(); + auto& passCache = framegraph::GetPassResourceCache(); + + nvrhi::ITexture* dummy2D = passCache.GetDummyShadowMap2D(nvDevice); + nvrhi::ITexture* sky0 = cfg.envSky0 + ? cfg.envSky0 + : passCache.GetDummyCubeMap(nvDevice); + nvrhi::ITexture* sky1 = cfg.envSky1 + ? cfg.envSky1 + : passCache.GetDummyCubeMap(nvDevice); auto* shaderLoader = GEnv.Render->GetShaderLoader(); auto* vsReflection = shaderLoader->GetCachedReflection("bindless_forward", ".vs"); auto* psReflection = shaderLoader->GetCachedReflection("bindless_forward", ".ps"); + if (!vsReflection || !psReflection) + return; framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Transparent"); bsb.ConstantBuffer("static_globals", staticGlobalsCB); @@ -191,37 +248,70 @@ framegraph::DefaultOutputLayout setupTransparentPass( bsb.BufferSRV("g_InstanceData", cfg.instanceBuffer); bsb.BufferSRV("g_CompactBatchIndices", cfg.compactBatchIndicesBuffer); bsb.BufferSRV("g_CompactMaterialIDs", cfg.compactMaterialIDBuffer); - bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); - bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); - bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); - - auto bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), data.passState->layout, nvDevice); - R_ASSERT2(bindingSet, "Transparent binding set creation failed"); + bsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); + bsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); + bsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); + { + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = cfg.shadowCascades[i] ? cfg.shadowCascades[i] + : (i == 0 && cfg.shadowMapArray ? cfg.shadowMapArray : dummy2D); + if (t) + bsb.Texture(kNames[i], t); + } + } + { + nvrhi::ITexture* contactHist = cfg.contactHistory + ? cfg.contactHistory + : passCache.GetDummyContactHistory(nvDevice); + if (contactHist) + bsb.Texture("g_ContactHistory", contactHist); + } + { + nvrhi::ITexture* localAtlas = cfg.localShadowAtlas + ? cfg.localShadowAtlas + : passCache.GetDummyShadowMap(nvDevice); + if (localAtlas) + bsb.Texture("g_LocalShadowAtlas", localAtlas); + } + if (sky0) + bsb.Texture("s_env0", sky0); + if (sky1) + bsb.Texture("s_env1", sky1); + + auto bindingSet = passCache.GetOrCreateBindingSet(bsb.Build(), data.passState->layout, nvDevice); + if (!bindingSet) + { + Msg("! [TransparentPass] Binding set create failed — skip draw"); + return; + } - nvrhi::GraphicsState state; - state.pipeline = data.passState->pipeline; - state.framebuffer = framebuffer; - state.bindings = { bindingSet }; + nvrhi::GraphicsState gfxState; + gfxState.pipeline = data.passState->pipeline; + gfxState.framebuffer = framebuffer; + gfxState.bindings = { bindingSet }; auto* backend = data.device->GetBackend(); + nvrhi::IBindingSet* bindlessTable = nullptr; if (backend) { - auto* bindlessTable = backend->GetBindlessDescriptorTable(); + bindlessTable = backend->GetBindlessDescriptorTable(); if (bindlessTable) - state.addBindingSet(bindlessTable); + gfxState.addBindingSet(bindlessTable); } - state.vertexBuffers = { + gfxState.vertexBuffers = { {cfg.megaVertexBuffer, 0, 0}, {drawIndexBuffer, 1, 0} }; - state.indexBuffer = { cfg.megaIndexBuffer, nvrhi::Format::R32_UINT, 0 }; - state.indirectParams = cfg.compactDrawArgsBuffer; - state.indirectCountBuffer = cfg.compactCountBuffer; + gfxState.indexBuffer = { cfg.megaIndexBuffer, nvrhi::Format::R32_UINT, 0 }; + gfxState.indirectParams = cfg.compactDrawArgsBuffer; + gfxState.indirectCountBuffer = cfg.compactCountBuffer; const auto& rtDesc = colorRT->getDesc(); nvrhi::Viewport viewport(0.0f, static_cast(rtDesc.width), 0.0f, static_cast(rtDesc.height), 0.0f, 1.0f); - state.viewport.addViewport(viewport); - state.viewport.addScissorRect(nvrhi::Rect(rtDesc.width, rtDesc.height)); + gfxState.viewport.addViewport(viewport); + gfxState.viewport.addScissorRect(nvrhi::Rect(rtDesc.width, rtDesc.height)); if (cfg.variantPartition.Enabled()) { auto* backendDev = data.device->GetBackend(); @@ -231,22 +321,143 @@ framegraph::DefaultOutputLayout setupTransparentPass( vpCfg.inputLayout = data.passState->inputLayout; vpCfg.passLayout = data.passState->layout; vpCfg.bindlessLayout = backendDev ? backendDev->GetBindlessLayout() : nullptr; - vpCfg.bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; - vpCfg.sampler = data.passState->sampler; + vpCfg.bindlessTable = bindlessTable; + vpCfg.sampler = data.passState->sampler + ? data.passState->sampler.Get() + : passCache.GetLinearWrapSampler(nvDevice); vpCfg.staticGlobalsCB = staticGlobalsCB; vpCfg.lightingCB = lightingCB; vpCfg.materialBuffer = matBuffer.GetBuffer(); vpCfg.variantTexBuffer = variantTexBuffer.GetBuffer(); vpCfg.instanceBuffer = cfg.instanceBuffer; vpCfg.megaVertexBuffer = cfg.megaVertexBuffer; + vpCfg.shadowMapArray = cfg.shadowCascades[0] ? cfg.shadowCascades[0] : cfg.shadowMapArray; + for (u32 i = 0; i < 3; ++i) + vpCfg.shadowCascades[i] = cfg.shadowCascades[i]; + vpCfg.localShadowAtlas = cfg.localShadowAtlas; + vpCfg.envSky0 = sky0; + vpCfg.envSky1 = sky1; + vpCfg.lightDataBuffer = clm.GetLightDataBuffer(); + vpCfg.clusterGridBuffer = clm.GetClusterGridBuffer(); + vpCfg.lightIndexListBuffer = clm.GetLightIndexListBuffer(); vpCfg.partition = cfg.variantPartition; vpCfg.selectTransparent = true; - DrawVariantPartition(cmdList, nvDevice, framebuffer, state, vpCfg); + DrawVariantPartition(cmdList, nvDevice, framebuffer, gfxState, vpCfg); } else { - cmdList->setGraphicsState(state); + cmdList->setGraphicsState(gfxState); DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, cfg.objectCount); } + + // Second draw: dedicated water PSO (soft depth + foam + SSR) + if (data.passState->waterPipeline && data.passState->waterLayout) + { + auto* waterVsRefl = shaderLoader->GetCachedReflection("water", ".vs"); + auto* waterPsRefl = shaderLoader->GetCachedReflection("water", ".ps"); + if (waterVsRefl && waterPsRefl) + { + // Snapshot HDR + depth for SSR/soft water (cannot sample RTs we write / DSV) + const auto& cdesc = colorRT->getDesc(); + if (!data.passState->waterSsrColor || + data.passState->waterSsrColor->getDesc().width != cdesc.width || + data.passState->waterSsrColor->getDesc().height != cdesc.height) + { + nvrhi::TextureDesc td = cdesc; + td.debugName = "WaterSSR_Color"; + td.isRenderTarget = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + data.passState->waterSsrColor = nvDevice->createTexture(td); + } + nvrhi::ITexture* ssrColor = data.passState->waterSsrColor; + // Unbind RT/DSV → Copy → SRV so water SSR can sample scene (Metal/MoltenVK). + if (ssrColor) + { + cmdList->setTextureState(colorRT, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmdList->setTextureState(ssrColor, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmdList->copyTexture(ssrColor, nvrhi::TextureSlice(), colorRT, nvrhi::TextureSlice()); + cmdList->setTextureState(ssrColor, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + + const auto& ddesc = depthRT->getDesc(); + if (!data.passState->waterSceneDepth || + data.passState->waterSceneDepth->getDesc().width != ddesc.width || + data.passState->waterSceneDepth->getDesc().height != ddesc.height) + { + nvrhi::TextureDesc td{}; + td.width = ddesc.width; + td.height = ddesc.height; + td.format = nvrhi::Format::D32; + td.debugName = "WaterSSR_Depth"; + td.isShaderResource = true; + td.isTypeless = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + data.passState->waterSceneDepth = nvDevice->createTexture(td); + } + nvrhi::ITexture* sceneDepthSrv = data.passState->waterSceneDepth; + if (sceneDepthSrv) + { + cmdList->setTextureState(depthRT, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmdList->setTextureState(sceneDepthSrv, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmdList->copyTexture(sceneDepthSrv, nvrhi::TextureSlice(), depthRT, nvrhi::TextureSlice()); + cmdList->setTextureState(sceneDepthSrv, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + // Restore attachments for the water draw that follows + cmdList->setTextureState(colorRT, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); + if (normalRT) + cmdList->setTextureState(normalRT, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); + if (baseColorRT) + cmdList->setTextureState(baseColorRT, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); + if (worldPosRT) + cmdList->setTextureState(worldPosRT, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); + // Transparent pass declares depth as read-only (test, no write) + cmdList->setTextureState(depthRT, nvrhi::AllSubresources, nvrhi::ResourceStates::DepthRead); + + auto waterCB = cache.GetOrCreateVolatileCB( + "TransparentWater", "WaterParams", sizeof(Fvector4), data.device); + Fvector4 wi{}; + wi.set(1.f, 1.f, 1.f, 1.f); + if (g_pGamePersistent) + { + const auto& env = g_pGamePersistent->Environment().CurrentEnv; + float intens = std::max(env.fog_color.x, std::max(env.fog_color.y, env.fog_color.z)); + wi.set(std::max(intens, 0.35f), intens, intens, 1.f); + } + cmdList->writeBuffer(waterCB, &wi, sizeof(wi)); + + nvrhi::ITexture* foam = data.passState->foamTexture; + if (!foam) + foam = passCache.GetDummyShadowMap2D(nvDevice); + + framegraph::BindingSetBuilder wbsb(*waterVsRefl, *waterPsRefl, nvDevice, "Transparent.Water"); + wbsb.ConstantBuffer("static_globals", staticGlobalsCB) + .ConstantBuffer("WaterParams", waterCB) + .BufferSRV("g_Materials", matBuffer.GetBuffer()) + .BufferSRV("g_InstanceData", cfg.instanceBuffer) + .BufferSRV("g_CompactBatchIndices", cfg.compactBatchIndicesBuffer) + .BufferSRV("g_CompactMaterialIDs", cfg.compactMaterialIDBuffer); + if (sky0) wbsb.Texture("s_env0", sky0); + if (sky1) wbsb.Texture("s_env1", sky1); + if (foam) wbsb.Texture("s_leaves", foam); + if (ssrColor) wbsb.Texture("g_SceneColor", ssrColor); + if (sceneDepthSrv) wbsb.Texture("g_SceneDepth", sceneDepthSrv); + + auto waterSet = passCache.GetOrCreateBindingSet( + wbsb.Build(), data.passState->waterLayout, nvDevice); + if (waterSet) + { + nvrhi::GraphicsState waterGfx = gfxState; + waterGfx.pipeline = data.passState->waterPipeline; + waterGfx.bindings = {waterSet}; + if (bindlessTable) + waterGfx.addBindingSet(bindlessTable); + cmdList->setGraphicsState(waterGfx); + DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, cfg.objectCount); + } + } + } } ); diff --git a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.h index 85f8bcd1259..c7c0549e41e 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.h @@ -30,6 +30,15 @@ struct TransparentPassConfig { VariantPartitionConfig variantPartition; + // Cascaded shadow maps + sky cubes for water reflections + nvrhi::ITexture* shadowMapArray = nullptr; + nvrhi::ITexture* shadowCascades[3] = {}; + nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* envSky0 = nullptr; + nvrhi::ITexture* envSky1 = nullptr; + nvrhi::ITexture* contactDepth = nullptr; // unused (history-only contact) + nvrhi::ITexture* contactHistory = nullptr; // g_ContactHistory @ t28 + bool IsValid() const { return objectCount > 0 && compactDrawArgsBuffer && megaVertexBuffer && megaIndexBuffer; } @@ -38,10 +47,17 @@ struct TransparentPassConfig { struct TransparentPassState { nvrhi::GraphicsPipelineHandle pipeline; nvrhi::BindingLayoutHandle layout; + nvrhi::GraphicsPipelineHandle waterPipeline; + nvrhi::BindingLayoutHandle waterLayout; nvrhi::InputLayoutHandle inputLayout; nvrhi::SamplerHandle sampler; nvrhi::ShaderHandle vs; nvrhi::ShaderHandle ps; + nvrhi::ShaderHandle waterVs; + nvrhi::ShaderHandle waterPs; + nvrhi::ITexture* foamTexture = nullptr; + nvrhi::TextureHandle waterSsrColor; // opaque HDR snapshot for SSR + nvrhi::TextureHandle waterSceneDepth; // depth copy (cannot sample DSV while attached) bool initialized = false; }; diff --git a/src/Layers/xrRender/FrameGraphPasses/UIPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/UIPassSetup.cpp index f390522141a..ce795484070 100644 --- a/src/Layers/xrRender/FrameGraphPasses/UIPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/UIPassSetup.cpp @@ -109,12 +109,20 @@ framegraph::VirtualResourceHandle setupUIPass( return; } - g_pGamePersistent->OnRenderPPUI_main(); + // Record order = draw order (last on top): + // in-game HUD → sequencers → loading → main menu → menu PP + uiRender->Clear(); + IUIRender* oldRenderer = GEnv.UIRender; + GEnv.UIRender = uiRender; + g_pGamePersistent->OnRenderInGameUI(); - if (g_pGamePersistent->IsLoadingScreenShown()) { - g_pGamePersistent->load_draw_internal(); - } g_pGamePersistent->OnRenderSequencers(); + if (g_pGamePersistent->IsLoadingScreenShown()) + g_pGamePersistent->load_draw_internal(); + g_pGamePersistent->OnRenderPPUI_main(); + g_pGamePersistent->OnRenderPPUI_PP(); + + GEnv.UIRender = oldRenderer; if (!uiRender->GetBatches().empty()) { StaticGlobals staticGlobalsCB = {}; diff --git a/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.cpp new file mode 100644 index 00000000000..f37510cc5be --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.cpp @@ -0,0 +1,455 @@ +#include "stdafx.h" +#include "VolumetricPassSetup.h" +#include "ParticlePassSetup.h" +#include "Layers/xrRender/Volumetrics/VolumetricRenderer.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/xrRender_console.h" +#include "xrEngine/IGame_Persistent.h" +#include "xrEngine/Environment.h" +#include "xrEngine/device.h" + +namespace fg +{ +extern xray::render::FrameGraphRenderer RImplementation; +} + +namespace xray::render::fg::passes +{ + +using namespace framegraph; + +namespace +{ + +void FillFroxelParams(FroxelParamsCB& cb, const VolumetricRenderer& vol) +{ + ZeroMemory(&cb, sizeof(cb)); + cb.dims[0] = VolumetricRenderer::kFroxelX; + cb.dims[1] = VolumetricRenderer::kFroxelY; + cb.dims[2] = VolumetricRenderer::kFroxelZ; + cb.nearPlane = VIEWPORT_NEAR; + cb.farPlane = std::max(g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.far_plane : 300.f, cb.nearPlane * 2.f); + const float logRatio = log2f(cb.farPlane / cb.nearPlane); + cb.invLogFarNear = (logRatio > 1e-5f) ? (1.f / logRatio) : 1.f; + + cb.invVP = Device.mInvFullTransform; + cb.cameraPos = Device.vCameraPosition; + + const auto& fog = vol.GetWorldFog().GetParams(); + cb.fogDensity = fog.density; + cb.fogHeightFalloff = fog.heightFalloff; + cb.fogBaseHeight = fog.baseHeight; + cb.fogAlbedo = fog.albedo; + + if (g_pGamePersistent) + { + const auto& env = g_pGamePersistent->Environment().CurrentEnv; + cb.sunDir.set(-env.sun_dir.x, -env.sun_dir.y, -env.sun_dir.z); + cb.sunDir.normalize_safe(); + cb.sunColor.set(env.sun_color.x, env.sun_color.y, env.sun_color.z); + cb.sunIntensity = std::max(env.m_fSunShaftsIntensity, 0.15f); + + const float n = env.fog_near; + const float f = std::max(env.fog_far, n + 1.0f); + const float r = 1.0f / (f - n); + cb.classicFogParams.set(-n * r, r, r, r); + } + else + { + cb.sunDir.set(0.3f, 0.8f, 0.2f); + cb.sunColor.set(1.f, 0.95f, 0.85f); + cb.sunIntensity = 1.0f; + cb.classicFogParams.set(0.0f, 0.001f, 0.001f, 0.001f); + } +} + +nvrhi::ComputePipelineHandle LoadComputePipe( + const char* passName, + const char* shaderName, + nvrhi::IDevice* device, + nvrhi::BindingLayoutHandle& outLayout) +{ + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + return nullptr; + + auto cs = loader->LoadComputeShader(shaderName); + if (!cs.handle || !cs.reflection) + { + Msg("! [VolumetricPass] Failed to load compute shader: %s", shaderName); + return nullptr; + } + + auto& cache = GetPassResourceCache(); + outLayout = cache.GetOrCreateBindingLayoutFromReflection(passName, *cs.reflection, device); + if (!outLayout) + return nullptr; + + nvrhi::ComputePipelineDesc desc; + desc.CS = cs.handle; + desc.bindingLayouts = {outLayout}; + return cache.GetOrCreateComputePipeline(passName, desc, device); +} + +} // namespace + +void InitializeVolumetricPass(nvrhi::IDevice* device, VolumetricPassState& state) +{ + if (state.initialized || !device) + return; + + state.clearPipeline = LoadComputePipe("VolClear", "volumetric\\clear_volume", device, state.clearLayout); + state.fogPipeline = LoadComputePipe("VolFog", "volumetric\\world_fog", device, state.fogLayout); + state.lightPipeline = LoadComputePipe("VolLight", "volumetric\\apply_lighting", device, state.lightLayout); + state.particlePipeline = LoadComputePipe("VolParticle", "volumetric\\particle_inject", device, state.particleLayout); + state.temporalPipeline = LoadComputePipe("VolTemporal", "volumetric\\temporal_reproject", device, state.temporalLayout); + + auto* loader = GEnv.Render->GetShaderLoader(); + auto vs = loader->LoadVertexShader("volumetric\\ray_march"); + auto ps = loader->LoadPixelShader("volumetric\\ray_march"); + if (vs.handle && ps.handle && vs.reflection && ps.reflection) + { + auto& cache = GetPassResourceCache(); + state.marchLayout = cache.GetOrCreateBindingLayoutFromReflection( + "VolMarch", *vs.reflection, *ps.reflection, device); + + if (state.marchLayout) + { + nvrhi::GraphicsPipelineDesc pipeDesc; + pipeDesc.setVertexShader(vs.handle); + pipeDesc.setPixelShader(ps.handle); + pipeDesc.addBindingLayout(state.marchLayout); + pipeDesc.setPrimType(nvrhi::PrimitiveType::TriangleList); + pipeDesc.renderState.blendState.targets[0].setBlendEnable(false); + pipeDesc.renderState.depthStencilState.setDepthTestEnable(false); + pipeDesc.renderState.depthStencilState.setDepthWriteEnable(false); + pipeDesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); + state.marchPipeline = cache.GetOrCreatePipeline("VolMarch", pipeDesc, fbInfo, device); + } + } + else + { + Msg("! [VolumetricPass] Failed to load ray_march shaders"); + } + + { + nvrhi::BufferDesc cb; + cb.byteSize = sizeof(FroxelParamsCB); + cb.isConstantBuffer = true; + cb.isVolatile = true; + cb.maxVersions = 16; + cb.debugName = "FroxelParamsCB"; + state.paramsCB = device->createBuffer(cb); + } + { + nvrhi::BufferDesc cb; + cb.byteSize = sizeof(TemporalParamsCB); + cb.isConstantBuffer = true; + cb.isVolatile = true; + cb.maxVersions = 16; + cb.debugName = "VolTemporalCB"; + state.temporalCB = device->createBuffer(cb); + } + { + nvrhi::BufferDesc cb; + cb.byteSize = sizeof(ParticleInjectParamsCB); + cb.isConstantBuffer = true; + cb.isVolatile = true; + cb.maxVersions = 16; + cb.debugName = "VolParticleParamsCB"; + state.particleParamsCB = device->createBuffer(cb); + } + { + nvrhi::BufferDesc bd; + bd.byteSize = sizeof(ParticleInjectPointGPU) * 256; + bd.structStride = sizeof(ParticleInjectPointGPU); + bd.canHaveUAVs = false; + bd.keepInitialState = true; + bd.initialState = nvrhi::ResourceStates::ShaderResource; + bd.debugName = "VolParticlePoints"; + state.particlePointsCB = device->createBuffer(bd); + } + + { + nvrhi::SamplerDesc s; + s.setAllFilters(true); + s.setAllAddressModes(nvrhi::SamplerAddressMode::Clamp); + state.linearSampler = device->createSampler(s); + } + + state.computeEnabled = state.clearPipeline && state.fogPipeline && state.lightPipeline && + state.marchPipeline && state.paramsCB; + state.initialized = true; + + Msg("* [VolumetricPass] Init: compute=%s temporal=%s particles=%s", + state.computeEnabled ? "OK" : "FAIL", + state.temporalPipeline ? "OK" : "off", + state.particlePipeline ? "OK" : "off"); +} + +void ShutdownVolumetricPass(VolumetricPassState& state) +{ + state.clearPipeline = nullptr; + state.fogPipeline = nullptr; + state.lightPipeline = nullptr; + state.particlePipeline = nullptr; + state.temporalPipeline = nullptr; + state.marchPipeline = nullptr; + state.clearLayout = nullptr; + state.fogLayout = nullptr; + state.lightLayout = nullptr; + state.particleLayout = nullptr; + state.temporalLayout = nullptr; + state.marchLayout = nullptr; + state.linearSampler = nullptr; + state.paramsCB = nullptr; + state.temporalCB = nullptr; + state.particleParamsCB = nullptr; + state.particlePointsCB = nullptr; + state.hasPrevVP = false; + state.initialized = false; + state.computeEnabled = false; +} + +VirtualResourceHandle setupVolumetricPass( + FrameGraph& fg, + RenderDevice* device, + VolumetricRenderer* volumetric, + VirtualResourceHandle sceneColor, + VirtualResourceHandle depth, + u32 width, + u32 height, + VolumetricPassState& state, + const xr_vector* particleBatches) +{ + if (!volumetric || !volumetric->IsReady() || !device) + return sceneColor; + + if (device->GetNVRHIDevice()) + InitializeVolumetricPass(device->GetNVRHIDevice(), state); + + if (!state.computeEnabled) + return sceneColor; + + volumetric->BeginFrame(); + + nvrhi::ITexture* froxelTex = volumetric->GetFroxelVolume(); + ResourceDesc froxelDesc; + froxelDesc.type = ResourceDesc::Type::Texture3D; + froxelDesc.width = VolumetricRenderer::kFroxelX; + froxelDesc.height = VolumetricRenderer::kFroxelY; + froxelDesc.depth = VolumetricRenderer::kFroxelZ; + froxelDesc.format = nvrhi::Format::RGBA16_FLOAT; + froxelDesc.allowUAV = true; + froxelDesc.isUAV = true; + froxelDesc.isImported = true; + froxelDesc.debugName = "FroxelVolume"; + auto froxelHandle = fg.ImportTexture("FroxelVolume", froxelTex, froxelDesc); + + ResourceDesc outDesc; + outDesc.type = ResourceDesc::Type::Texture2D; + outDesc.width = width; + outDesc.height = height; + outDesc.format = nvrhi::Format::RGBA16_FLOAT; + outDesc.isRenderTarget = true; + outDesc.debugName = "rt_VolumetricOut"; + + auto& passData = fg.addCallbackPass( + "VolumetricFog", + [&, sceneColor, depth, froxelHandle, width, height, volumetric, particleBatches]( + FrameGraph& builder, PassHandle passHandle, VolumetricPassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.sceneInput = pb.read(sceneColor, ResourceState::ShaderResource); + data.depthInput = pb.read(depth, ResourceState::ShaderResource); + data.froxelVolume = pb.readWrite(froxelHandle, ResourceState::UnorderedAccess); + data.sceneOutput = pb.createTexture("rt_VolumetricOut", outDesc); + data.volumetric = volumetric; + data.passState = &state; + data.particleBatches = particleBatches; + data.width = width; + data.height = height; + }, + [](const VolumetricPassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + if (!data.passState || !data.passState->computeEnabled || !data.volumetric) + return; + + nvrhi::ICommandList* cmd = ctx->GetCommandList(); + nvrhi::IDevice* nv = cmd ? cmd->getDevice() : nullptr; + auto* froxel = graph.GetPhysicalTexture(data.froxelVolume); + auto* scene = graph.GetPhysicalTexture(data.sceneInput); + auto* depthTex = graph.GetPhysicalTexture(data.depthInput); + auto* output = graph.GetPhysicalTexture(data.sceneOutput); + if (!cmd || !nv || !froxel || !scene || !depthTex || !output) + return; + + VolumetricPassState& st = *data.passState; + VolumetricRenderer& vol = *data.volumetric; + + FroxelParamsCB params{}; + FillFroxelParams(params, vol); + cmd->writeBuffer(st.paramsCB, ¶ms, sizeof(params)); + + const u32 gx = (VolumetricRenderer::kFroxelX + 7) / 8; + const u32 gy = (VolumetricRenderer::kFroxelY + 7) / 8; + const u32 gz = VolumetricRenderer::kFroxelZ; + + auto dispatchUAV = [&](nvrhi::IComputePipeline* pipe, nvrhi::IBindingLayout* layout) { + if (!pipe || !layout) + return; + nvrhi::BindingSetDesc bsDesc; + bsDesc.bindings = { + nvrhi::BindingSetItem::ConstantBuffer(5, st.paramsCB), + nvrhi::BindingSetItem::Texture_UAV(0, froxel), + }; + auto bs = nv->createBindingSet(bsDesc, layout); + if (!bs) + return; + nvrhi::ComputeState cs; + cs.pipeline = pipe; + cs.bindings = {bs}; + cmd->setComputeState(cs); + cmd->dispatch(gx, gy, gz); + }; + + cmd->setTextureState(froxel, nvrhi::TextureSubresourceSet{}, nvrhi::ResourceStates::UnorderedAccess); + dispatchUAV(st.clearPipeline, st.clearLayout); + dispatchUAV(st.fogPipeline, st.fogLayout); + + // Particle density inject (batch centers as smoke blobs) + if (st.particlePipeline && st.particleLayout && st.particlePointsCB && + st.particleParamsCB && data.particleBatches && !data.particleBatches->empty()) + { + ParticleInjectPointGPU points[256]; + u32 count = 0; + for (const auto& batch : *data.particleBatches) + { + if (count >= 256 || batch.particleCount == 0 || batch.isHUDMode) + continue; + auto& p = points[count++]; + p.position = batch.worldMatrix.c; + p.radius = 1.5f + 0.02f * float(std::min(batch.particleCount, 64u)); + p.albedo.set(0.7f, 0.7f, 0.75f); + p.density = 0.08f * float(std::min(batch.particleCount, 32u)); + } + if (count > 0) + { + cmd->writeBuffer(st.particlePointsCB, points, sizeof(ParticleInjectPointGPU) * count); + ParticleInjectParamsCB pp{}; + pp.particleCount = count; + cmd->writeBuffer(st.particleParamsCB, &pp, sizeof(pp)); + + nvrhi::BindingSetDesc bsDesc; + bsDesc.bindings = { + nvrhi::BindingSetItem::ConstantBuffer(5, st.paramsCB), + nvrhi::BindingSetItem::ConstantBuffer(6, st.particleParamsCB), + nvrhi::BindingSetItem::StructuredBuffer_SRV(0, st.particlePointsCB), + nvrhi::BindingSetItem::Texture_UAV(0, froxel), + }; + auto bs = nv->createBindingSet(bsDesc, st.particleLayout); + if (bs) + { + nvrhi::ComputeState cs; + cs.pipeline = st.particlePipeline; + cs.bindings = {bs}; + cmd->setComputeState(cs); + cmd->dispatch(gx, gy, gz); + } + } + } + + dispatchUAV(st.lightPipeline, st.lightLayout); + + // Temporal accumulate from previous frame + nvrhi::ITexture* prevFroxel = vol.GetPrevFroxelVolume(); + if (st.temporalPipeline && st.temporalLayout && st.temporalCB && prevFroxel && + vol.HasFroxelHistory() && st.hasPrevVP) + { + TemporalParamsCB tcb{}; + tcb.prevVP = st.prevVP; + tcb.temporalAlpha = 0.88f; + cmd->writeBuffer(st.temporalCB, &tcb, sizeof(tcb)); + + cmd->setTextureState(prevFroxel, nvrhi::TextureSubresourceSet{}, + nvrhi::ResourceStates::ShaderResource); + + nvrhi::BindingSetDesc bsDesc; + bsDesc.bindings = { + nvrhi::BindingSetItem::ConstantBuffer(5, st.paramsCB), + nvrhi::BindingSetItem::ConstantBuffer(6, st.temporalCB), + nvrhi::BindingSetItem::Texture_SRV(0, prevFroxel), + nvrhi::BindingSetItem::Texture_UAV(0, froxel), + nvrhi::BindingSetItem::Sampler(0, st.linearSampler), + }; + auto bs = nv->createBindingSet(bsDesc, st.temporalLayout); + if (bs) + { + nvrhi::ComputeState cs; + cs.pipeline = st.temporalPipeline; + cs.bindings = {bs}; + cmd->setComputeState(cs); + cmd->dispatch(gx, gy, gz); + } + } + + // Copy current → prev for next frame + if (prevFroxel) + { + cmd->setTextureState(froxel, nvrhi::TextureSubresourceSet{}, + nvrhi::ResourceStates::CopySource); + cmd->setTextureState(prevFroxel, nvrhi::TextureSubresourceSet{}, + nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(prevFroxel, nvrhi::TextureSlice{}, froxel, nvrhi::TextureSlice{}); + vol.SwapFroxelHistory(); + } + + st.prevVP = Device.mFullTransform; + st.hasPrevVP = true; + + cmd->setTextureState(froxel, nvrhi::TextureSubresourceSet{}, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(scene, nvrhi::TextureSubresourceSet{}, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(depthTex, nvrhi::TextureSubresourceSet{}, nvrhi::ResourceStates::ShaderResource); + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(output); + auto fb = GetPassResourceCache().GetOrCreateFramebuffer("VolumetricMarch", fbDesc, nv); + if (!fb || !st.marchPipeline || !st.marchLayout) + return; + + nvrhi::BindingSetDesc marchBs; + marchBs.bindings = { + nvrhi::BindingSetItem::ConstantBuffer(5, st.paramsCB), + nvrhi::BindingSetItem::Texture_SRV(0, scene), + nvrhi::BindingSetItem::Texture_SRV(1, depthTex), + nvrhi::BindingSetItem::Texture_SRV(2, froxel), + nvrhi::BindingSetItem::Sampler(0, st.linearSampler), + }; + auto marchSet = nv->createBindingSet(marchBs, st.marchLayout); + if (!marchSet) + return; + + nvrhi::GraphicsState gs; + gs.pipeline = st.marchPipeline; + gs.framebuffer = fb; + gs.bindings = {marchSet}; + gs.viewport.addViewportAndScissorRect(nvrhi::Viewport( + static_cast(data.width), static_cast(data.height))); + cmd->setGraphicsState(gs); + + nvrhi::DrawArguments args; + args.vertexCount = 3; + cmd->draw(args); + }); + + return passData.sceneOutput; +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.h new file mode 100644 index 00000000000..ab3fdbee3f4 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.h @@ -0,0 +1,125 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include + +namespace xray::render::framegraph +{ +class FrameGraph; +} + +namespace xray::render::fg +{ +class RenderDevice; +class VolumetricRenderer; +} + +namespace xray::render::fg::passes +{ + +struct ParticleBatch; // ParticlePassSetup.h + +// Must match volumetric/shared_froxel.h FroxelParams (b5) +struct alignas(16) FroxelParamsCB +{ + u32 dims[3]; + float nearPlane; + float farPlane; + float invLogFarNear; + float pad0[2]; + + Fmatrix invVP; + Fvector cameraPos; + float pad1; + + float fogDensity; + float fogHeightFalloff; + float fogBaseHeight; + float pad2; + Fvector fogAlbedo; + float pad3; + Fvector sunDir; + float pad4; + Fvector sunColor; + float sunIntensity; + Fvector4 classicFogParams; +}; +static_assert(sizeof(FroxelParamsCB) % 16 == 0, "FroxelParamsCB must be 16-byte aligned"); + +struct alignas(16) TemporalParamsCB +{ + Fmatrix prevVP; + float temporalAlpha; + float pad[3]; +}; +static_assert(sizeof(TemporalParamsCB) % 16 == 0); + +struct alignas(16) ParticleInjectPointGPU +{ + Fvector position; + float radius; + Fvector albedo; + float density; +}; +static_assert(sizeof(ParticleInjectPointGPU) == 32); + +struct alignas(16) ParticleInjectParamsCB +{ + u32 particleCount; + u32 pad[3]; +}; + +struct VolumetricPassState +{ + nvrhi::ComputePipelineHandle clearPipeline; + nvrhi::ComputePipelineHandle fogPipeline; + nvrhi::ComputePipelineHandle lightPipeline; + nvrhi::ComputePipelineHandle particlePipeline; + nvrhi::ComputePipelineHandle temporalPipeline; + nvrhi::GraphicsPipelineHandle marchPipeline; + nvrhi::BindingLayoutHandle clearLayout; + nvrhi::BindingLayoutHandle fogLayout; + nvrhi::BindingLayoutHandle lightLayout; + nvrhi::BindingLayoutHandle particleLayout; + nvrhi::BindingLayoutHandle temporalLayout; + nvrhi::BindingLayoutHandle marchLayout; + nvrhi::SamplerHandle linearSampler; + nvrhi::BufferHandle paramsCB; + nvrhi::BufferHandle temporalCB; + nvrhi::BufferHandle particleParamsCB; + nvrhi::BufferHandle particlePointsCB; + Fmatrix prevVP; + bool hasPrevVP = false; + bool initialized = false; + bool computeEnabled = false; +}; + +struct VolumetricPassData +{ + framegraph::VirtualResourceHandle sceneInput; + framegraph::VirtualResourceHandle depthInput; + framegraph::VirtualResourceHandle froxelVolume; + framegraph::VirtualResourceHandle sceneOutput; + VolumetricRenderer* volumetric = nullptr; + VolumetricPassState* passState = nullptr; + const xr_vector* particleBatches = nullptr; + u32 width = 0; + u32 height = 0; +}; + +framegraph::VirtualResourceHandle setupVolumetricPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + VolumetricRenderer* volumetric, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + u32 width, + u32 height, + VolumetricPassState& state, + const xr_vector* particleBatches = nullptr); + +void InitializeVolumetricPass(nvrhi::IDevice* device, VolumetricPassState& state); +void ShutdownVolumetricPass(VolumetricPassState& state); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.cpp new file mode 100644 index 00000000000..7ae6a137ed3 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.cpp @@ -0,0 +1,497 @@ +#include "stdafx.h" +#include "WetSurfacesPassSetup.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/ResourceManager/FGResourceManager.h" +#include "Layers/xrRender/ResourceManager/TextureManager.h" +#include "Layers/xrRender/xrRender_console.h" +#include "xrEngine/Environment.h" +#include "xrEngine/IGame_Persistent.h" + +namespace xray::render::fg::passes { + +using namespace framegraph; + +// Phase 2 CB — matrices for wet SSR + rain SM projection +struct alignas(16) WetConstantsGPU +{ + Fvector4 RainDensity; // x = y = rain density + Fvector4 EyePos; + Fvector4 SunColor; + Fvector4 Timers; // x = time + Fmatrix m_VP; + Fmatrix m_RainSampleVP; +}; +static_assert(sizeof(WetConstantsGPU) == 192, "WetConstantsGPU size mismatch"); + +struct WetPassData +{ + VirtualResourceHandle colorIn; + VirtualResourceHandle normal; + VirtualResourceHandle worldPos; + VirtualResourceHandle patched; + VirtualResourceHandle colorOut; + VirtualResourceHandle normalOut; + VirtualResourceHandle rainSM; + VirtualResourceHandle sceneReflection; + u32 width = 0; + u32 height = 0; + WetSurfacesPassState* passState = nullptr; + WetSurfacesExtras extras; + bool hasRainSM = false; + bool hasSceneReflection = false; +}; + +static void BlitColor(nvrhi::ICommandList* cmdList, nvrhi::ITexture* dst, nvrhi::ITexture* src) +{ + if (!cmdList || !dst || !src) + return; + cmdList->setTextureState(src, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmdList->setTextureState(dst, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmdList->copyTexture(dst, nvrhi::TextureSlice(), src, nvrhi::TextureSlice()); + cmdList->setTextureState(dst, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); +} + +static WetConstantsGPU MakeWetConstants(const Fmatrix& rainSampleVP) +{ + WetConstantsGPU wet{}; + float rainDensity = g_pGamePersistent + ? g_pGamePersistent->Environment().CurrentEnv.rain_density + : 0.f; + wet.RainDensity.set(rainDensity, rainDensity, 0.f, 0.f); + wet.EyePos.set(Device.vCameraPosition.x, Device.vCameraPosition.y, Device.vCameraPosition.z, 0.f); + if (g_pGamePersistent) + { + const auto& env = g_pGamePersistent->Environment().CurrentEnv; + wet.SunColor.set(env.sun_color.x, env.sun_color.y, env.sun_color.z, 0.f); + } + const float t = Device.fTimeGlobal; + wet.Timers.set(t, t * 10.f, t / 10.f, _sin(t)); + wet.m_VP = Device.mFullTransform; + wet.m_RainSampleVP = rainSampleVP; + return wet; +} + +constexpr u32 kWetPipeVersion = 7; + +void InitializeWetSurfacesPass(nvrhi::IDevice* device, WetSurfacesPassState& state) +{ + if (!device) + return; + if (state.initialized && state.pipeVersion == kWetPipeVersion) + return; + state.initialized = false; + + auto* loader = GEnv.Render ? GEnv.Render->GetShaderLoader() : nullptr; + if (!loader) + { + state.initialized = true; + return; + } + + auto vs = loader->LoadVertexShader("fullscreen"); + auto patchPs = loader->LoadPixelShader("wet/rain_patch_normal"); + auto applyPs = loader->LoadPixelShader("wet/rain_apply"); + auto writePs = loader->LoadPixelShader("wet/rain_write_normal"); + + // Apply is mandatory; patch/write are optional (Slang often crashes on patch). + if (!vs.handle || !vs.reflection || !applyPs.handle || !applyPs.reflection) + { + Msg("! [WetSurfaces] Failed to load apply shader — wet disabled"); + state.initialized = true; + return; + } + + auto& cache = GetPassResourceCache(); + state.layout = cache.GetOrCreateBindingLayoutFromReflection( + "WetApply_v7", *vs.reflection, *applyPs.reflection, device); + + if (patchPs.handle && patchPs.reflection) + { + state.patchLayout = cache.GetOrCreateBindingLayoutFromReflection( + "WetPatch_v7", *vs.reflection, *patchPs.reflection, device); + } + else + { + Msg("! [WetSurfaces] patch shader missing — apply-only mode"); + } + + if (writePs.handle && writePs.reflection) + { + state.writeNormalLayout = cache.GetOrCreateBindingLayoutFromReflection( + "WetWriteNormal_v7", *vs.reflection, *writePs.reflection, device); + } + + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); + + auto makePipe = [&](auto& ps, nvrhi::BindingLayoutHandle layout, + nvrhi::GraphicsPipelineHandle& pipe, const char* name) { + if (!layout || !ps.handle) + return; + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs.handle); + desc.setPixelShader(ps.handle); + desc.addBindingLayout(layout); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + pipe = cache.GetOrCreatePipeline(name, desc, fbInfo, device); + }; + + makePipe(applyPs, state.layout, state.pipeline, "WetApply_v7"); + makePipe(patchPs, state.patchLayout, state.patchPipeline, "WetPatch_v7"); + makePipe(writePs, state.writeNormalLayout, state.writeNormalPipeline, "WetWriteNormal_v7"); + + state.initialized = true; + state.pipeVersion = kWetPipeVersion; + if (state.pipeline && state.layout) + { + Msg("* [WetSurfaces] Rebuild v9: apply=%d patch=%d write=%d (SSR-stable normals, non-transient RT)", + !!state.pipeline, !!state.patchPipeline, !!state.writeNormalPipeline); + } + else + Msg("! [WetSurfaces] Pipeline create failed — wet disabled"); +} + +DefaultOutputLayout setupWetSurfacesPass( + FrameGraph& fg, + fg::RenderDevice* device, + const DefaultOutputLayout& inputs, + u32 width, + u32 height, + WetSurfacesPassState& passState, + const WetSurfacesExtras& extras) +{ + DefaultOutputLayout outputs = inputs; + static bool s_loggedNoFlag = false; + static bool s_loggedMissingInputs = false; + static bool s_loggedNoPipelines = false; + + if (!ps_r2_ls_flags.test(R3FLAG_DYN_WET_SURF)) + { + if (!s_loggedNoFlag) + { + Msg("! [WetSurfaces] Disabled by r3_dynamic_wet_surfaces flag"); + s_loggedNoFlag = true; + } + return outputs; + } + + const float rainDensity = g_pGamePersistent + ? g_pGamePersistent->Environment().CurrentEnv.rain_density + : 0.f; + // Rain-only (SSFX/CoP behavior) — no wet rewrite in dry weather + if (rainDensity < 0.001f) + return outputs; + + if (!inputs.albedo.is_valid() || !inputs.normal.is_valid() || + !inputs.worldPos.is_valid()) + { + if (!s_loggedMissingInputs) + { + Msg("! [WetSurfaces] Missing inputs: albedo=%d normal=%d worldPos=%d", + inputs.albedo.is_valid(), inputs.normal.is_valid(), + inputs.worldPos.is_valid()); + s_loggedMissingInputs = true; + } + return outputs; + } + + if (device && device->GetNVRHIDevice()) + InitializeWetSurfacesPass(device->GetNVRHIDevice(), passState); + + if (!passState.pipeline || !passState.layout) + { + if (!s_loggedNoPipelines) + { + Msg("! [WetSurfaces] Missing apply pipeline/layout (patch=%d write=%d)", + !!passState.patchPipeline, !!passState.writeNormalPipeline); + s_loggedNoPipelines = true; + } + return outputs; + } + + ResourceDesc texDesc; + texDesc.type = ResourceDesc::Type::Texture2D; + texDesc.width = width; + texDesc.height = height; + texDesc.format = nvrhi::Format::RGBA16_FLOAT; + texDesc.isRenderTarget = true; + // Non-transient: aliased wet normal/color made fullscreen SSR hard-switch by + // camera/map position (same class of bug as rt_SceneReflection). + texDesc.isTransient = false; + + texDesc.debugName = "rt_WetPatched"; + auto patched = fg.CreateTexture("rt_WetPatched", texDesc); + texDesc.debugName = "rt_WetColor"; + auto outColor = fg.CreateTexture("rt_WetColor", texDesc); + + const bool doWriteNormal = passState.writeNormalPipeline && passState.writeNormalLayout + && passState.patchPipeline && passState.patchLayout; + VirtualResourceHandle outNormal{}; + if (doWriteNormal) + { + texDesc.debugName = "rt_WetNormal"; + outNormal = fg.CreateTexture("rt_WetNormal", texDesc); + } + + const bool hasRainSM = extras.rainSMValid && extras.rainSM.is_valid(); + const bool hasSceneReflection = extras.sceneReflectionValid && extras.sceneReflection.is_valid(); + + (void)fg.addCallbackPass( + "WetSurfaces", + [inputs, patched, outColor, outNormal, doWriteNormal, hasRainSM, hasSceneReflection, + width, height, &passState, extras]( + FrameGraph& builder, PassHandle passHandle, WetPassData& data) + { + RenderPassBuilder passBuilder(builder, passHandle); + data.width = width; + data.height = height; + data.passState = &passState; + data.extras = extras; + data.hasRainSM = hasRainSM; + data.hasSceneReflection = hasSceneReflection; + data.colorIn = passBuilder.read(inputs.albedo, ResourceState::ShaderResource); + data.normal = passBuilder.read(inputs.normal, ResourceState::ShaderResource); + data.worldPos = passBuilder.read(inputs.worldPos, ResourceState::ShaderResource); + if (hasRainSM) + data.rainSM = passBuilder.read(extras.rainSM, ResourceState::ShaderResource); + if (hasSceneReflection) + data.sceneReflection = passBuilder.read(extras.sceneReflection, ResourceState::ShaderResource); + data.patched = passBuilder.write(patched, ResourceState::RenderTarget); + data.colorOut = passBuilder.write(outColor, ResourceState::RenderTarget); + if (doWriteNormal && outNormal.is_valid()) + data.normalOut = passBuilder.write(outNormal, ResourceState::RenderTarget); + passBuilder.sideEffects(); + }, + [](const WetPassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) + { + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + nvrhi::IDevice* nvDevice = cmdList->getDevice(); + auto* ps = data.passState; + + auto* colorIn = fgGraph.GetPhysicalTexture(data.colorIn); + auto* colorOut = fgGraph.GetPhysicalTexture(data.colorOut); + BlitColor(cmdList, colorOut, colorIn); + + if (!ps || !ps->pipeline || !ps->layout) + return; + + auto* normalTex = fgGraph.GetPhysicalTexture(data.normal); + auto* worldPosTex = fgGraph.GetPhysicalTexture(data.worldPos); + auto* patchedTex = fgGraph.GetPhysicalTexture(data.patched); + if (!colorIn || !normalTex || !worldPosTex || !patchedTex || !colorOut) + return; + + if (colorIn == colorOut) + { + static bool s_loggedAlias = false; + if (!s_loggedAlias) + { + Msg("! [WetSurfaces] colorIn == colorOut — wet cannot composite"); + s_loggedAlias = true; + } + return; + } + + auto& cache = GetPassResourceCache(); + auto* rd = ctx->GetDevice(); + auto wetCB = cache.GetOrCreateVolatileCB( + "WetSurfaces", "WetConstants_v1n_RainVP", sizeof(WetConstantsGPU), rd); + WetConstantsGPU wet = MakeWetConstants(data.extras.rainSampleVP); + cmdList->writeBuffer(wetCB, &wet, sizeof(wet)); + + static int s_wetExecLog = 0; + if ((s_wetExecLog++ % 120) == 0) + { + Msg("* [WetSurfaces] exec rain=%.3f ssrSrc=%d in=%p out=%p", + wet.RainDensity.x, data.hasSceneReflection, + (void*)colorIn, (void*)colorOut); + } + + auto* vsRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("fullscreen", ".vs"); + auto* applyRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("wet/rain_apply", ".ps"); + if (!vsRefl || !applyRefl) + return; + auto* patchRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("wet/rain_patch_normal", ".ps"); + auto* writeRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("wet/rain_write_normal", ".ps"); + + nvrhi::ITexture* rainSM = data.hasRainSM + ? fgGraph.GetPhysicalTexture(data.rainSM) + : nullptr; + if (!rainSM) + rainSM = data.extras.rainSMTex; + if (!rainSM) + rainSM = cache.GetDummyShadowMap2D(nvDevice); + + auto* texMgr = rd && rd->GetFGResourceManager() + ? rd->GetFGResourceManager()->GetTextureManager() + : nullptr; + auto loadTex = [&](const char* name) -> nvrhi::ITexture* { + if (!texMgr) + return nullptr; + auto h = texMgr->LoadTexture(name); + return texMgr->GetNVRHITexture(h); + }; + nvrhi::ITexture* waterRipple = loadTex("fx" DELIMITER "water_sbumpvolume"); + if (!waterRipple) + waterRipple = loadTex("fx" DELIMITER "rain_splash"); + if (!waterRipple) + waterRipple = loadTex("water" DELIMITER "water_sbumpvolume"); + nvrhi::ITexture* waterFall = loadTex("fx" DELIMITER "water_normal"); + if (!waterFall) + waterFall = loadTex("water" DELIMITER "water_normal"); + nvrhi::ITexture* puddlesPerlin = loadTex("fx" DELIMITER "puddles_perlin"); + if (!waterRipple) + waterRipple = cache.GetDummyContactHistory(nvDevice); + if (!waterFall) + waterFall = waterRipple; + if (!puddlesPerlin) + puddlesPerlin = waterRipple; + + nvrhi::Viewport viewport; + viewport.minX = 0; + viewport.minY = 0; + viewport.maxX = static_cast(data.width); + viewport.maxY = static_cast(data.height); + viewport.minZ = 0.0f; + viewport.maxZ = 1.0f; + + // 1) Patch normals + wetness (optional) + bool havePatched = false; + if (ps->patchPipeline && ps->patchLayout && patchRefl) + { + BindingSetBuilder bsb(*vsRefl, *patchRefl, nvDevice, "WetPatch"); + bsb.ConstantBuffer("WetConstants", wetCB) + .Texture("g_Normal", normalTex) + .Texture("g_WorldPos", worldPosTex) + .Texture("g_RainShadow", rainSM) + .Texture("g_Water", waterRipple) + .Texture("g_WaterFall", waterFall) + .Texture("g_PuddlesPerlin", puddlesPerlin); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->patchLayout, nvDevice); + if (bindingSet) + { + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(patchedTex); + auto framebuffer = cache.GetOrCreateFramebuffer("WetPatch_v1d", fbDesc, nvDevice); + if (framebuffer) + { + nvrhi::GraphicsState state; + state.pipeline = ps->patchPipeline; + state.framebuffer = framebuffer; + state.viewport.addViewportAndScissorRect(viewport); + state.addBindingSet(bindingSet); + cmdList->setGraphicsState(state); + cmdList->draw(nvrhi::DrawArguments().setVertexCount(3)); + cmdList->setTextureState(patchedTex, nvrhi::AllSubresources, + nvrhi::ResourceStates::ShaderResource); + havePatched = true; + } + } + } + + // Apply-only: feed scene normals; wet amount comes from rain in PS + nvrhi::ITexture* patchedForApply = havePatched ? patchedTex : normalTex; + + // 2) Write normals for AO (optional) + if (ps->writeNormalPipeline && ps->writeNormalLayout && writeRefl + && data.normalOut.is_valid() && havePatched) + { + auto* normalOut = fgGraph.GetPhysicalTexture(data.normalOut); + if (normalOut) + { + BindingSetBuilder bsb(*vsRefl, *writeRefl, nvDevice, "WetWriteNormal"); + bsb.Texture("g_Normal", normalTex).Texture("g_Patched", patchedTex); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->writeNormalLayout, nvDevice); + if (bindingSet) + { + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(normalOut); + auto framebuffer = cache.GetOrCreateFramebuffer("WetWriteNormal_v1d", fbDesc, nvDevice); + if (framebuffer) + { + nvrhi::GraphicsState state; + state.pipeline = ps->writeNormalPipeline; + state.framebuffer = framebuffer; + state.viewport.addViewportAndScissorRect(viewport); + state.addBindingSet(bindingSet); + cmdList->setGraphicsState(state); + cmdList->draw(nvrhi::DrawArguments().setVertexCount(3)); + cmdList->setTextureState(normalOut, nvrhi::AllSubresources, + nvrhi::ResourceStates::ShaderResource); + } + } + } + } + + // 3) Apply darken + streaks + wet SSR + { + nvrhi::ITexture* sceneRefl = data.hasSceneReflection + ? fgGraph.GetPhysicalTexture(data.sceneReflection) + : nullptr; + if (!sceneRefl) + sceneRefl = colorIn; // fallback: still safe (no write feedback to colorIn) + + cmdList->setTextureState(colorIn, nvrhi::AllSubresources, + nvrhi::ResourceStates::ShaderResource); + cmdList->setTextureState(patchedForApply, nvrhi::AllSubresources, + nvrhi::ResourceStates::ShaderResource); + cmdList->setTextureState(worldPosTex, nvrhi::AllSubresources, + nvrhi::ResourceStates::ShaderResource); + cmdList->setTextureState(sceneRefl, nvrhi::AllSubresources, + nvrhi::ResourceStates::ShaderResource); + cmdList->setTextureState(colorOut, nvrhi::AllSubresources, + nvrhi::ResourceStates::RenderTarget); + + BindingSetBuilder bsb(*vsRefl, *applyRefl, nvDevice, "WetApply"); + bsb.ConstantBuffer("WetConstants", wetCB) + .Texture("g_Color", colorIn) + .Texture("g_Patched", patchedForApply) + .Texture("g_WorldPos", worldPosTex) + .Texture("g_WaterFall", waterFall) + .Texture("g_SceneReflection", sceneRefl); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->layout, nvDevice); + if (!bindingSet) + { + static bool s_loggedApplyBind = false; + if (!s_loggedApplyBind) + { + Msg("! [WetSurfaces] Apply binding failed — passthrough blit kept"); + s_loggedApplyBind = true; + } + return; + } + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(colorOut); + auto framebuffer = cache.GetOrCreateFramebuffer("WetApply_v7", fbDesc, nvDevice); + if (!framebuffer) + return; + + nvrhi::GraphicsState state; + state.pipeline = ps->pipeline; + state.framebuffer = framebuffer; + state.viewport.addViewportAndScissorRect(viewport); + state.addBindingSet(bindingSet); + cmdList->setGraphicsState(state); + cmdList->draw(nvrhi::DrawArguments().setVertexCount(3)); + cmdList->setTextureState(colorOut, nvrhi::AllSubresources, + nvrhi::ResourceStates::ShaderResource); + } + }); + + outputs.albedo = outColor; + if (doWriteNormal && outNormal.is_valid()) + outputs.normal = outNormal; + return outputs; +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.h new file mode 100644 index 00000000000..a92e63ae009 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.h @@ -0,0 +1,52 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include "Layers/xrRender/FrameGraph/IPass.h" +#include + +namespace xray::render::framegraph { + class FrameGraph; +} + +namespace xray::render::fg { + class RenderDevice; +} + +namespace xray::render::fg::passes { + +struct WetSurfacesPassState { + nvrhi::GraphicsPipelineHandle pipeline; // apply + nvrhi::BindingLayoutHandle layout; + nvrhi::GraphicsPipelineHandle patchPipeline; + nvrhi::BindingLayoutHandle patchLayout; + nvrhi::GraphicsPipelineHandle writeNormalPipeline; + nvrhi::BindingLayoutHandle writeNormalLayout; + bool initialized = false; + u32 pipeVersion = 0; +}; + +void InitializeWetSurfacesPass(nvrhi::IDevice* device, WetSurfacesPassState& state); + +// Phase 1 wet + phase 2 SSR source (rt_SceneReflection, non-aliased). +struct WetSurfacesExtras +{ + framegraph::VirtualResourceHandle rainSM; + nvrhi::ITexture* rainSMTex = nullptr; + Fmatrix rainSampleVP; + bool rainSMValid = false; + + framegraph::VirtualResourceHandle sceneReflection; + bool sceneReflectionValid = false; +}; + +framegraph::DefaultOutputLayout setupWetSurfacesPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + const framegraph::DefaultOutputLayout& inputs, + u32 width, + u32 height, + WetSurfacesPassState& state, + const WetSurfacesExtras& extras = {}); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/GPUCullingManager.cpp b/src/Layers/xrRender/GPUCullingManager.cpp index 99c0a2be22b..97e89726fc9 100644 --- a/src/Layers/xrRender/GPUCullingManager.cpp +++ b/src/Layers/xrRender/GPUCullingManager.cpp @@ -53,7 +53,8 @@ struct CullParamsCB { u32 hizHeight; // Hi-Z pyramid height u32 hizMipLevels; // Hi-Z mip levels u32 frameId; // Frame stamp for visibility - u32 padding[3]; + u32 hizEnable; // 0=skip Hi-Z occlusion test + u32 padding[2]; }; // ═══════════════════════════════════════════════════════ @@ -155,6 +156,29 @@ void GPUCullingManager::Initialize(fg::RenderDevice* device) CreateDebugResources(device); CreateParticleResources(device); + { + nvrhi::TextureDesc td; + td.width = 1; + td.height = 1; + td.format = nvrhi::Format::R32_FLOAT; + td.mipLevels = 1; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "GPUCull_DummyHiZ"; + m_dummyHizTexture = nvDevice->createTexture(td); + // Far depth (1.0) so HiZTestSphere always passes — all-visible path for + // DetailCull / frame-0 / forceDisableHiz when no real pyramid is bound. + if (m_dummyHizTexture) + { + nvrhi::CommandListHandle cmd = nvDevice->createCommandList(); + cmd->open(); + const float farD = 1.0f; + cmd->writeTexture(m_dummyHizTexture, 0, 0, &farD, sizeof(farD)); + cmd->close(); + nvDevice->executeCommandList(cmd); + } + } + m_initialized = true; Msg("* [GPUCulling] Initialized (max objects: %d, max particles: %d, compact: %s)", m_maxObjects, m_maxParticles, m_compactEnabled ? "yes" : "no"); @@ -456,6 +480,86 @@ void GPUCullingManager::CreateBuffers(fg::RenderDevice* device) Msg("* [GPUCulling] Transparent buffers created (max: %u objects)", maxTransparent); } + // ─────────────────────────────────────────────────────── + // TESSELLATION CULLING SET (opaque bump# / PN+HM) + // ─────────────────────────────────────────────────────── + { + const u32 maxTess = 4096; + m_tessSet.maxObjects = maxTess; + + nvrhi::BufferDesc desc; + desc.debugName = "GPUCull_Tess_Objects"; + desc.byteSize = maxTess * sizeof(GPUObjectData); + desc.structStride = sizeof(GPUObjectData); + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + m_tessSet.objectBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_VisibleIndices"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.visibleIndexBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_VisibleCount"; + desc.byteSize = sizeof(u32); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.visibleCountBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_DrawArgs"; + desc.byteSize = maxTess * sizeof(IndirectDrawArgs); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.drawArgsBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_Visibility"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.visibilityBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_MaterialIDs"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + m_tessSet.materialIDBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_InstanceData"; + desc.byteSize = maxTess * sizeof(GPUInstanceData); + desc.structStride = sizeof(GPUInstanceData); + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + m_tessSet.instanceBuffer = nvDevice->createBuffer(desc); + + // Identity batch map for direct drawIndexed (DRAWINDEX → batch index) + desc = {}; + desc.debugName = "GPUCull_Tess_BatchIndices"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + m_tessBatchIndicesBuffer = nvDevice->createBuffer(desc); + + Msg("* [GPUCulling] Tessellation buffers created (max: %u objects)", maxTess); + } + // ─────────────────────────────────────────────────────── // SKINNED MESH CULLING BUFFERS // ─────────────────────────────────────────────────────── @@ -1215,6 +1319,89 @@ void GPUCullingManager::CreateCompactionResources(fg::RenderDevice* device) m_transparentSet.compactGroupOffsetsBuffer = nvDevice->createBuffer(desc); } + // Tessellation compaction buffers + { + const u32 maxTess = m_tessSet.maxObjects; + u32 maxTessGroups = (maxTess + COMPACT_THREAD_GROUP_SIZE - 1) / COMPACT_THREAD_GROUP_SIZE; + R_ASSERT2(maxTessGroups > 0 && maxTessGroups <= COMPACT_THREAD_GROUP_SIZE, + "Tess compaction group count invalid"); + + nvrhi::BufferDesc desc; + desc.debugName = "GPUCull_Tess_CompactDrawArgs"; + desc.byteSize = maxTess * sizeof(IndirectDrawArgs); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactDrawArgsBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactBatchIndices"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactBatchIndicesBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactMaterialIDs"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactMaterialIDBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactCount"; + desc.byteSize = sizeof(u32); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactCountBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactDispatchArgs"; + desc.byteSize = 3 * sizeof(u32); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactDispatchArgsBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactLocalPrefix"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactLocalPrefixBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactGroupCounts"; + desc.byteSize = maxTessGroups * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactGroupCountsBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactGroupOffsets"; + desc.byteSize = maxTessGroups * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactGroupOffsetsBuffer = nvDevice->createBuffer(desc); + } + { fg::RenderDevice::BufferDesc desc; desc.debugName = "GPUCull_CompactParams"; @@ -1515,6 +1702,9 @@ void GPUCullingManager::Shutdown() m_staticSet.compactGroupCountsBuffer = nullptr; m_staticSet.compactGroupOffsetsBuffer = nullptr; m_staticSet.instanceBuffer = nullptr; + m_staticSet.shadowIndicesBuffer = nullptr; + m_staticSet.shadowCountBuffer = nullptr; + m_staticSet.shadowBuffersCapacity = 0; m_staticSet.objectCount = 0; m_staticSet.maxObjects = 0; m_staticSet.drawArgsUploaded = false; @@ -1535,12 +1725,21 @@ void GPUCullingManager::Shutdown() m_dynamicSet.compactGroupCountsBuffer = nullptr; m_dynamicSet.compactGroupOffsetsBuffer = nullptr; m_dynamicSet.instanceBuffer = nullptr; + m_dynamicSet.shadowIndicesBuffer = nullptr; + m_dynamicSet.shadowCountBuffer = nullptr; + m_dynamicSet.shadowBuffersCapacity = 0; m_dynamicSet.objectCount = 0; m_dynamicSet.maxObjects = 0; m_dynamicSet.drawArgsUploaded = false; m_dynamicSet.objectsUploaded = false; m_transparentSet = {}; + m_tessSet = {}; + m_tessBatchIndicesBuffer = nullptr; + m_tessObjectData.clear(); + m_tessDrawArgsData.clear(); + m_tessMaterialIDData.clear(); + m_tessInstanceData.clear(); m_cullParamsCB = fg::BufferHandle(); m_cullPipeline = nullptr; @@ -1773,10 +1972,22 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr if (totalBatches == 0) { m_staticSet.objectCount = 0; m_dynamicSet.objectCount = 0; + m_tessSet.objectCount = 0; m_objectCount = 0; return; } + // Tess routing depends on MaterialBuffer.tessMethod, which fills in as textures load. + // Rebuild static lists every frame while tessellation is enabled. + // Apple/MoltenVK: never route into PatchList (IOGPU kernel panic risk). +#if defined(XR_PLATFORM_APPLE) + const bool tessFlagOn = false; +#else + const bool tessFlagOn = ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION); +#endif + if (tessFlagOn) + m_staticDataCached = false; + R_ASSERT2(m_staticSet.objectBuffer && m_staticSet.drawArgsBuffer && m_staticSet.visibilityBuffer, "Static GPU culling buffers not initialized"); R_ASSERT2(m_dynamicSet.objectBuffer && m_dynamicSet.drawArgsBuffer && m_dynamicSet.visibilityBuffer, @@ -1823,11 +2034,21 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr m_transparentMaterialIDData.clear(); m_transparentInstanceData.clear(); + // Tessellation-specific arrays (rebuilt every frame like transparent) + m_tessObjectData.clear(); + m_tessDrawArgsData.clear(); + m_tessMaterialIDData.clear(); + m_tessInstanceData.clear(); + auto appendBatch = [&](const GeometryBatch& batch, xr_vector& objectData, xr_vector& drawArgsData, xr_vector& materialIDData, - xr_vector& instanceData) { + xr_vector& instanceData) -> bool { + const u32 matCount = bindless::MaterialBuffer::Instance().GetMaterialCount(); + if (batch.bindlessMaterialID == UINT32_MAX || batch.bindlessMaterialID >= matCount) + return false; + // ───────────────────────────────────────────────────── // BUILD OBJECT DATA (for culling) // ───────────────────────────────────────────────────── @@ -1862,7 +2083,12 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr args.startIndexLocation = batch.startIndex; args.baseVertexLocation = batch.baseVertex; } - args.startInstanceLocation = 0; + // startInstanceLocation carries the per-draw DRAWINDEX for the view-independent + // "cast all" shadow path, which draws these raw args directly (no compaction). + // The forward path ignores this: batch_compact.cs overwrites it with the compacted + // output index. With it left at 0, every cast-all draw read instance 0, collapsing + // all static/transparent shadow casters (trees included) onto the first object. + args.startInstanceLocation = static_cast(drawArgsData.size()); drawArgsData.push_back(args); // ───────────────────────────────────────────────────── @@ -1880,10 +2106,81 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr inst.pad0 = 0.0f; inst.pad1 = 0.0f; instanceData.push_back(inst); + return true; }; + // [ShadowDbg] one-shot: classify tree/skinned batches to find why they never cast. + { + static bool s_treeDbgDone = false; + if (!s_treeDbgDone && totalBatches > 0) { + u32 treeTotal = 0, treeStatic = 0, treeTrans = 0, treeTess = 0, treeSkinnedOrOther = 0; + u32 treeAllocValid = 0, treeAllocInvalid = 0, skinnedTotal = 0; + const bool tessOn = tessFlagOn; + for (u32 i = 0; i < totalBatches; i++) { + const auto& b = batches[i]; + const u32 vt = b.visual ? b.visual->getType() : 0; + if (b.isSkinned) { skinnedTotal++; continue; } + if (vt != MT_TREE_ST && vt != MT_TREE_PM) + continue; + treeTotal++; + if (b.megaBufferAlloc.valid) treeAllocValid++; else treeAllocInvalid++; + bool routedTess = false; + if (tessOn) { + const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(b.bindlessMaterialID); + routedTess = mat && mat->tessMethod != 0 && b.indexCount > 0 && b.indexCount <= 8192; + } + if (b.IsStrictB2F()) treeTrans++; + else if (routedTess) treeTess++; + else if (b.isStatic) treeStatic++; + else treeSkinnedOrOther++; + + if (treeTotal <= 5) { + const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(b.bindlessMaterialID); + Msg("* [ShadowDbg] tree#%u type=%u matID=%u flags=0x%X alphaRef=%.3f diffIdx=%u alphaTest=%d strictB2F=%d shader='%s' tex='%s'", + treeTotal, vt, b.bindlessMaterialID, + mat ? mat->flags : 0u, mat ? mat->alphaRef : -1.f, + mat ? mat->diffuseIndex : 0xFFFFFFFFu, + b.IsAlphaTested() ? 1 : 0, b.IsStrictB2F() ? 1 : 0, + (b.visual && b.visual->shaderName.size()) ? b.visual->shaderName.c_str() : "", + (b.visual && b.visual->textureName.size()) ? b.visual->textureName.c_str() : ""); + } + } + Msg("* [ShadowDbg] batches=%u trees=%u (static=%u trans=%u tess=%u dyn=%u) allocValid=%u allocInvalid=%u | skinned=%u", + totalBatches, treeTotal, treeStatic, treeTrans, treeTess, treeSkinnedOrOther, + treeAllocValid, treeAllocInvalid, skinnedTotal); + + u32 tessRouted = 0, tessSkippedBig = 0, tessMats = 0, skinnedTessMats = 0; + for (u32 i = 0; i < totalBatches; i++) { + const auto& b = batches[i]; + const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(b.bindlessMaterialID); + if (!mat || mat->tessMethod == 0) + continue; + if (b.isSkinned) { + skinnedTessMats++; + continue; + } + const u32 vt = b.visual ? b.visual->getType() : 0; + if (vt == MT_TREE_ST || vt == MT_TREE_PM) + continue; + tessMats++; + if (!tessOn) + continue; + if (b.indexCount > 8192) + tessSkippedBig++; + else if (b.indexCount > 0) + tessRouted++; + } + Msg("* [TessDbg] tessOn=%d staticRouted=%u skippedBig=%u staticMats=%u skinnedMatsWithTess=%u", + tessOn ? 1 : 0, tessRouted, tessSkippedBig, tessMats, skinnedTessMats); + if (treeTotal > 0 || skinnedTotal > 0) + s_treeDbgDone = true; + } + } + { ZoneScopedN("Upload::Rebuild"); + u32 badMatSkipped = 0; + const u32 terrainMatCount = bindless::TerrainMaterialBuffer::Instance().GetMaterialCount(); for (u32 i = 0; i < totalBatches; i++) { const auto& batch = batches[i]; @@ -1894,6 +2191,13 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr // Route terrain batches to separate arrays for terrain shader rendering if (batch.isTerrain) { + // Only reject completely unregistered IDs. Do not compare against + // GetMaterialCount() — pending terrain slots are valid after Register. + if (batch.terrainMaterialID == UINT32_MAX) + { + badMatSkipped++; + continue; + } // ───────────────────────────────────────────────────── // TERRAIN BATCH - Goes to terrain arrays // ───────────────────────────────────────────────────── @@ -1937,19 +2241,47 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr } if (batch.IsStrictB2F()) { - appendBatch(batch, m_transparentObjectData, m_transparentDrawArgsData, m_transparentMaterialIDData, m_transparentInstanceData); + if (!appendBatch(batch, m_transparentObjectData, m_transparentDrawArgsData, m_transparentMaterialIDData, m_transparentInstanceData)) + badMatSkipped++; continue; } + // Route model tess materials to a dedicated PatchList draw set. + // Skip huge meshes: MoltenVK tess expands patches and can exceed its + // ~131k temporary-vertex cap (view-dependent freeze until camera moves). + constexpr u32 kMaxTessIndexCount = 8192; + if (tessFlagOn) + { + const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(batch.bindlessMaterialID); + if (mat && mat->tessMethod != 0 && batch.indexCount > 0 && + batch.indexCount <= kMaxTessIndexCount) + { + if (!appendBatch(batch, m_tessObjectData, m_tessDrawArgsData, m_tessMaterialIDData, m_tessInstanceData)) + badMatSkipped++; + continue; + } + } + if (batch.isStatic) { if (!m_staticDataCached) { - appendBatch(batch, m_staticObjectData, m_staticDrawArgsData, m_staticMaterialIDData, m_staticInstanceData); - m_staticBatchVertexCounts.push_back(batch.megaBufferAlloc.valid ? batch.megaBufferAlloc.vertexCount : 0); + if (appendBatch(batch, m_staticObjectData, m_staticDrawArgsData, m_staticMaterialIDData, m_staticInstanceData)) + m_staticBatchVertexCounts.push_back(batch.megaBufferAlloc.valid ? batch.megaBufferAlloc.vertexCount : 0); + else + badMatSkipped++; } } else { - appendBatch(batch, m_dynamicObjectData, m_dynamicDrawArgsData, m_dynamicMaterialIDData, m_dynamicInstanceData); + if (!appendBatch(batch, m_dynamicObjectData, m_dynamicDrawArgsData, m_dynamicMaterialIDData, m_dynamicInstanceData)) + badMatSkipped++; } } + static bool s_badMatLogged = false; + if (!s_badMatLogged && totalBatches > 0) + { + s_badMatLogged = true; + Msg("* [GPUCull] badMat skipped=%u / batches=%u (matCount=%u terrainMatCount=%u)", + badMatSkipped, totalBatches, + bindless::MaterialBuffer::Instance().GetMaterialCount(), terrainMatCount); + } } // Set object counts with total cap @@ -2064,6 +2396,15 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr m_dynamicSet.objectCount * sizeof(GPUInstanceData)); } + // View-independent shadow-caster buffers (identity drawID->batch map + count). + // Lets the shadow pass draw every batch regardless of camera-frustum culling, so + // casters that leave the camera view still cast into the CSM cascades. + { + nvrhi::IDevice* shadowNvDevice = m_device->GetNVRHIDevice(); + EnsureShadowCasterBuffers(cmdList, shadowNvDevice, m_staticSet, "Static"); + EnsureShadowCasterBuffers(cmdList, shadowNvDevice, m_dynamicSet, "Dynamic"); + } + // ───────────────────────────────────────────────────── // TERRAIN BUFFER UPLOADS // ───────────────────────────────────────────────────── @@ -2171,6 +2512,54 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr m_transparentInstanceData.data(), m_transparentSet.objectCount * sizeof(GPUInstanceData)); cmdList->setBufferState(m_transparentSet.instanceBuffer, nvrhi::ResourceStates::ShaderResource); + + // View-independent foliage shadow casting: trees/bushes are alpha-blended and + // live here, so the shadow pass draws this set (foliage-only) into the CSM. + EnsureShadowCasterBuffers(cmdList, m_device->GetNVRHIDevice(), m_transparentSet, "Transparent"); + } + + // ───────────────────────────────────────────────────── + // TESSELLATION GPU UPLOAD (per-frame) + // ───────────────────────────────────────────────────── + m_tessSet.objectCount = std::min(static_cast(m_tessObjectData.size()), m_tessSet.maxObjects); + + if (m_tessSet.objectCount > 0 && m_tessSet.objectBuffer && m_tessSet.drawArgsBuffer) { + ZoneScopedN("Upload::TessWrite"); + for (u32 i = 0; i < m_tessSet.objectCount; ++i) + { + m_tessDrawArgsData[i].instanceCount = 1; + m_tessDrawArgsData[i].startInstanceLocation = i; + } + + cmdList->writeBuffer(m_tessSet.objectBuffer, + m_tessObjectData.data(), + m_tessSet.objectCount * sizeof(GPUObjectData)); + cmdList->setBufferState(m_tessSet.objectBuffer, nvrhi::ResourceStates::ShaderResource); + + cmdList->writeBuffer(m_tessSet.drawArgsBuffer, + m_tessDrawArgsData.data(), + m_tessSet.objectCount * sizeof(IndirectDrawArgs)); + cmdList->setBufferState(m_tessSet.drawArgsBuffer, nvrhi::ResourceStates::ShaderResource); + + cmdList->writeBuffer(m_tessSet.materialIDBuffer, + m_tessMaterialIDData.data(), + m_tessSet.objectCount * sizeof(u32)); + cmdList->setBufferState(m_tessSet.materialIDBuffer, nvrhi::ResourceStates::ShaderResource); + + cmdList->writeBuffer(m_tessSet.instanceBuffer, + m_tessInstanceData.data(), + m_tessSet.objectCount * sizeof(GPUInstanceData)); + cmdList->setBufferState(m_tessSet.instanceBuffer, nvrhi::ResourceStates::ShaderResource); + + if (m_tessBatchIndicesBuffer) + { + xr_vector identity(m_tessSet.objectCount); + for (u32 i = 0; i < m_tessSet.objectCount; ++i) + identity[i] = i; + cmdList->writeBuffer(m_tessBatchIndicesBuffer, identity.data(), + m_tessSet.objectCount * sizeof(u32)); + cmdList->setBufferState(m_tessBatchIndicesBuffer, nvrhi::ResourceStates::ShaderResource); + } } } @@ -2276,7 +2665,8 @@ void GPUCullingManager::UploadSkinnedObjects(fg::RenderContext* ctx, const Geome GPUObjectData obj; obj.position = batch.worldBoundsCenter; - obj.radius = batch.worldBoundsRadius; + // Animated poses exceed rest-pose spheres → inflate to avoid pop-in/out + obj.radius = batch.worldBoundsRadius * 1.75f; obj.flags = 0; // Skinned meshes use their own alpha handling obj.pad0 = 0.0f; obj.pad1 = 0.0f; @@ -2504,6 +2894,176 @@ void GPUCullingManager::ExtractFrustumPlanes(Fmatrix& M, Fvector4* outPlanes) } } +void GPUCullingManager::EnsureShadowCasterBuffers(nvrhi::ICommandList* cmdList, + nvrhi::IDevice* nvDevice, CullSetBuffers& set, const char* name) +{ + if (!cmdList || !nvDevice || set.objectCount == 0) + return; + + // (Re)create the identity map when it is missing or too small. Identity content is a + // prefix, so an oversized buffer already contains a valid [0..objectCount-1] range. + if (!set.shadowIndicesBuffer || set.shadowBuffersCapacity < set.objectCount) + { + const u32 capacity = set.objectCount; + + char nameBuf[128]; + nvrhi::BufferDesc idxDesc; + snprintf(nameBuf, sizeof(nameBuf), "GPUCull_%s_ShadowIndices", name); + idxDesc.debugName = nameBuf; + idxDesc.byteSize = u64(capacity) * sizeof(u32); + idxDesc.structStride = sizeof(u32); + idxDesc.initialState = nvrhi::ResourceStates::ShaderResource; + idxDesc.keepInitialState = true; + set.shadowIndicesBuffer = nvDevice->createBuffer(idxDesc); + + if (set.shadowIndicesBuffer) + { + xr_vector ids(capacity); + for (u32 i = 0; i < capacity; ++i) + ids[i] = i; + cmdList->writeBuffer(set.shadowIndicesBuffer, ids.data(), u64(capacity) * sizeof(u32)); + } + + if (!set.shadowCountBuffer) + { + nvrhi::BufferDesc cntDesc; + snprintf(nameBuf, sizeof(nameBuf), "GPUCull_%s_ShadowCount", name); + cntDesc.debugName = nameBuf; + cntDesc.byteSize = sizeof(u32); + cntDesc.canHaveRawViews = true; + cntDesc.isDrawIndirectArgs = true; + cntDesc.initialState = nvrhi::ResourceStates::IndirectArgument; + cntDesc.keepInitialState = true; + set.shadowCountBuffer = nvDevice->createBuffer(cntDesc); + } + + { + nvrhi::BufferDesc argsDesc; + snprintf(nameBuf, sizeof(nameBuf), "GPUCull_%s_ShadowCompactDrawArgs", name); + argsDesc.debugName = nameBuf; + argsDesc.byteSize = u64(capacity) * sizeof(IndirectDrawArgs); + argsDesc.canHaveRawViews = true; + argsDesc.isDrawIndirectArgs = true; + argsDesc.initialState = nvrhi::ResourceStates::IndirectArgument; + argsDesc.keepInitialState = true; + set.shadowCompactDrawArgsBuffer = nvDevice->createBuffer(argsDesc); + } + { + nvrhi::BufferDesc matDesc; + snprintf(nameBuf, sizeof(nameBuf), "GPUCull_%s_ShadowCompactMats", name); + matDesc.debugName = nameBuf; + matDesc.byteSize = u64(capacity) * sizeof(u32); + matDesc.structStride = sizeof(u32); + matDesc.initialState = nvrhi::ResourceStates::ShaderResource; + matDesc.keepInitialState = true; + set.shadowCompactMaterialIDBuffer = nvDevice->createBuffer(matDesc); + } + + set.shadowBuffersCapacity = capacity; + } + + // Count changes per frame (dynamic set), keep it in sync for cast-all fallback. + if (set.shadowCountBuffer) + cmdList->writeBuffer(set.shadowCountBuffer, &set.objectCount, sizeof(u32)); +} + +void GPUCullingManager::BuildLightFrustumCasters(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const Fmatrix& lightVP, float radiusInflate) +{ + if (!cmdList || !nvDevice) + return; + + ZoneScopedN("BuildLightFrustumCasters"); + + CFrustum frustum; + Fmatrix vp = lightVP; + frustum.CreateFromMatrix(vp, FRUSTUM_P_LRTB | FRUSTUM_P_FAR); + + auto filterSet = [&](CullSetBuffers& set, + const xr_vector& objects, + const xr_vector& drawArgs, + const xr_vector& materialIDs, + const char* name) + { + if (set.objectCount == 0) + return; + EnsureShadowCasterBuffers(cmdList, nvDevice, set, name); + if (!set.shadowIndicesBuffer || !set.shadowCompactDrawArgsBuffer || + !set.shadowCompactMaterialIDBuffer || !set.shadowCountBuffer) + return; + + static thread_local xr_vector ids; + static thread_local xr_vector args; + static thread_local xr_vector mats; + ids.clear(); + args.clear(); + mats.clear(); + ids.reserve(set.objectCount); + args.reserve(set.objectCount); + mats.reserve(set.objectCount); + + const u32 n = std::min(set.objectCount, static_cast(objects.size())); + for (u32 i = 0; i < n; ++i) + { + const GPUObjectData& o = objects[i]; + const float r = o.radius * radiusInflate; + if (!frustum.testSphere_dirty(o.position, r)) + continue; + ids.push_back(i); + if (i < drawArgs.size()) + { + IndirectDrawArgs a = drawArgs[i]; + a.instanceCount = 1; + // DRAWINDEX must index the compact buffers (0..count-1), not the + // source object slot. Keeping the original startInstanceLocation + // made foliage sample wrong materials → solid triangle shadows. + a.startInstanceLocation = static_cast(args.size()); + args.push_back(a); + } + mats.push_back(i < materialIDs.size() ? materialIDs[i] : 0u); + } + + const u32 count = static_cast(ids.size()); + if (count > 0) + { + cmdList->writeBuffer(set.shadowIndicesBuffer, ids.data(), u64(count) * sizeof(u32)); + cmdList->writeBuffer(set.shadowCompactDrawArgsBuffer, args.data(), u64(count) * sizeof(IndirectDrawArgs)); + cmdList->writeBuffer(set.shadowCompactMaterialIDBuffer, mats.data(), u64(count) * sizeof(u32)); + } + cmdList->writeBuffer(set.shadowCountBuffer, &count, sizeof(u32)); + }; + + filterSet(m_staticSet, m_staticObjectData, m_staticDrawArgsData, m_staticMaterialIDData, "Static"); + filterSet(m_dynamicSet, m_dynamicObjectData, m_dynamicDrawArgsData, m_dynamicMaterialIDData, "Dynamic"); + filterSet(m_transparentSet, m_transparentObjectData, m_transparentDrawArgsData, m_transparentMaterialIDData, "Transparent"); +} + +void GPUCullingManager::EnsureShadowCasterIdentityBuffers( + nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice) +{ + if (!cmdList || !nvDevice) + return; + + auto restoreIdentity = [&](CullSetBuffers& set, const char* name) { + EnsureShadowCasterBuffers(cmdList, nvDevice, set, name); + if (!set.shadowIndicesBuffer || set.objectCount == 0) + return; + + // CSM light-frustum cull overwrites indices with a compact subset. Restore a + // full identity map so cast-all local shadows see every caster again. + xr_vector ids(set.objectCount); + for (u32 i = 0; i < set.objectCount; ++i) + ids[i] = i; + cmdList->writeBuffer(set.shadowIndicesBuffer, ids.data(), u64(set.objectCount) * sizeof(u32)); + if (set.shadowCountBuffer) + cmdList->writeBuffer(set.shadowCountBuffer, &set.objectCount, sizeof(u32)); + }; + + restoreIdentity(m_staticSet, "Static"); + restoreIdentity(m_dynamicSet, "Dynamic"); + restoreIdentity(m_transparentSet, "Transparent"); +} + // ═══════════════════════════════════════════════════════ // SETUP CULLING PASS // ═══════════════════════════════════════════════════════ @@ -2515,7 +3075,9 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( u32 hizHeight, u32 hizMipLevels, const GeometryCollector* geometry, - const Fmatrix& prevViewProj) + const Fmatrix& prevViewProj, + bool forceDisableHiz, + bool skipUpload) { using namespace framegraph; @@ -2556,15 +3118,17 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( struct GPUCullPassData { VirtualResourceHandle hizPyramid; - VirtualResourceHandle staticDrawArgsBuffer; // For framegraph tracking - VirtualResourceHandle dynamicDrawArgsBuffer; // For framegraph tracking + VirtualResourceHandle staticDrawArgsBuffer; + VirtualResourceHandle dynamicDrawArgsBuffer; GPUCullingManager* manager; - const GeometryCollector* geometry; // For uploading during execute - Fmatrix prevViewProj; // Previous frame's viewProj for temporal Hi-Z + const GeometryCollector* geometry; + Fmatrix prevViewProj; u32 hizWidth; u32 hizHeight; u32 hizMipLevels; + bool forceDisableHiz = false; + bool skipUpload = false; }; // Import draw args buffers into framegraph for proper state tracking @@ -2577,28 +3141,55 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( drawArgsDesc.isUAV = true; drawArgsDesc.isTransient = false; // Persistent - forward pass needs it - VirtualResourceHandle staticDrawArgsHandle = fg.ImportBuffer("gpu_cull_static_drawargs", m_staticSet.drawArgsBuffer, drawArgsDesc); - VirtualResourceHandle dynamicDrawArgsHandle = fg.ImportBuffer("gpu_cull_dynamic_drawargs", m_dynamicSet.drawArgsBuffer, drawArgsDesc); + VirtualResourceHandle staticDrawArgsHandle = fg.ImportBuffer( + skipUpload ? "gpu_cull_static_drawargs_hiz" : "gpu_cull_static_drawargs", + m_staticSet.drawArgsBuffer, drawArgsDesc); + VirtualResourceHandle dynamicDrawArgsHandle = fg.ImportBuffer( + skipUpload ? "gpu_cull_dynamic_drawargs_hiz" : "gpu_cull_dynamic_drawargs", + m_dynamicSet.drawArgsBuffer, drawArgsDesc); + + // Ensure Hi-Z handle is valid for binding (dummy 1x1 when pyramid missing) + VirtualResourceHandle hizHandle = hizPyramid; + u32 effHizW = hizWidth; + u32 effHizH = hizHeight; + u32 effHizMips = hizMipLevels; + if (!hizHandle.is_valid() && m_dummyHizTexture) + { + ResourceDesc dummyDesc; + dummyDesc.type = ResourceDesc::Type::Texture2D; + dummyDesc.debugName = "rt_DummyHiZ"; + dummyDesc.width = 1; + dummyDesc.height = 1; + dummyDesc.format = nvrhi::Format::R32_FLOAT; + dummyDesc.isImported = true; + dummyDesc.isTransient = false; + hizHandle = fg.ImportTexture("rt_DummyHiZ", m_dummyHizTexture, dummyDesc); + effHizW = 1; + effHizH = 1; + effHizMips = 1; + forceDisableHiz = true; + } auto& passData = fg.addCallbackPass( - "GPU Culling", + skipUpload ? "GPU Culling HiZ" : "GPU Culling", // Setup lambda - [&, hizWidth, hizHeight, hizMipLevels, staticDrawArgsHandle, dynamicDrawArgsHandle, geometry, prevViewProj](FrameGraph& builder, PassHandle passHandle, GPUCullPassData& data) { + [&, effHizW, effHizH, effHizMips, staticDrawArgsHandle, dynamicDrawArgsHandle, geometry, prevViewProj, forceDisableHiz, skipUpload, hizHandle](FrameGraph& builder, PassHandle passHandle, GPUCullPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); - passBuilder.asyncCompute(); + // Graphics queue: asyncCompute left Forward/Skinning sampling compact + // buffers without FG barriers → terrain flicker / empty draws (MoltenVK). + passBuilder.sideEffects(); data.manager = this; - data.geometry = geometry; // Capture for upload during execute - data.prevViewProj = prevViewProj; // Previous frame's viewProj for temporal Hi-Z - data.hizWidth = hizWidth; - data.hizHeight = hizHeight; - data.hizMipLevels = hizMipLevels; - - // Read Hi-Z pyramid - data.hizPyramid = passBuilder.read(hizPyramid, ResourceState::ShaderResource); + data.geometry = geometry; + data.prevViewProj = prevViewProj; + data.hizWidth = effHizW; + data.hizHeight = effHizH; + data.hizMipLevels = effHizMips; + data.forceDisableHiz = forceDisableHiz; + data.skipUpload = skipUpload; - // Write draw args buffers (for dependency tracking) + data.hizPyramid = passBuilder.read(hizHandle, ResourceState::ShaderResource); data.staticDrawArgsBuffer = passBuilder.write(staticDrawArgsHandle, ResourceState::UnorderedAccess); data.dynamicDrawArgsBuffer = passBuilder.write(dynamicDrawArgsHandle, ResourceState::UnorderedAccess); }, @@ -2622,18 +3213,20 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( // ───────────────────────────────────────────────────── // UPLOAD SCENE OBJECTS (must happen during execute, not setup) // ───────────────────────────────────────────────────── - // This ensures we use the correct command list - mgr->UploadSceneObjects(ctx, data.geometry); - - bindless::MaterialBuffer::Instance().Upload(ctx); - - if (mgr->m_rtAccelMgr) { - mgr->m_rtAccelMgr->BuildIfNeeded(cmdList, mgr); - if (mgr->m_rtAccelMgr->IsReady()) { - if (!mgr->m_rtAccelMgr->GetMaterialBuffer()) - mgr->m_rtAccelMgr->SetMaterialBuffer(bindless::MaterialBuffer::Instance().GetBuffer()); - if (!mgr->m_rtAccelMgr->GetTerrainMaterialBuffer()) - mgr->m_rtAccelMgr->SetTerrainMaterialBuffer(bindless::TerrainMaterialBuffer::Instance().GetBuffer()); + if (!data.skipUpload) + { + mgr->UploadSceneObjects(ctx, data.geometry); + + bindless::MaterialBuffer::Instance().Upload(ctx); + + if (mgr->m_rtAccelMgr) { + mgr->m_rtAccelMgr->BuildIfNeeded(cmdList, mgr); + if (mgr->m_rtAccelMgr->IsReady()) { + if (!mgr->m_rtAccelMgr->GetMaterialBuffer()) + mgr->m_rtAccelMgr->SetMaterialBuffer(bindless::MaterialBuffer::Instance().GetBuffer()); + if (!mgr->m_rtAccelMgr->GetTerrainMaterialBuffer()) + mgr->m_rtAccelMgr->SetTerrainMaterialBuffer(bindless::TerrainMaterialBuffer::Instance().GetBuffer()); + } } } @@ -2644,7 +3237,7 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( return; } - auto dispatchCullSet = [&](CullSetBuffers& set, VariantPartitionBuffers* partition = nullptr) { + auto dispatchCullSet = [&](CullSetBuffers& set, VariantPartitionBuffers* partition = nullptr, bool allowHiz = true) { if (set.objectCount == 0) return; @@ -2658,7 +3251,7 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( // Fill constant buffer CullParamsCB cb; cb.viewProj = Device.mFullTransform; - cb.prevViewProj = data.prevViewProj; // Previous frame's viewProj for temporal Hi-Z + cb.prevViewProj = data.prevViewProj; cb.cameraPos = Device.vCameraPosition; float farPlane = g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.far_plane : 300.0f; cb.maxDistanceSq = farPlane * farPlane; @@ -2667,7 +3260,10 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( cb.hizHeight = data.hizHeight; cb.hizMipLevels = data.hizMipLevels; cb.frameId = frameId; - cb.padding[0] = cb.padding[1] = cb.padding[2] = 0; + // Hi-Z occlusion only for static world geometry (stable, doesn't move). + // Dynamic props flicker under temporal Hi-Z → frustum/distance only. + cb.hizEnable = (data.forceDisableHiz || ps_r_hiz_occlusion == 0 || !allowHiz) ? 0u : 1u; + cb.padding[0] = cb.padding[1] = 0; mgr->ExtractFrustumPlanes(Device.mFullTransform, cb.frustumPlanes); @@ -2780,6 +3376,10 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( cmdList->setBufferState(set.compactBatchIndicesBuffer, nvrhi::ResourceStates::UnorderedAccess); cmdList->setBufferState(set.compactMaterialIDBuffer, nvrhi::ResourceStates::UnorderedAccess); + // Without drawIndirectCount, trailing compact slots must be zero (instanceCount=0). + if (GEnv.Backend && !GEnv.Backend->GetCapabilities().drawIndirectCount) + cmdList->clearBufferUInt(set.compactDrawArgsBuffer, 0); + auto* compactScatterRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("batch_compact", ".cs"); framegraph::BindingSetBuilder scatterBsb(*compactScatterRefl, nvDevice); scatterBsb.ConstantBuffer("CompactParams", mgr->m_device->GetNativeBuffer(mgr->m_compactParamsCB)) @@ -2812,8 +3412,8 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( }; dispatchCullSet(mgr->m_staticSet, - mgr->m_variantPartitionEnabled ? &mgr->m_staticPartition : nullptr); - dispatchCullSet(mgr->m_dynamicSet); + mgr->m_variantPartitionEnabled ? &mgr->m_staticPartition : nullptr, /*allowHiz*/ true); + dispatchCullSet(mgr->m_dynamicSet, nullptr, /*allowHiz*/ false); // ───────────────────────────────────────────────────── // TERRAIN CULLING PASS (uses same shader, different data) @@ -2824,9 +3424,11 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( } if (mgr->m_terrainObjectCount > 0 && mgr->m_terrainObjectBuffer && mgr->m_terrainDrawArgsBuffer) { - // Clear terrain visible count and visibility buffer + // Clear terrain visible count and visibility stamps (stale frameId → flicker) u32 zeroTerrain = 0; cmdList->writeBuffer(mgr->m_terrainVisibleCountBuffer, &zeroTerrain, sizeof(u32)); + if (mgr->m_terrainVisibilityBuffer) + cmdList->clearBufferUInt(mgr->m_terrainVisibilityBuffer, 0); // Update constant buffer for terrain (reuse same CB, different object count) CullParamsCB terrainCB; terrainCB.viewProj = Device.mFullTransform; @@ -2839,7 +3441,10 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( terrainCB.hizHeight = data.hizHeight; terrainCB.hizMipLevels = data.hizMipLevels; terrainCB.frameId = frameId; - terrainCB.padding[0] = terrainCB.padding[1] = terrainCB.padding[2] = 0; + // Terrain: frustum/distance only. Temporal Hi-Z on the ground plane + // false-culls patches at mip boundaries → visible flicker. + terrainCB.hizEnable = 0u; + terrainCB.padding[0] = terrainCB.padding[1] = 0; mgr->ExtractFrustumPlanes(Device.mFullTransform, terrainCB.frustumPlanes); cmdList->writeBuffer(mgr->m_device->GetNativeBuffer(mgr->m_cullParamsCB), &terrainCB, sizeof(terrainCB)); @@ -2951,6 +3556,10 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( cmdList->setBufferState(mgr->m_terrainCompactBatchIndicesBuffer, nvrhi::ResourceStates::UnorderedAccess); cmdList->setBufferState(mgr->m_terrainCompactMaterialIDBuffer, nvrhi::ResourceStates::UnorderedAccess); + // Without drawIndirectCount, trailing compact slots must be zero (instanceCount=0). + if (GEnv.Backend && !GEnv.Backend->GetCapabilities().drawIndirectCount) + cmdList->clearBufferUInt(mgr->m_terrainCompactDrawArgsBuffer, 0); + auto* compactScatterRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("batch_compact", ".cs"); framegraph::BindingSetBuilder terrainScatterBsb(*compactScatterRefl, nvDevice); terrainScatterBsb.ConstantBuffer("CompactParams", mgr->m_device->GetNativeBuffer(mgr->m_compactParamsCB)) @@ -2982,8 +3591,13 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( // ───────────────────────────────────────────────────── // TRANSPARENT CULLING (uses CullSetBuffers + dispatchCullSet) // ───────────────────────────────────────────────────── + // Transparent/foliage: frustum only — same-frame Hi-Z false-culls swaying trees. dispatchCullSet(mgr->m_transparentSet, - mgr->m_variantPartitionEnabled ? &mgr->m_transparentPartition : nullptr); + mgr->m_variantPartitionEnabled ? &mgr->m_transparentPartition : nullptr, + /*allowHiz*/ false); + + // Tessellation uses CPU frustum + direct drawIndexed in ForwardColor + // (no GPU compact / DrawIndexedIndirect — hangs on MoltenVK with PatchList). // Note: partition buffers are left in IndirectArgument/ShaderResource state // for the render passes to consume. keepInitialState handles reset for next frame. @@ -3175,6 +3789,7 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( u32 hizWidth; u32 hizHeight; u32 hizMipLevels; + bool forceDisableHiz = false; }; // Import visibility buffer into framegraph for proper dependency tracking @@ -3199,24 +3814,48 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( VirtualResourceHandle argsBufferHandle = fg.ImportBuffer( "skinned_draw_args", m_skinnedDrawArgsBuffer, argsBufferDesc); + VirtualResourceHandle hizHandle = hizPyramid; + u32 effHizW = hizWidth; + u32 effHizH = hizHeight; + u32 effHizMips = hizMipLevels; + bool forceDisableHiz = false; + if (!hizHandle.is_valid() && m_dummyHizTexture) + { + ResourceDesc dummyDesc; + dummyDesc.type = ResourceDesc::Type::Texture2D; + dummyDesc.debugName = "rt_DummyHiZ_Skinned"; + dummyDesc.width = 1; + dummyDesc.height = 1; + dummyDesc.format = nvrhi::Format::R32_FLOAT; + dummyDesc.isImported = true; + dummyDesc.isTransient = false; + hizHandle = fg.ImportTexture("rt_DummyHiZ_Skinned", m_dummyHizTexture, dummyDesc); + effHizW = 1; + effHizH = 1; + effHizMips = 1; + forceDisableHiz = true; + } + auto& passData = fg.addCallbackPass( "Skinned GPU Culling", // Setup lambda - [&, hizWidth, hizHeight, hizMipLevels, geometry, prevViewProj, visBufferHandle, argsBufferHandle, overlayMgr](FrameGraph& builder, PassHandle passHandle, SkinnedCullPassData& data) { + [&, effHizW, effHizH, effHizMips, geometry, prevViewProj, visBufferHandle, argsBufferHandle, overlayMgr, hizHandle, forceDisableHiz](FrameGraph& builder, PassHandle passHandle, SkinnedCullPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); - passBuilder.asyncCompute(); + // Keep on graphics queue so Skinning sees compacted args same-frame. + passBuilder.sideEffects(); data.manager = this; data.geometry = geometry; data.overlayMgr = overlayMgr; data.prevViewProj = prevViewProj; - data.hizWidth = hizWidth; - data.hizHeight = hizHeight; - data.hizMipLevels = hizMipLevels; + data.hizWidth = effHizW; + data.hizHeight = effHizH; + data.hizMipLevels = effHizMips; + data.forceDisableHiz = forceDisableHiz; - // Read Hi-Z pyramid - data.hizPyramid = passBuilder.read(hizPyramid, ResourceState::ShaderResource); + // Read Hi-Z pyramid (real or dummy 1x1) + data.hizPyramid = passBuilder.read(hizHandle, ResourceState::ShaderResource); // Write visibility buffer (ensures pass isn't culled by framegraph) data.visibilityBuffer = passBuilder.write(visBufferHandle, ResourceState::UnorderedAccess); @@ -3261,6 +3900,25 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( cmdList->clearBufferUInt(mgr->m_skinnedVisibleCountBuffer, 0); + // Frustum-only cull false-rejects animated characters (tight rest-pose spheres). + // When Hi-Z occlusion is off, keep all skinned visible and only compact MDI. + if (data.forceDisableHiz || ps_r_hiz_occlusion == 0) + { + static thread_local xr_vector visStamp; + for (u32 f = SkinnedGeometryPools::FIRST_FORMAT; f < SkinnedGeometryPools::FORMAT_COUNT; ++f) + { + SkinnedBucket& bucket = mgr->m_skinnedBuckets[f]; + if (bucket.count == 0) + continue; + visStamp.assign(bucket.count, frameId); + cmdList->writeBuffer(bucket.visibilityBuffer, visStamp.data(), + u64(bucket.count) * sizeof(u32)); + mgr->DispatchSkinnedBucketCompaction(cmdList, nvDevice, bucket, frameId); + } + // Residual draw args already uploaded with instanceCount=1 — no gate. + return; + } + // Fill constant buffer (reuse m_cullParamsCB); objectCount rewritten per dispatch CullParamsCB cb; cb.viewProj = Device.mFullTransform; @@ -3272,7 +3930,8 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( cb.hizHeight = data.hizHeight; cb.hizMipLevels = data.hizMipLevels; cb.frameId = frameId; - cb.padding[0] = cb.padding[1] = cb.padding[2] = 0; + cb.hizEnable = 1u; + cb.padding[0] = cb.padding[1] = 0; mgr->ExtractFrustumPlanes(Device.mFullTransform, cb.frustumPlanes); auto* objectCullRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("object_cull", ".cs"); @@ -3925,6 +4584,7 @@ GPUParticleCullOutput GPUCullingManager::SetupParticleCullingPass( u32 hizWidth; u32 hizHeight; u32 hizMipLevels; + bool forceDisableHiz = false; }; ResourceDesc drawArgsDesc; @@ -3937,21 +4597,44 @@ GPUParticleCullOutput GPUCullingManager::SetupParticleCullingPass( VirtualResourceHandle drawArgsHandle = fg.ImportBuffer("gpu_cull_particle_drawargs", m_particleDrawArgsBuffer, drawArgsDesc); + VirtualResourceHandle hizHandle = hizPyramid; + u32 effHizW = hizWidth; + u32 effHizH = hizHeight; + u32 effHizMips = hizMipLevels; + bool forceDisableHiz = false; + if (!hizHandle.is_valid() && m_dummyHizTexture) + { + ResourceDesc dummyDesc; + dummyDesc.type = ResourceDesc::Type::Texture2D; + dummyDesc.debugName = "rt_DummyHiZ_Particle"; + dummyDesc.width = 1; + dummyDesc.height = 1; + dummyDesc.format = nvrhi::Format::R32_FLOAT; + dummyDesc.isImported = true; + dummyDesc.isTransient = false; + hizHandle = fg.ImportTexture("rt_DummyHiZ_Particle", m_dummyHizTexture, dummyDesc); + effHizW = 1; + effHizH = 1; + effHizMips = 1; + forceDisableHiz = true; + } + auto& passData = fg.addCallbackPass( "GPU Particle Culling", - [&, hizWidth, hizHeight, hizMipLevels, drawArgsHandle, batches](FrameGraph& builder, PassHandle passHandle, ParticleCullPassData& data) { + [&, effHizW, effHizH, effHizMips, drawArgsHandle, batches, hizHandle, forceDisableHiz](FrameGraph& builder, PassHandle passHandle, ParticleCullPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); - passBuilder.asyncCompute(); + passBuilder.sideEffects(); data.manager = this; data.batches = batches; data.particleCount = m_particleCount; - data.hizWidth = hizWidth; - data.hizHeight = hizHeight; - data.hizMipLevels = hizMipLevels; + data.hizWidth = effHizW; + data.hizHeight = effHizH; + data.hizMipLevels = effHizMips; + data.forceDisableHiz = forceDisableHiz; - data.hizPyramid = passBuilder.read(hizPyramid, ResourceState::ShaderResource); + data.hizPyramid = passBuilder.read(hizHandle, ResourceState::ShaderResource); data.drawArgsBuffer = passBuilder.write(drawArgsHandle, ResourceState::UnorderedAccess); }, @@ -3991,7 +4674,8 @@ GPUParticleCullOutput GPUCullingManager::SetupParticleCullingPass( cb.hizHeight = data.hizHeight; cb.hizMipLevels = data.hizMipLevels; cb.frameId = frameId; - cb.padding[0] = cb.padding[1] = cb.padding[2] = 0; + cb.hizEnable = (data.forceDisableHiz || ps_r_hiz_occlusion == 0) ? 0u : 1u; + cb.padding[0] = cb.padding[1] = 0; mgr->ExtractFrustumPlanes(Device.mFullTransform, cb.frustumPlanes); diff --git a/src/Layers/xrRender/GPUCullingManager.h b/src/Layers/xrRender/GPUCullingManager.h index 7f8d7667546..961e568c584 100644 --- a/src/Layers/xrRender/GPUCullingManager.h +++ b/src/Layers/xrRender/GPUCullingManager.h @@ -207,9 +207,13 @@ class GPUCullingManager { u32 hizHeight, u32 hizMipLevels, const GeometryCollector* geometry, - const Fmatrix& prevViewProj // Previous frame's viewProj for temporal Hi-Z + const Fmatrix& prevViewProj, // Previous frame's viewProj for temporal Hi-Z + bool forceDisableHiz = false, + bool skipUpload = false ); + nvrhi::ITexture* GetDummyHiZTexture() const { return m_dummyHizTexture.Get(); } + // Get number of objects uploaded this frame (static + dynamic) u32 GetObjectCount() const { return m_objectCount; } u32 GetStaticObjectCount() const { return m_staticSet.objectCount; } @@ -320,6 +324,41 @@ class GPUCullingManager { nvrhi::IBuffer* GetStaticDrawArgsBuffer() const { return m_staticSet.drawArgsBuffer.Get(); } nvrhi::IBuffer* GetDynamicDrawArgsBuffer() const { return m_dynamicSet.drawArgsBuffer.Get(); } + // ─────────────────────────────────────────────────────── + // SHADOW CASTER (view-independent, unculled) BUFFERS + // ─────────────────────────────────────────────────────── + // drawArgsBuffer holds the full template (instanceCount=1 for every batch) and is + // never touched by the camera cull in compaction mode; materialIDBuffer/instanceBuffer + // are full per-batch. Combined with an identity drawID->batch map these let the shadow + // pass draw all casters regardless of camera direction. + nvrhi::IBuffer* GetStaticFullMaterialIDBuffer() const { return m_staticSet.materialIDBuffer.Get(); } + nvrhi::IBuffer* GetStaticShadowIndicesBuffer() const { return m_staticSet.shadowIndicesBuffer.Get(); } + nvrhi::IBuffer* GetStaticShadowCountBuffer() const { return m_staticSet.shadowCountBuffer.Get(); } + nvrhi::IBuffer* GetDynamicFullMaterialIDBuffer() const { return m_dynamicSet.materialIDBuffer.Get(); } + nvrhi::IBuffer* GetDynamicShadowIndicesBuffer() const { return m_dynamicSet.shadowIndicesBuffer.Get(); } + nvrhi::IBuffer* GetDynamicShadowCountBuffer() const { return m_dynamicSet.shadowCountBuffer.Get(); } + + // Transparent set doubles as the foliage shadow caster (trees/bushes are alpha-blended). + nvrhi::IBuffer* GetTransparentDrawArgsBuffer() const { return m_transparentSet.drawArgsBuffer.Get(); } + nvrhi::IBuffer* GetTransparentFullMaterialIDBuffer() const { return m_transparentSet.materialIDBuffer.Get(); } + nvrhi::IBuffer* GetTransparentShadowIndicesBuffer() const { return m_transparentSet.shadowIndicesBuffer.Get(); } + nvrhi::IBuffer* GetTransparentShadowCountBuffer() const { return m_transparentSet.shadowCountBuffer.Get(); } + + // Light-frustum CSM caster compact (CPU filter + upload). Call once per cascade before draw. + void BuildLightFrustumCasters(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const Fmatrix& lightVP, float radiusInflate = 1.25f); + + /// Ensure identity shadow-caster index/count buffers exist (for cast-all fallback). + /// Does not frustum-filter — safe to call before local-shadow cast-all draws. + void EnsureShadowCasterIdentityBuffers(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice); + + nvrhi::IBuffer* GetStaticShadowCompactDrawArgsBuffer() const { return m_staticSet.shadowCompactDrawArgsBuffer.Get(); } + nvrhi::IBuffer* GetStaticShadowCompactMaterialIDBuffer() const { return m_staticSet.shadowCompactMaterialIDBuffer.Get(); } + nvrhi::IBuffer* GetDynamicShadowCompactDrawArgsBuffer() const { return m_dynamicSet.shadowCompactDrawArgsBuffer.Get(); } + nvrhi::IBuffer* GetDynamicShadowCompactMaterialIDBuffer() const { return m_dynamicSet.shadowCompactMaterialIDBuffer.Get(); } + nvrhi::IBuffer* GetTransparentShadowCompactDrawArgsBuffer() const { return m_transparentSet.shadowCompactDrawArgsBuffer.Get(); } + nvrhi::IBuffer* GetTransparentShadowCompactMaterialIDBuffer() const { return m_transparentSet.shadowCompactMaterialIDBuffer.Get(); } + // ─────────────────────────────────────────────────────── // TERRAIN-SPECIFIC BUFFERS // ─────────────────────────────────────────────────────── @@ -337,6 +376,14 @@ class GPUCullingManager { nvrhi::IBuffer* GetTransparentCompactMaterialIDBuffer() const { return m_transparentSet.compactMaterialIDBuffer.Get(); } nvrhi::IBuffer* GetTransparentCompactCountBuffer() const { return m_transparentSet.compactCountBuffer.Get(); } + // Tessellation set (opaque bump# materials — direct drawIndexed, not MDI) + u32 GetTessObjectCount() const { return m_tessSet.objectCount; } + nvrhi::IBuffer* GetTessInstanceBuffer() const { return m_tessSet.instanceBuffer.Get(); } + nvrhi::IBuffer* GetTessMaterialIDBuffer() const { return m_tessSet.materialIDBuffer.Get(); } + nvrhi::IBuffer* GetTessBatchIndicesBuffer() const { return m_tessBatchIndicesBuffer.Get(); } + const xr_vector& GetTessDrawArgsData() const { return m_tessDrawArgsData; } + const xr_vector& GetTessObjectData() const { return m_tessObjectData; } + // ─────────────────────────────────────────────────────── // VARIANT PARTITIONING (multi-PSO rendering) // ─────────────────────────────────────────────────────── @@ -501,6 +548,14 @@ class GPUCullingManager { nvrhi::BufferHandle compactGroupCountsBuffer; // Visible count per group (scratch) nvrhi::BufferHandle compactGroupOffsetsBuffer; // Prefix offsets per group (scratch) nvrhi::BufferHandle instanceBuffer; // Instance data buffer (GPUInstanceData) + // Shadow caster (view-independent) buffers: identity drawID->batch map + count. + // Used to draw ALL batches into the shadow map regardless of camera frustum, + // so casters that leave the camera view still cast shadows. + nvrhi::BufferHandle shadowIndicesBuffer; // Identity or light-culled batch indices + nvrhi::BufferHandle shadowCountBuffer; // Single u32 = caster count (indirect count) + nvrhi::BufferHandle shadowCompactDrawArgsBuffer; // Compacted draw args for light-frustum CSM + nvrhi::BufferHandle shadowCompactMaterialIDBuffer; + u32 shadowBuffersCapacity = 0; // Capacity the identity buffer was built for u32 objectCount = 0; u32 maxObjects = 0; bool drawArgsUploaded = false; @@ -511,6 +566,11 @@ class GPUCullingManager { CullSetBuffers m_staticSet; CullSetBuffers m_dynamicSet; + // Ensure the view-independent shadow-caster identity/count buffers for a set + // exist and are sized/filled for the current objectCount. + void EnsureShadowCasterBuffers(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + CullSetBuffers& set, const char* name); + // Shared constant buffer fg::BufferHandle m_cullParamsCB; // Constant buffer @@ -583,6 +643,10 @@ class GPUCullingManager { // ─────────────────────────────────────────────────────── CullSetBuffers m_transparentSet; + // Opaque tessellation materials (direct drawIndexed; identity batch map) + CullSetBuffers m_tessSet; + nvrhi::BufferHandle m_tessBatchIndicesBuffer; + // ─────────────────────────────────────────────────────── // DEBUG VISUALIZATION RESOURCES // ─────────────────────────────────────────────────────── @@ -617,6 +681,7 @@ class GPUCullingManager { u32 m_maxObjects = 0; bool m_initialized = false; bool m_computeEnabled = false; + nvrhi::TextureHandle m_dummyHizTexture; // 1x1 R32F when no real Hi-Z yet xr_vector m_staticObjectData; xr_vector m_staticDrawArgsData; @@ -643,6 +708,12 @@ class GPUCullingManager { xr_vector m_transparentMaterialIDData; xr_vector m_transparentInstanceData; + // Tessellation-specific CPU data (bump# / PN+HM materials) + xr_vector m_tessObjectData; + xr_vector m_tessDrawArgsData; + xr_vector m_tessMaterialIDData; + xr_vector m_tessInstanceData; + // ─────────────────────────────────────────────────────── // SKINNED MESH CULLING (GPU-Driven) // ─────────────────────────────────────────────────────── diff --git a/src/Layers/xrRender/Geometry/MaterialCache.cpp b/src/Layers/xrRender/Geometry/MaterialCache.cpp index 23097cd67fe..d9a42d888cc 100644 --- a/src/Layers/xrRender/Geometry/MaterialCache.cpp +++ b/src/Layers/xrRender/Geometry/MaterialCache.cpp @@ -930,7 +930,7 @@ MaterialPSO* MaterialCache::CreateUIPSO( psoDesc.blendState.renderTargets[0].blendEnable = true; psoDesc.blendState.renderTargets[0].srcBlend = fg::BlendFactor::SrcAlpha; psoDesc.blendState.renderTargets[0].dstBlend = fg::BlendFactor::InvSrcAlpha; - psoDesc.blendState.renderTargets[0].srcBlendAlpha = fg::BlendFactor::SrcAlpha; + psoDesc.blendState.renderTargets[0].srcBlendAlpha = fg::BlendFactor::One; psoDesc.blendState.renderTargets[0].dstBlendAlpha = fg::BlendFactor::InvSrcAlpha; psoDesc.rasterizerState.cullMode = fg::CullMode::None; @@ -1059,6 +1059,10 @@ u32 MaterialCache::RegisterBindlessMaterial(MaterialPSO* matPSO) matData.alphaRef = 0.5f; matData.flags = 0; matData.shaderVariant = 0; + matData.normalXIndex = INVALID_TEXTURE_INDEX; + matData.detailBumpIndex = INVALID_TEXTURE_INDEX; + matData.detailBumpXIndex = INVALID_TEXTURE_INDEX; + matData.tessMethod = 0; if (matPSO->pass) { fg::STextureList* texList = matPSO->pass->T._get(); @@ -1128,11 +1132,26 @@ u32 MaterialCache::PreRegisterTerrainMaterial(dxRender_Visual* visual) matData.pbrG_Index = INVALID_TEXTURE_INDEX; matData.pbrB_Index = INVALID_TEXTURE_INDEX; matData.pbrA_Index = INVALID_TEXTURE_INDEX; + matData.normalXR_Index = INVALID_TEXTURE_INDEX; + matData.normalXG_Index = INVALID_TEXTURE_INDEX; + matData.normalXB_Index = INVALID_TEXTURE_INDEX; + matData.normalXA_Index = INVALID_TEXTURE_INDEX; + matData.lmapIndex = INVALID_TEXTURE_INDEX; + matData.pad0 = matData.pad1 = matData.pad2 = 0; matData.flags = MAT_FLAG_TERRAIN; - if (visual->textureName.size() > 0) { - matData.detailScale = GetDetailScale(visual->textureName); + // textureName may be "base,lmap,$null" — use first token for detail scale + shared_str baseForScale = visual->textureName; + if (baseForScale.size()) + { + pcstr p = baseForScale.c_str(); + pcstr comma = strchr(p, ','); + if (comma) + baseForScale = shared_str(xr_string(p, comma - p).c_str()); + } + if (baseForScale.size() > 0) { + matData.detailScale = GetDetailScale(baseForScale); } else { matData.detailScale = 4.0f; } @@ -1221,21 +1240,53 @@ void MaterialCache::FinalizePendingTerrainMaterials(fg::RenderContext* ctx) }; if (visual->textureName.size()) { - u32 idx = RegisterTexture(visual->textureName.c_str(), "base"); + // Classic L_textures: "base[,lmap[,hemi]]" + xr_vector texSlots; + { + const char* p = visual->textureName.c_str(); + while (p && *p) + { + while (*p == ' ') + ++p; + const char* start = p; + while (*p && *p != ',') + ++p; + if (p > start) + texSlots.emplace_back(start, p - start); + if (*p == ',') + ++p; + } + } + + const char* baseName = !texSlots.empty() ? texSlots[0].c_str() : visual->textureName.c_str(); + u32 idx = RegisterTexture(baseName, "base"); if (idx != INVALID_TEXTURE_INDEX) { matData.baseAlbedoIndex = idx; updated = true; } - } - if (visual->textureName.size()) { - xr_string maskName(visual->textureName.c_str()); + xr_string maskName(baseName); maskName += "_mask"; - u32 idx = RegisterTexture(maskName.c_str(), "mask"); + idx = RegisterTexture(maskName.c_str(), "mask"); if (idx != INVALID_TEXTURE_INDEX) { matData.blendMaskIndex = idx; updated = true; } + + if (texSlots.size() > 1) + { + const char* lmapName = texSlots[1].c_str(); + if (lmapName && lmapName[0] && 0 != xr_strcmp(lmapName, "$null")) + { + u32 lmIdx = RegisterTexture(lmapName, "lmap"); + if (lmIdx != INVALID_TEXTURE_INDEX) + { + matData.lmapIndex = lmIdx; + matData.flags |= MAT_FLAG_HAS_LMAP; + updated = true; + } + } + } } const char* detailR = hasTerrainDetail ? detailNames.r : nullptr; @@ -1260,36 +1311,34 @@ void MaterialCache::FinalizePendingTerrainMaterials(fg::RenderContext* ctx) if (idx != INVALID_TEXTURE_INDEX) { matData.detailA_Index = idx; updated = true; } } - auto& texDescMgr = TextureDescr; - if (detailR && detailR[0]) { - shared_str bumpR = texDescMgr.GetBumpName(detailR); - if (bumpR.size()) { - u32 idx = RegisterTexture(bumpR.c_str(), "normalR"); - if (idx != INVALID_TEXTURE_INDEX) { matData.normalR_Index = idx; updated = true; } - } - } - if (detailG && detailG[0]) { - shared_str bumpG = texDescMgr.GetBumpName(detailG); - if (bumpG.size()) { - u32 idx = RegisterTexture(bumpG.c_str(), "normalG"); - if (idx != INVALID_TEXTURE_INDEX) { matData.normalG_Index = idx; updated = true; } - } - } - if (detailB && detailB[0]) { - shared_str bumpB = texDescMgr.GetBumpName(detailB); - if (bumpB.size()) { - u32 idx = RegisterTexture(bumpB.c_str(), "normalB"); - if (idx != INVALID_TEXTURE_INDEX) { matData.normalB_Index = idx; updated = true; } + // Classic Blender_BmmD: s_dn_* = "_bump", s_dn_*X = "_bump#" + // (NOT textures.ltx GetBumpName — that path is for object materials.) + auto loadTerrainBump = [&](const char* detailName, u32& normalIdx, u32& normalXIdx, + const char* tagN, const char* tagX) { + if (!detailName || !detailName[0]) + return; + string256 bumpName, bumpXName; + strconcat(sizeof(bumpName), bumpName, detailName, "_bump"); + u32 idx = RegisterTexture(bumpName, tagN); + if (idx != INVALID_TEXTURE_INDEX) { + normalIdx = idx; + updated = true; } - } - if (detailA && detailA[0]) { - shared_str bumpA = texDescMgr.GetBumpName(detailA); - if (bumpA.size()) { - u32 idx = RegisterTexture(bumpA.c_str(), "normalA"); - if (idx != INVALID_TEXTURE_INDEX) { matData.normalA_Index = idx; updated = true; } + strconcat(sizeof(bumpXName), bumpXName, detailName, "_bump#"); + u32 idxX = RegisterTexture(bumpXName, tagX); + if (idxX != INVALID_TEXTURE_INDEX) { + normalXIdx = idxX; + matData.flags |= MAT_FLAG_HAS_NORMAL_X; + updated = true; } - } + }; + + loadTerrainBump(detailR, matData.normalR_Index, matData.normalXR_Index, "normalR", "normalXR"); + loadTerrainBump(detailG, matData.normalG_Index, matData.normalXG_Index, "normalG", "normalXG"); + loadTerrainBump(detailB, matData.normalB_Index, matData.normalXB_Index, "normalB", "normalXB"); + loadTerrainBump(detailA, matData.normalA_Index, matData.normalXA_Index, "normalA", "normalXA"); + auto& texDescMgr = TextureDescr; if (detailR && detailR[0]) { shared_str pbrR = texDescMgr.GetPBRName(detailR); if (pbrR.size()) { @@ -1331,11 +1380,13 @@ void MaterialCache::FinalizePendingTerrainMaterials(fg::RenderContext* ctx) bool hasZeroIndex = (matData.baseAlbedoIndex == 0 || matData.blendMaskIndex == 0 || matData.detailR_Index == 0 || matData.detailG_Index == 0 || matData.normalR_Index == 0 || matData.pbrR_Index == 0); - Msg("* [TerrainMat] id=%u base=%u mask=%u dR=%u dG=%u dB=%u dA=%u nR=%u nG=%u pR=%u%s", - terrainMaterialID, matData.baseAlbedoIndex, matData.blendMaskIndex, - matData.detailR_Index, matData.detailG_Index, matData.detailB_Index, matData.detailA_Index, - matData.normalR_Index, matData.normalG_Index, matData.pbrR_Index, - hasZeroIndex ? " [IDX 0!]" : ""); + Msg("* [TerrainMat] id=%u base=%u mask=%u dR=%u nR=%u nXR=%u flags=0x%x%s", + terrainMaterialID, + matData.baseAlbedoIndex, matData.blendMaskIndex, + matData.detailR_Index, + matData.normalR_Index, matData.normalXR_Index, + matData.flags, + hasZeroIndex ? " [WARN: index 0!]" : ""); s_debugLogCount++; } @@ -1393,6 +1444,14 @@ u32 MaterialCache::PreRegisterBindlessMaterial(dxRender_Visual* visual) matData.alphaRef = 0.5f; matData.flags = 0; matData.shaderVariant = 0; + matData.normalXIndex = INVALID_TEXTURE_INDEX; + matData.detailBumpIndex = INVALID_TEXTURE_INDEX; + matData.detailBumpXIndex = INVALID_TEXTURE_INDEX; + matData.tessMethod = 0; + matData.normalXIndex = INVALID_TEXTURE_INDEX; + matData.detailBumpIndex = INVALID_TEXTURE_INDEX; + matData.detailBumpXIndex = INVALID_TEXTURE_INDEX; + matData.tessMethod = 0; if (visual->textureName.size() > 0) { matData.detailScale = GetDetailScale(visual->textureName); @@ -1408,8 +1467,30 @@ u32 MaterialCache::PreRegisterBindlessMaterial(dxRender_Visual* visual) matData.flags |= MAT_FLAG_ALPHA_BLEND; } if (strstr(visual->shaderName.c_str(), "water") != nullptr) + { matData.flags |= MAT_FLAG_WATER; + // Ensure water goes through transparent partition even if blender props are stale + matData.flags |= MAT_FLAG_ALPHA_BLEND; + } + // Trees / bushes / leaves — enable foliage SSS path + { + const char* sh = visual->shaderName.c_str(); + const char* tex = visual->textureName.size() ? visual->textureName.c_str() : ""; + const u16 vtype = visual->getType(); + const bool treeType = (vtype == MT_TREE_ST || vtype == MT_TREE_PM); + if (treeType || + strstr(sh, "trees") || strstr(sh, "tree\\") || strstr(sh, "tree/") || + strstr(sh, "bush") || strstr(sh, "flora") || strstr(sh, "leaf") || + strstr(sh, "vegetation") || strstr(sh, "lod\\") || strstr(sh, "lod/") || + strstr(tex, "trees\\") || strstr(tex, "trees/") || + strstr(tex, "bush") || strstr(tex, "flora") || strstr(tex, "leaves")) + { + matData.flags |= MAT_FLAG_FOLIAGE; + } + } matData.shaderVariant = matInfo.shaderVariant; + if (matInfo.transparent) + matData.flags |= MAT_FLAG_ALPHA_BLEND; matData.flags |= MAT_FLAG_HAS_NORMAL; } @@ -1435,14 +1516,19 @@ u32 MaterialCache::PreRegisterBindlessMaterial(dxRender_Visual* visual) return materialID; } -u32 MaterialCache::PreRegisterParticleMaterial(const shared_str& textureName) +u32 MaterialCache::PreRegisterParticleMaterial(const shared_str& textureName, u8 blendMode) { using namespace fg::bindless; if (!textureName.size() || !textureName[0]) return UINT32_MAX; - auto it = m_particleTextureToMaterialID.find(textureName); + // Key by texture + blend: SET needs aref 200, BLEND/ADD keep near-zero for soft fade. + string256 cacheKey; + xr_sprintf(cacheKey, "%s#%u", textureName.c_str(), (u32)blendMode); + shared_str key(cacheKey); + + auto it = m_particleTextureToMaterialID.find(key); if (it != m_particleTextureToMaterialID.end()) return it->second; @@ -1450,19 +1536,29 @@ u32 MaterialCache::PreRegisterParticleMaterial(const shared_str& textureName) if (!materialBuffer.IsInitialized()) return UINT32_MAX; + // PARTICLE_BLEND_SET = 0 (classic deferred blender aref 200 + depth write) + constexpr u8 kParticleBlendSet = 0; MaterialData matData = {}; matData.diffuseIndex = INVALID_TEXTURE_INDEX; matData.normalIndex = INVALID_TEXTURE_INDEX; matData.detailIndex = INVALID_TEXTURE_INDEX; matData.pbrIndex = INVALID_TEXTURE_INDEX; matData.detailScale = 1.0f; - matData.alphaRef = 0.01f / 255.0f; - matData.flags = 0; + if (blendMode == kParticleBlendSet) + { + matData.alphaRef = 200.0f / 255.0f; + matData.flags = MAT_FLAG_ALPHA_TEST; + } + else + { + matData.alphaRef = 0.01f / 255.0f; + matData.flags = 0; + } matData.shaderVariant = 0; u32 materialID = materialBuffer.RegisterMaterial(matData); - m_particleTextureToMaterialID[textureName] = materialID; + m_particleTextureToMaterialID[key] = materialID; PendingMaterial pending; pending.materialID = materialID; @@ -1470,8 +1566,9 @@ u32 MaterialCache::PreRegisterParticleMaterial(const shared_str& textureName) pending.textureName = textureName; m_pendingMaterials.push_back(pending); - Msg("* [MaterialCache] PreRegisterParticle: matID=%u tex='%s' pending=%u", - materialID, textureName.c_str(), static_cast(m_pendingMaterials.size())); + Msg("* [MaterialCache] PreRegisterParticle: matID=%u tex='%s' blend=%u aref=%.3f pending=%u", + materialID, textureName.c_str(), (u32)blendMode, matData.alphaRef, + static_cast(m_pendingMaterials.size())); return materialID; } @@ -1522,9 +1619,34 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) diffuseName = pending.textureName; } + // effects_water*.s hardcode s_base / s_nmap (ignore visual tex name). + const bool isWater = (matData.flags & MAT_FLAG_WATER) != 0; + if (isWater) + { + const char* shader = visual && visual->shaderName.size() ? visual->shaderName.c_str() : ""; + if (strstr(shader, "studen")) + diffuseName = "water\\water_studen"; + else if (strstr(shader, "ryaska")) + diffuseName = "water\\water_ryaska1"; + else + diffuseName = "water\\water_water"; + } + if (!diffuseName.size() || !diffuseName[0]) continue; + // Classic texture lists are "s_base[,s_distort|lmap|...]". Particle + // registration already picks the correct slot; still strip commas here + // so any leftover "a,b" names cannot hit the loader as one path. + { + const char* p = diffuseName.c_str(); + const char* comma = strchr(p, ','); + if (comma) + diffuseName = shared_str(xr_string(p, comma - p).c_str()); + if (!diffuseName.size() || !diffuseName[0]) + continue; + } + { resources::TextureHandle handle = texManager->LoadTexture(diffuseName.c_str()); if (handle.IsValid()) { @@ -1540,7 +1662,7 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) } auto& texDescMgr = TextureDescr; - shared_str bumpName = texDescMgr.GetBumpName(diffuseName); + shared_str bumpName = isWater ? shared_str("water\\water_normal") : texDescMgr.GetBumpName(diffuseName); if (bumpName.size() && bumpName[0]) { resources::TextureHandle handle = texManager->LoadTexture(bumpName.c_str()); if (handle.IsValid()) { @@ -1554,9 +1676,28 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) } } } + + // bump# (s_bumpX) — dual-bump error + height for HM tess / POM + if (!isWater) { + string256 bumpXName; + xr_sprintf(bumpXName, "%s#", bumpName.c_str()); + resources::TextureHandle hx = texManager->LoadTexture(bumpXName); + if (hx.IsValid()) { + nvrhi::ITexture* nvrhiTex = texManager->GetNVRHITexture(hx); + if (nvrhiTex) { + u32 descriptorIndex = backend->RegisterBindlessTexture(nvrhiTex); + if (descriptorIndex != INVALID_TEXTURE_INDEX) { + matData.normalXIndex = descriptorIndex; + matData.flags |= MAT_FLAG_HAS_NORMAL_X; + updated = true; + } + } + } + } } LPCSTR detailTexName = nullptr; + bool detailHasBump = false; if (texDescMgr.GetDetailTexture(diffuseName, detailTexName)) { if (detailTexName && detailTexName[0]) { resources::TextureHandle handle = texManager->LoadTexture(detailTexName); @@ -1572,9 +1713,73 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) } } } + + bool bDiffuseDetail = false; + texDescMgr.GetTextureUsage(diffuseName, bDiffuseDetail, detailHasBump); + if (detailHasBump) { + shared_str detailBump = texDescMgr.GetBumpName(shared_str(detailTexName)); + if (detailBump.size() && detailBump[0]) { + resources::TextureHandle hb = texManager->LoadTexture(detailBump.c_str()); + if (hb.IsValid()) { + nvrhi::ITexture* nvrhiTex = texManager->GetNVRHITexture(hb); + if (nvrhiTex) { + u32 idx = backend->RegisterBindlessTexture(nvrhiTex); + if (idx != INVALID_TEXTURE_INDEX) { + matData.detailBumpIndex = idx; + updated = true; + } + } + } + string256 detailBumpX; + xr_sprintf(detailBumpX, "%s#", detailBump.c_str()); + resources::TextureHandle hbx = texManager->LoadTexture(detailBumpX); + if (hbx.IsValid()) { + nvrhi::ITexture* nvrhiTex = texManager->GetNVRHITexture(hbx); + if (nvrhiTex) { + u32 idx = backend->RegisterBindlessTexture(nvrhiTex); + if (idx != INVALID_TEXTURE_INDEX) { + matData.detailBumpXIndex = idx; + updated = true; + } + } + } + if (matData.detailBumpIndex != INVALID_TEXTURE_INDEX && + matData.detailBumpXIndex != INVALID_TEXTURE_INDEX) + { + matData.flags |= MAT_FLAG_HAS_DETAIL_BUMP; + } + } + } } } + // Tessellation method assignment (used only when r4_enable_tessellation is + // on — and that flag is forcibly cleared on Apple/MoltenVK). + // Classic: deffer_model / deffer_flat blender token. We approximate with + // models\ + bump# only (PN+HM). Level/trees use steep parallax instead. + bool denyTess = isWater || + (matData.flags & (MAT_FLAG_FOLIAGE | MAT_FLAG_WATER | MAT_FLAG_TERRAIN | + MAT_FLAG_ALPHA_TEST | MAT_FLAG_ALPHA_BLEND)) != 0; + bool isModelShader = false; + if (visual && visual->shaderName.size()) + { + const char* sh = visual->shaderName.c_str(); + if (strstr(sh, "models\\") || strstr(sh, "models/") || + strstr(sh, "model\\") || strstr(sh, "model/")) + isModelShader = true; + if (strstr(sh, "flora") || strstr(sh, "tree") || strstr(sh, "grass") || + strstr(sh, "particle") || strstr(sh, "effects\\")) + denyTess = true; + } + u32 wantMethod = 0; + if (!denyTess && isModelShader && (matData.flags & MAT_FLAG_HAS_NORMAL_X)) + wantMethod = 3; // TESS_PN_HM + if (matData.tessMethod != wantMethod) + { + matData.tessMethod = wantMethod; + updated = true; + } + if (diffuseName.c_str() && diffuseName[0]) { shared_str pbrName = texDescMgr.GetPBRName(diffuseName); if (!pbrName.empty()) { @@ -1593,6 +1798,25 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) } } + // One-shot bump + tess diagnostic + { + static u32 s_bumpDbgCount = 0; + static u32 s_tessMatCount = 0; + if (matData.tessMethod != 0) + s_tessMatCount++; + if (s_bumpDbgCount < 40) { + Msg("* [BumpDbg] mat=%u diff='%s' bump='%s' nIdx=%u nXIdx=%u flags=0x%X tess=%u", + materialID, + diffuseName.size() ? diffuseName.c_str() : "", + bumpName.size() ? bumpName.c_str() : "(none)", + matData.normalIndex, matData.normalXIndex, matData.flags, + matData.tessMethod); + s_bumpDbgCount++; + if (s_bumpDbgCount == 40) + Msg("* [TessDbg] materials with tessMethod!=0 so far: %u", s_tessMatCount); + } + } + if (updated) { materialBuffer.UpdateMaterial(materialID, matData); processedCount++; diff --git a/src/Layers/xrRender/Geometry/MaterialCache.h b/src/Layers/xrRender/Geometry/MaterialCache.h index e370362fe87..74f924791f2 100644 --- a/src/Layers/xrRender/Geometry/MaterialCache.h +++ b/src/Layers/xrRender/Geometry/MaterialCache.h @@ -343,7 +343,7 @@ class MaterialCache { u32 PreRegisterBindlessMaterial(dxRender_Visual* visual); - u32 PreRegisterParticleMaterial(const shared_str& textureName); + u32 PreRegisterParticleMaterial(const shared_str& textureName, u8 blendMode = 1); bool IsTerrainMaterial(dxRender_Visual* visual); diff --git a/src/Layers/xrRender/HOM.cpp b/src/Layers/xrRender/HOM.cpp index 546a57d30cd..60bb9df86da 100644 --- a/src/Layers/xrRender/HOM.cpp +++ b/src/Layers/xrRender/HOM.cpp @@ -263,7 +263,7 @@ void CHOM::Render_DB(CFrustum& base) void CHOM::Render(CFrustum& base) { - if (!bEnabled) + if (!bEnabled || !m_pModel) return; ZoneScoped; diff --git a/src/Layers/xrRender/Materials/MaterialSystem.cpp b/src/Layers/xrRender/Materials/MaterialSystem.cpp index 673506b6fef..e3930028f14 100644 --- a/src/Layers/xrRender/Materials/MaterialSystem.cpp +++ b/src/Layers/xrRender/Materials/MaterialSystem.cpp @@ -110,9 +110,20 @@ const MaterialSystem::MaterialInfo& MaterialSystem::GetMaterialInfo(const shared case BlendMode::Additive: case BlendMode::Multiply: case BlendMode::Multiply2X: - info.alphaTest = false; - info.alphaRef = 0; + // Classic blenders often keep AREF together with blend (model/detail/ + // screen_set with oAREF). Dropping aref made bindless_forward skip + // clip() → fringe / no cutoff on alpha effects. info.transparent = true; + if (blendInfo.alphaRef > 0) + { + info.alphaTest = true; + info.alphaRef = blendInfo.alphaRef; + } + else + { + info.alphaTest = false; + info.alphaRef = 0; + } break; } if (blendInfo.strictB2F) @@ -135,6 +146,12 @@ const MaterialSystem::MaterialInfo& MaterialSystem::GetMaterialInfo(const shared } } + // Water shaders are always transparent (r3 blender: SRCALPHA/INVSRCALPHA). + // Do not register a water.s.json until variant-partition cost is acceptable — + // water shading lives in bindless_forward.ps (MAT_FLAG_WATER). + if (strstr(shaderName, "water")) + info.transparent = true; + m_materialCache[key] = info; return m_materialCache[key]; } diff --git a/src/Layers/xrRender/RenderContext/RenderDevice.h b/src/Layers/xrRender/RenderContext/RenderDevice.h index a7773c77e06..ade4fc42b46 100644 --- a/src/Layers/xrRender/RenderContext/RenderDevice.h +++ b/src/Layers/xrRender/RenderContext/RenderDevice.h @@ -163,7 +163,7 @@ class RenderDevice { bool isVolatile = false; u32 maxVersions = 0; - static constexpr u32 VOLATILE_CB_MAX_VERSIONS = 4; + static constexpr u32 VOLATILE_CB_MAX_VERSIONS = 64; shared_str debugName; }; diff --git a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp index c2c65b91391..23ef33bbeac 100644 --- a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp +++ b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp @@ -1,6 +1,8 @@ #include "stdafx.h" #include "VariantPSOCache.h" #include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" #include "Layers/xrRender/GPUCullingManager.h" #include "Layers/xrRender/FrameGraphPasses/PassCommon.h" @@ -45,14 +47,22 @@ static const char* GetDefaultSkinnedVS(u32 vertexFormat) switch (vertexFormat) { case VF_SKINNED_NONHQ: return "bindless_skinned"; - case VF_SKINNED_HQ1W: - case VF_SKINNED_HQ2W: - case VF_SKINNED_HQ3W: - case VF_SKINNED_HQ4W: return "bindless_skinned_hq"; + case VF_SKINNED_HQ1W: return "bindless_skinned_hq"; + case VF_SKINNED_HQ2W: return "bindless_skinned_2w"; + case VF_SKINNED_HQ3W: return "bindless_skinned_3w"; + case VF_SKINNED_HQ4W: return "bindless_skinned_4w"; default: return nullptr; } } +static const char* GetDefaultSkinnedPS(u32 vertexFormat) +{ + // All skinned VS share the same lit PS (IBL + CSM + clustered) + if (vertexFormat >= VF_SKINNED_NONHQ && vertexFormat <= VF_SKINNED_HQ3W) + return "bindless_skinned"; + return nullptr; +} + nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( nvrhi::IDevice* device, nvrhi::IFramebuffer* framebuffer, @@ -76,28 +86,78 @@ nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( const auto& pass = variant.GetPass(passIndex); const char* vsName = pass.vsName.c_str(); - if (vertexFormat >= VF_SKINNED_NONHQ && !xr_strcmp(vsName, "bindless_forward")) + const char* psName = pass.psName.c_str(); + bool remappedToSkinned = false; + + // Model materials are authored as bindless_forward; skinned draws must use + // bindless_skinned* so NPCs/bots get the same IBL/CSM/clustered path as world. + if (vertexFormat >= VF_SKINNED_NONHQ && vertexFormat <= VF_SKINNED_HQ3W) { - const char* skinnedVS = GetDefaultSkinnedVS(vertexFormat); - if (skinnedVS) - vsName = skinnedVS; + if (!xr_strcmp(vsName, "bindless_forward")) + { + if (const char* skinnedVS = GetDefaultSkinnedVS(vertexFormat)) + { + vsName = skinnedVS; + remappedToSkinned = true; + } + } + if (!xr_strcmp(psName, "bindless_forward")) + { + if (const char* skinnedPS = GetDefaultSkinnedPS(vertexFormat)) + { + psName = skinnedPS; + remappedToSkinned = true; + } + } } - auto vs = LoadShader(nvrhi::ShaderType::Vertex, vsName); - auto ps = LoadShader(nvrhi::ShaderType::Pixel, pass.psName.c_str()); - if (!vs || !ps) + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + if (!shaderLoader) + return nullptr; + + auto vsResult = shaderLoader->LoadVertexShader(vsName, "main"); + auto psResult = shaderLoader->LoadPixelShader(psName, "main"); + if (!vsResult.handle || !psResult.handle) + return nullptr; + + // Cache handles for reuse + { + string256 vsKey, psKey; + xr_sprintf(vsKey, "%d:%s", static_cast(nvrhi::ShaderType::Vertex), vsName); + xr_sprintf(psKey, "%d:%s", static_cast(nvrhi::ShaderType::Pixel), psName); + m_shaderCache[shared_str(vsKey)] = vsResult.handle; + m_shaderCache[shared_str(psKey)] = psResult.handle; + } + + // Remapped skinned default shaders share SkinningPass layout (IBL/CSM binds). + // Other custom variants (e.g. water) keep a reflection-matched layout. + nvrhi::BindingLayoutHandle layoutHandle = passBindingLayout; + const bool customShaders = + !remappedToSkinned && + (xr_strcmp(vsName, "bindless_forward") != 0 || + xr_strcmp(psName, "bindless_forward") != 0); + if (customShaders && vsResult.reflection && psResult.reflection) + { + string256 layoutName; + xr_sprintf(layoutName, "Variant_%s_p%u_fmt%u", variant.name.c_str(), passIndex, vertexFormat); + auto customLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection( + layoutName, *vsResult.reflection, *psResult.reflection, device); + if (customLayout) + layoutHandle = customLayout; + } + if (!layoutHandle) return nullptr; nvrhi::GraphicsPipelineDesc pipeDesc; - pipeDesc.VS = vs; - pipeDesc.PS = ps; + pipeDesc.VS = vsResult.handle; + pipeDesc.PS = psResult.handle; pipeDesc.inputLayout = inputLayout; pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; if (bindlessLayout) - pipeDesc.bindingLayouts = {passBindingLayout, bindlessLayout}; + pipeDesc.bindingLayouts = { layoutHandle, bindlessLayout }; else - pipeDesc.bindingLayouts = {passBindingLayout}; + pipeDesc.bindingLayouts = { layoutHandle }; pipeDesc.renderState.depthStencilState = pass.depthStencil; pipeDesc.renderState.rasterState = pass.rasterState; @@ -116,9 +176,9 @@ nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( return nullptr; } - Msg("* [VariantPSO] Created pipeline: '%s' pass=%d vs=%s ps=%s blend=%s", - variant.name.c_str(), passIndex, vsName, pass.psName.c_str(), - pass.blendEnabled ? "yes" : "no"); + Msg("* [VariantPSO] Created pipeline: '%s' pass=%d vs=%s ps=%s blend=%s skinnedRemap=%d", + variant.name.c_str(), passIndex, vsName, psName, + pass.blendEnabled ? "yes" : "no", remappedToSkinned ? 1 : 0); m_cache[key] = pipeline; return pipeline.Get(); @@ -139,11 +199,19 @@ void DrawVariantPartition( { auto& registry = ShaderVariantRegistry::Instance(); auto& psoCache = VariantPSOCache::Instance(); + auto& smpCache = framegraph::GetPassResourceCache(); const auto& p = cfg.partition; + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + if (!shaderLoader) + return; for (u32 v = 0; v < p.variantCount; v++) { nvrhi::IGraphicsPipeline* pso; + const char* vsName = "bindless_forward"; + const char* psName = "bindless_forward"; + nvrhi::IBindingLayout* layout = cfg.passLayout; + if (v == 0) { pso = cfg.defaultPipeline; } else { @@ -153,21 +221,76 @@ void DrawVariantPartition( pso = psoCache.GetOrCreatePSO(nvDevice, framebuffer, v, *variant, 0, VF_MDI, cfg.inputLayout, cfg.passLayout, cfg.bindlessLayout); if (!pso) continue; + + if (!variant->passes.empty()) + { + vsName = variant->passes[0].vsName.c_str(); + psName = variant->passes[0].psName.c_str(); + } + // Prefer layout baked into the variant PSO (may differ from default forward). + const auto& pipeDesc = pso->getDesc(); + if (!pipeDesc.bindingLayouts.empty()) + layout = pipeDesc.bindingLayouts[0]; } - nvrhi::BindingSetDesc bindDesc; - bindDesc.bindings = { - nvrhi::BindingSetItem::ConstantBuffer(2, cfg.staticGlobalsCB), - nvrhi::BindingSetItem::ConstantBuffer(4, cfg.lightingCB), - nvrhi::BindingSetItem::StructuredBuffer_SRV(8, cfg.materialBuffer), - nvrhi::BindingSetItem::StructuredBuffer_SRV(10, cfg.variantTexBuffer), - nvrhi::BindingSetItem::Sampler(0, cfg.sampler), - nvrhi::BindingSetItem::StructuredBuffer_SRV(14, cfg.instanceBuffer), - nvrhi::BindingSetItem::StructuredBuffer_SRV(15, p.batchIndicesBuffer), - nvrhi::BindingSetItem::StructuredBuffer_SRV(16, p.materialIDsBuffer), - }; - auto bindingSet = nvDevice->createBindingSet(bindDesc, cfg.passLayout); - if (!bindingSet) continue; + nvrhi::ITexture* dummy2D = smpCache.GetDummyShadowMap2D(nvDevice); + + nvrhi::ITexture* sky0 = cfg.envSky0 ? cfg.envSky0 : smpCache.GetDummyCubeMap(nvDevice); + nvrhi::ITexture* sky1 = cfg.envSky1 ? cfg.envSky1 : smpCache.GetDummyCubeMap(nvDevice); + + auto* vsReflection = shaderLoader->GetCachedReflection(vsName, ".vs"); + auto* psReflection = shaderLoader->GetCachedReflection(psName, ".ps"); + if (!vsReflection || !psReflection || !layout) + { + Msg("! [VariantPartition] Missing reflection/layout for %s/%s — skip variant %u", + vsName, psName, v); + continue; + } + + framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "VariantPartition"); + bsb.ConstantBuffer("static_globals", cfg.staticGlobalsCB); + bsb.BufferSRV("g_Materials", cfg.materialBuffer); + bsb.BufferSRV("g_VariantTextures", cfg.variantTexBuffer); + bsb.BufferSRV("g_InstanceData", cfg.instanceBuffer); + bsb.BufferSRV("g_CompactBatchIndices", p.batchIndicesBuffer); + bsb.BufferSRV("g_CompactMaterialIDs", p.materialIDsBuffer); + + if (cfg.lightDataBuffer) + bsb.BufferSRV("g_LightData", cfg.lightDataBuffer); + if (cfg.clusterGridBuffer) + bsb.BufferSRV("g_ClusterGrid", cfg.clusterGridBuffer); + if (cfg.lightIndexListBuffer) + bsb.BufferSRV("g_LightIndexList", cfg.lightIndexListBuffer); + + // CSM is fixed-register in bindless_forward; water has no shadow map. + if (xr_strcmp(psName, "water") != 0) + { + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = cfg.shadowCascades[i] ? cfg.shadowCascades[i] + : (i == 0 && cfg.shadowMapArray ? cfg.shadowMapArray : dummy2D); + if (t) + bsb.Texture(kNames[i], t); + } + nvrhi::ITexture* localAtlas = cfg.localShadowAtlas + ? cfg.localShadowAtlas + : framegraph::GetPassResourceCache().GetDummyShadowMap(nvDevice); + if (localAtlas) + bsb.Texture("g_LocalShadowAtlas", localAtlas); + } + if (sky0) + bsb.Texture("s_env0", sky0); + if (sky1) + bsb.Texture("s_env1", sky1); + + auto bindingSet = smpCache.GetOrCreateBindingSet(bsb.Build(), layout, nvDevice); + if (!bindingSet) + { + Msg("! [VariantPartition] createBindingSet failed for variant %u (%s/%s)", + v, vsName, psName); + continue; + } state.pipeline = pso; state.bindings = { bindingSet }; diff --git a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h index 0a7d11eb219..7b9e09593c7 100644 --- a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h +++ b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h @@ -74,6 +74,14 @@ struct VariantPartitionDrawConfig nvrhi::IBuffer* variantTexBuffer = nullptr; nvrhi::IBuffer* instanceBuffer = nullptr; nvrhi::IBuffer* megaVertexBuffer = nullptr; + nvrhi::ITexture* shadowMapArray = nullptr; + nvrhi::ITexture* shadowCascades[3] = {}; + nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* envSky0 = nullptr; + nvrhi::ITexture* envSky1 = nullptr; + nvrhi::IBuffer* lightDataBuffer = nullptr; + nvrhi::IBuffer* clusterGridBuffer = nullptr; + nvrhi::IBuffer* lightIndexListBuffer = nullptr; VariantPartitionConfig partition; bool selectTransparent = false; }; diff --git a/src/Layers/xrRender/Shaders/SlangCompiler.cpp b/src/Layers/xrRender/Shaders/SlangCompiler.cpp index dcbeabb8f05..11ff4b36d7f 100644 --- a/src/Layers/xrRender/Shaders/SlangCompiler.cpp +++ b/src/Layers/xrRender/Shaders/SlangCompiler.cpp @@ -253,9 +253,20 @@ SlangCompiler::CompileResult SlangCompiler::CompileFromSource( Slang::ComPtr codeBlob; slangResult = linkedProgram->getEntryPointCode(0, 0, codeBlob.writeRef(), diagnosticBlob.writeRef()); + if (diagnosticBlob) + { + auto* msg = static_cast(diagnosticBlob->getBufferPointer()); + if (msg && msg[0]) + { + result.errorMessage = msg; + Msg("! [SlangCompiler] Codegen errors for %s: %s", sourcePath, msg); + } + diagnosticBlob = nullptr; + } if (SLANG_FAILED(slangResult) || !codeBlob) { - result.errorMessage = "Failed to retrieve compiled bytecode"; + if (result.errorMessage.empty()) + result.errorMessage = "Failed to retrieve compiled bytecode"; Msg("! [SlangCompiler] %s", result.errorMessage.c_str()); return result; } diff --git a/src/Layers/xrRender/Volumetrics/IVolumetricSource.h b/src/Layers/xrRender/Volumetrics/IVolumetricSource.h new file mode 100644 index 00000000000..09dd07c5a8a --- /dev/null +++ b/src/Layers/xrRender/Volumetrics/IVolumetricSource.h @@ -0,0 +1,30 @@ +#pragma once + +#include "xrCore/xrCore.h" +#include + +namespace xray::render::fg +{ + +struct FroxelContribution +{ + float density = 0.f; + float extinction = 0.f; + Fcolor albedo{}; + float anisotropy = 0.f; +}; + +class IVolumetricSource +{ +public: + virtual ~IVolumetricSource() = default; + + // Upload / refresh GPU-side params (MVP: may only fill CPU struct consumed by renderer CB) + virtual void PrepareGPUData() = 0; + + virtual const char* GetShaderName() const = 0; + virtual void GetWorldBounds(Fvector& outMin, Fvector& outMax) const = 0; + virtual const char* GetTypeName() const = 0; +}; + +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/Volumetrics/VolumetricRenderer.cpp b/src/Layers/xrRender/Volumetrics/VolumetricRenderer.cpp new file mode 100644 index 00000000000..572fc77fced --- /dev/null +++ b/src/Layers/xrRender/Volumetrics/VolumetricRenderer.cpp @@ -0,0 +1,100 @@ +#include "stdafx.h" +#include "VolumetricRenderer.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" + +namespace xray::render::fg +{ + +namespace +{ +nvrhi::TextureHandle CreateFroxelTex(nvrhi::IDevice* nv, const char* name) +{ + nvrhi::TextureDesc desc; + desc.debugName = name; + desc.dimension = nvrhi::TextureDimension::Texture3D; + desc.width = VolumetricRenderer::kFroxelX; + desc.height = VolumetricRenderer::kFroxelY; + desc.depth = VolumetricRenderer::kFroxelZ; + desc.format = nvrhi::Format::RGBA16_FLOAT; + desc.mipLevels = 1; + desc.isUAV = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + return nv->createTexture(desc); +} +} // namespace + +void VolumetricRenderer::Initialize(RenderDevice* device) +{ + if (m_initialized) + return; + + m_device = device; + nvrhi::IDevice* nv = device ? device->GetNVRHIDevice() : nullptr; + if (!nv) + { + Msg("! [VolumetricRenderer] No NVRHI device"); + return; + } + + m_froxelVolume = CreateFroxelTex(nv, "FroxelVolume"); + m_prevFroxelVolume = CreateFroxelTex(nv, "FroxelVolume_Prev"); + if (!m_froxelVolume) + { + Msg("! [VolumetricRenderer] Failed to create froxel volume"); + return; + } + + ClearSources(); + RegisterSource(&m_worldFog); + + m_initialized = true; + m_hasHistory = false; + Msg("* [VolumetricRenderer] Initialized froxel volume %ux%ux%u (+temporal)", + kFroxelX, kFroxelY, kFroxelZ); +} + +void VolumetricRenderer::Shutdown() +{ + m_sources.clear(); + m_froxelVolume = nullptr; + m_prevFroxelVolume = nullptr; + m_device = nullptr; + m_initialized = false; + m_hasHistory = false; +} + +void VolumetricRenderer::RegisterSource(IVolumetricSource* source) +{ + if (!source) + return; + for (auto* s : m_sources) + { + if (s == source) + return; + } + m_sources.push_back(source); +} + +void VolumetricRenderer::ClearSources() +{ + m_sources.clear(); +} + +void VolumetricRenderer::BeginFrame() +{ + for (auto* s : m_sources) + { + if (s) + s->PrepareGPUData(); + } +} + +void VolumetricRenderer::SwapFroxelHistory() +{ + // Mark that a complete lit volume exists for next-frame temporal. + // Actual copy is done in VolumetricPass execute (GPU copyTexture). + m_hasHistory = (m_froxelVolume != nullptr && m_prevFroxelVolume != nullptr); +} + +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/Volumetrics/VolumetricRenderer.h b/src/Layers/xrRender/Volumetrics/VolumetricRenderer.h new file mode 100644 index 00000000000..ccc4806c70f --- /dev/null +++ b/src/Layers/xrRender/Volumetrics/VolumetricRenderer.h @@ -0,0 +1,51 @@ +#pragma once + +#include "IVolumetricSource.h" +#include "WorldFogSource.h" +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include + +namespace xray::render::framegraph +{ +class FrameGraph; +} + +namespace xray::render::fg +{ +class RenderDevice; + +class VolumetricRenderer +{ +public: + static constexpr u32 kFroxelX = 160; + static constexpr u32 kFroxelY = 90; + static constexpr u32 kFroxelZ = 16; + + void Initialize(RenderDevice* device); + void Shutdown(); + + void RegisterSource(IVolumetricSource* source); + void ClearSources(); + void BeginFrame(); // refresh WorldFog from Environment + void SwapFroxelHistory(); // after frame: current → prev + + WorldFogSource& GetWorldFog() { return m_worldFog; } + const WorldFogSource& GetWorldFog() const { return m_worldFog; } + nvrhi::ITexture* GetFroxelVolume() const { return m_froxelVolume.Get(); } + nvrhi::ITexture* GetPrevFroxelVolume() const { return m_prevFroxelVolume.Get(); } + bool HasFroxelHistory() const { return m_hasHistory && m_prevFroxelVolume; } + bool IsReady() const { return m_initialized && m_froxelVolume; } + + RenderDevice* GetDevice() const { return m_device; } + +private: + RenderDevice* m_device = nullptr; + bool m_initialized = false; + bool m_hasHistory = false; + nvrhi::TextureHandle m_froxelVolume; + nvrhi::TextureHandle m_prevFroxelVolume; + WorldFogSource m_worldFog; + xr_vector m_sources; +}; + +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/Volumetrics/WorldFogSource.cpp b/src/Layers/xrRender/Volumetrics/WorldFogSource.cpp new file mode 100644 index 00000000000..9070f400990 --- /dev/null +++ b/src/Layers/xrRender/Volumetrics/WorldFogSource.cpp @@ -0,0 +1,34 @@ +#include "stdafx.h" +#include "WorldFogSource.h" +#include "xrEngine/IGame_Persistent.h" +#include "xrEngine/Environment.h" + +namespace xray::render::fg +{ + +void WorldFogSource::PrepareGPUData() +{ + if (!g_pGamePersistent) + return; + + const auto& env = g_pGamePersistent->Environment().CurrentEnv; + // env.fog_density is a unitless 0..1 blend factor (drives fog_near), NOT an + // extinction coefficient. Map it to a soft physical density so a full-length + // sky ray does not crush transmittance to ~0 (black sky). + const float fogFar = std::max(env.fog_distance, 50.f); + const float density01 = std::min(std::max(env.fog_density, 0.f), 1.f); + // Softer than MVP: optical depth ~0.2 at fogFar (shafts carry volumetric look) + const float targetOD = 0.2f * density01; + m_params.density = targetOD / fogFar; + m_params.heightFalloff = 0.01f + density01 * 0.015f; + m_params.baseHeight = 0.0f; + m_params.albedo.set(env.fog_color.x, env.fog_color.y, env.fog_color.z); +} + +void WorldFogSource::GetWorldBounds(Fvector& outMin, Fvector& outMax) const +{ + outMin.set(-FLT_MAX, -FLT_MAX, -FLT_MAX); + outMax.set(FLT_MAX, FLT_MAX, FLT_MAX); +} + +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/Volumetrics/WorldFogSource.h b/src/Layers/xrRender/Volumetrics/WorldFogSource.h new file mode 100644 index 00000000000..16366560f65 --- /dev/null +++ b/src/Layers/xrRender/Volumetrics/WorldFogSource.h @@ -0,0 +1,30 @@ +#pragma once + +#include "IVolumetricSource.h" + +namespace xray::render::fg +{ + +struct WorldFogGPUParams +{ + float density = 0.02f; + float heightFalloff = 0.05f; + float baseHeight = 0.f; + Fvector albedo{0.7f, 0.75f, 0.85f}; +}; + +class WorldFogSource : public IVolumetricSource +{ +public: + void PrepareGPUData() override; + const char* GetShaderName() const override { return "volumetric\\world_fog"; } + void GetWorldBounds(Fvector& outMin, Fvector& outMax) const override; + const char* GetTypeName() const override { return "WorldFog"; } + + const WorldFogGPUParams& GetParams() const { return m_params; } + +private: + WorldFogGPUParams m_params; +}; + +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/fgEnvironmentRender.cpp b/src/Layers/xrRender/fgEnvironmentRender.cpp index 1771441d481..0310b3f0bc1 100644 --- a/src/Layers/xrRender/fgEnvironmentRender.cpp +++ b/src/Layers/xrRender/fgEnvironmentRender.cpp @@ -335,6 +335,13 @@ void FGEnvironmentRender::DrawSky(nvrhi::ICommandList* cmdList, nvrhi::IFramebuf auto dynamicCBBuffer = cache.GetOrCreateVolatileCB("FGEnv_Sky", "DynamicCB", sizeof(passes::DynamicTransforms), renderDevice); cmdList->writeBuffer(dynamicCBBuffer, &dynamicCB, sizeof(dynamicCB)); + // Horizon fog matched to distance-fog color so terrain→sky has no hard seam. + struct SkyFogCB { Fvector4 color; Fvector4 params; } skyFog; + skyFog.color.set(env.fog_color.x, env.fog_color.y, env.fog_color.z, 0.0f); + skyFog.params.set(0.22f, 1.0f, 0.0f, 0.0f); // band height, strength + auto skyFogCB = cache.GetOrCreateVolatileCB("FGEnv_Sky", "SkyFogCB", sizeof(SkyFogCB), renderDevice); + cmdList->writeBuffer(skyFogCB, &skyFog, sizeof(skyFog)); + nvrhi::ITexture* sky0Tex = nullptr; nvrhi::ITexture* sky1Tex = nullptr; auto* texManager = renderDevice->GetFGResourceManager() @@ -358,6 +365,7 @@ void FGEnvironmentRender::DrawSky(nvrhi::ICommandList* cmdList, nvrhi::IFramebuf framegraph::BindingSetBuilder bsb(*vsRefl, *psRefl, m_device, "FGEnv_Sky"); bsb.ConstantBuffer("dynamic_transforms", dynamicCBBuffer); + bsb.ConstantBuffer("SkyFog", skyFogCB); bsb.Texture("s_sky0", sky0Tex); bsb.Texture("s_sky1", sky1Tex); @@ -468,8 +476,10 @@ void FGEnvironmentRender::InitSunResources() renderState.blendState.targets[0].setSrcBlend(nvrhi::BlendFactor::One); renderState.blendState.targets[0].setDestBlend(nvrhi::BlendFactor::One); renderState.blendState.targets[0].setBlendOp(nvrhi::BlendOp::Add); - renderState.depthStencilState.setDepthTestEnable(false); + // Depth-test so mountains/objects occlude the disc (classic). No depth write. + renderState.depthStencilState.setDepthTestEnable(true); renderState.depthStencilState.setDepthWriteEnable(false); + renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); nvrhi::GraphicsPipelineDesc pipelineDesc; @@ -482,6 +492,7 @@ void FGEnvironmentRender::InitSunResources() nvrhi::FramebufferInfoEx fbInfo; fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.depthFormat = nvrhi::Format::D32; m_sunPipeline = cache.GetOrCreatePipeline("FGEnv_Sun", pipelineDesc, fbInfo, m_device); R_ASSERT(m_sunPipeline); diff --git a/src/Layers/xrRender/fgRainRender.cpp b/src/Layers/xrRender/fgRainRender.cpp index 4a7749ab88d..4aeac28e8d6 100644 --- a/src/Layers/xrRender/fgRainRender.cpp +++ b/src/Layers/xrRender/fgRainRender.cpp @@ -20,6 +20,7 @@ namespace constexpr float kDropWidth = 0.30f; constexpr int kMaxDesired = 2500; constexpr int kParticleCacheLimit = 400; + constexpr float kParticlesTime = 0.3f; } FGRainRender::FGRainRender() @@ -51,15 +52,36 @@ void FGRainRender::InitResources() m_streakTexture = textureManager->GetNVRHITexture(textureManager->LoadTexture("fx" DELIMITER "fx_rain")); R_ASSERT2(m_streakTexture, "FGRainRender: failed to load fx/fx_rain texture"); + // Splash: prefer detail-model texture, else SSFX rain_splash, else streak + m_splashTexture = m_streakTexture; + if (m_dropModel && m_dropModel->textureName.size()) + { + auto h = textureManager->LoadTexture(*m_dropModel->textureName); + if (auto* t = textureManager->GetNVRHITexture(h)) + m_splashTexture = t; + } + if (m_splashTexture == m_streakTexture) + { + auto h = textureManager->LoadTexture("fx" DELIMITER "rain_splash"); + if (auto* t = textureManager->GetNVRHITexture(h)) + m_splashTexture = t; + else + { + h = textureManager->LoadTexture("fx" DELIMITER "rain_drop"); + if (auto* t = textureManager->GetNVRHITexture(h)) + m_splashTexture = t; + } + } + auto* shaderLoader = RImplementation.GetShaderLoader(); R_ASSERT(shaderLoader); - auto vsResult = shaderLoader->LoadVertexShader("effects_world_textured", "main"); - R_ASSERT2(vsResult.handle, "FGRainRender: failed to load effects_world_textured.vs"); + auto vsResult = shaderLoader->LoadVertexShader("effects_world_soft", "main"); + R_ASSERT2(vsResult.handle, "FGRainRender: failed to load effects_world_soft.vs"); m_vs = vsResult.handle; - auto psResult = shaderLoader->LoadPixelShader("effects_world_textured", "main"); - R_ASSERT2(psResult.handle, "FGRainRender: failed to load effects_world_textured.ps"); + auto psResult = shaderLoader->LoadPixelShader("effects_world_soft", "main"); + R_ASSERT2(psResult.handle, "FGRainRender: failed to load effects_world_soft.ps"); m_ps = psResult.handle; nvrhi::VertexAttributeDesc vertexAttrs[] = { @@ -71,11 +93,11 @@ void FGRainRender::InitResources() R_ASSERT2(m_inputLayout, "FGRainRender: createInputLayout failed"); nvrhi::BufferDesc cbDesc; - cbDesc.byteSize = sizeof(passes::DynamicTransforms); + cbDesc.byteSize = sizeof(passes::SoftFXConstants); cbDesc.isConstantBuffer = true; cbDesc.isVolatile = true; cbDesc.maxVersions = 16; - cbDesc.debugName = "FGRainRender_CB"; + cbDesc.debugName = "FGRainRender_SoftCB"; m_constantBuffer = m_device->createBuffer(cbDesc); R_ASSERT2(m_constantBuffer, "FGRainRender: createBuffer(CB) failed"); @@ -90,20 +112,12 @@ void FGRainRender::InitResources() bindingLayoutDesc.bindings = { nvrhi::BindingLayoutItem::VolatileConstantBuffer(0), nvrhi::BindingLayoutItem::Texture_SRV(0), + nvrhi::BindingLayoutItem::Texture_SRV(1), nvrhi::BindingLayoutItem::Sampler(0), }; m_bindingLayout = m_device->createBindingLayout(bindingLayoutDesc); R_ASSERT2(m_bindingLayout, "FGRainRender: createBindingLayout failed"); - nvrhi::BindingSetDesc bindingSetDesc; - bindingSetDesc.bindings = { - nvrhi::BindingSetItem::ConstantBuffer(0, m_constantBuffer), - nvrhi::BindingSetItem::Texture_SRV(0, m_streakTexture), - nvrhi::BindingSetItem::Sampler(0, m_sampler), - }; - m_bindingSet = m_device->createBindingSet(bindingSetDesc, m_bindingLayout); - R_ASSERT2(m_bindingSet, "FGRainRender: createBindingSet failed at init"); - nvrhi::FramebufferInfo fbInfo; fbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); fbInfo.setDepthFormat(nvrhi::Format::D32); @@ -116,8 +130,10 @@ void FGRainRender::InitResources() pipelineDesc.bindingLayouts = { m_bindingLayout }; pipelineDesc.primType = nvrhi::PrimitiveType::TriangleList; pipelineDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + // Soft occlusion is primary; HW depth is a backup. pipelineDesc.renderState.depthStencilState.depthTestEnable = true; pipelineDesc.renderState.depthStencilState.depthWriteEnable = false; + pipelineDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; pipelineDesc.renderState.blendState.targets[0] .setBlendEnable(true) .setSrcBlend(nvrhi::BlendFactor::SrcAlpha) @@ -127,10 +143,28 @@ void FGRainRender::InitResources() m_pipeline = m_device->createGraphicsPipeline(pipelineDesc, fbInfo); R_ASSERT2(m_pipeline, "FGRainRender: createGraphicsPipeline failed"); + + if (renderDevice) + m_gpuSimReady = m_gpuSim.Initialize(renderDevice); + + Msg("* [FGRainRender] Soft-depth rain pipeline ready (GPU sim=%s)", + m_gpuSimReady ? "on" : "off"); } void FGRainRender::Copy(IRainRender& _in) { *this = *static_cast(&_in); } +void FGRainRender::SetRainShadowInputs(nvrhi::ITexture* rainSM, const Fmatrix& sampleVP, bool valid) +{ + m_rainShadow = rainSM; + m_rainSampleVP = sampleVP; + m_rainSMValid = valid; +} + +bool FGRainRender::HasWork() const +{ + return m_rainDensity > EPS_L || !m_splashBatches.empty() || !m_quadBatches.empty(); +} + const Fsphere& FGRainRender::GetDropBounds() const { static Fsphere fallback; @@ -142,18 +176,94 @@ void FGRainRender::Clear() m_vertices.clear(); m_indices.clear(); m_quadBatches.clear(); + m_splashBatches.clear(); + m_desiredParticles = 0; + m_rainDensity = 0.f; } -void FGRainRender::Render(CEffect_Rain& owner) +void FGRainRender::AppendSplashParticles(CEffect_Rain& owner, u32 rainColor) { - Clear(); - if (!m_dropModel) return; + if (!m_dropModel || m_dropModel->number_vertices == 0 || m_dropModel->number_indices == 0) + return; - const float factor = g_pGamePersistent->Environment().CurrentEnv.rain_density; - if (factor < EPS_L) return; + const float dt = Device.fTimeDelta; + CEffect_Rain::Particle* P = owner.particle_active; + if (!P) + return; + + Fmatrix mScale, mXform; + int pcount = 0; + u32 splashIndexStart = static_cast(m_indices.size()); + + auto flushSplashBatch = [&]() { + const u32 splashIndexCount = static_cast(m_indices.size()) - splashIndexStart; + if (splashIndexCount > 0) + { + Batch b{}; + b.indexOffset = splashIndexStart; + b.indexCount = splashIndexCount; + m_splashBatches.push_back(b); + } + splashIndexStart = static_cast(m_indices.size()); + pcount = 0; + }; + + while (P) + { + CEffect_Rain::Particle* next = P->next; + + P->time -= dt; + if (P->time < 0.f) + { + owner.p_free(P); + P = next; + continue; + } + + if (RImplementation.ViewBase.testSphere_dirty(P->bounds.P, P->bounds.R)) + { + const float scale = P->time / kParticlesTime; + mScale.scale(scale, scale, scale); + mXform.mul_43(P->mXForm, mScale); + + const u32 vBase = static_cast(m_vertices.size()); + // Stay within 16-bit index range for absolute indices + if (vBase + m_dropModel->number_vertices > 65000) + { + flushSplashBatch(); + // Drop remaining if still over (extreme); rare with pool=1000 + if (m_vertices.size() + m_dropModel->number_vertices > 65000) + { + P = next; + continue; + } + } - const u32 desired = iFloor(0.5f * (1.f + factor) * float(kMaxDesired)); + const u32 iBase = static_cast(m_indices.size()); + m_vertices.resize(vBase + m_dropModel->number_vertices); + m_indices.resize(iBase + m_dropModel->number_indices); + m_dropModel->transfer( + mXform, + reinterpret_cast(m_vertices.data() + vBase), + rainColor, + m_indices.data() + iBase, + vBase); + + ++pcount; + if (pcount >= kParticleCacheLimit) + flushSplashBatch(); + } + + P = next; + } + + flushSplashBatch(); +} + +void FGRainRender::SimulateRainItems(CEffect_Rain& owner) +{ + const u32 desired = m_desiredParticles; if (owner.items.size() < desired) { owner.items.reserve(desired); @@ -165,30 +275,33 @@ void FGRainRender::Render(CEffect_Rain& owner) } } - const float factor_visual = factor / 2.f + .5f; - const Fvector3 fc = g_pGamePersistent->Environment().CurrentEnv.rain_color; - const u32 col = color_rgba_f(fc.x, fc.y, fc.z, factor_visual); const float radius_wrap_sqr = _sqr(kSourceRadius + .5f); - Fplane src_plane; Fvector norm = {0.f, -1.f, 0.f}; Fvector upper; upper.set(Device.vCameraPosition.x, Device.vCameraPosition.y + kSourceOffset, Device.vCameraPosition.z); src_plane.build(upper, norm); - static Fvector2 UV[2][4] = {{{0,1},{0,0},{1,1},{1,0}},{{1,0},{1,1},{0,0},{0,1}}}; - const Fvector& vEye = Device.vCameraPosition; - const u32 quadIndexStart = static_cast(m_indices.size()); for (u32 I = 0; I < desired; I++) { CEffect_Rain::Item& one = owner.items[I]; - if (one.dwTime_Hit < Device.dwTimeGlobal) owner.Hit(one.Phit); - if (one.dwTime_Life < Device.dwTimeGlobal) owner.Born(one, kSourceRadius); + if (one.dwTime_Hit < Device.dwTimeGlobal) + owner.Hit(one.Phit); + if (one.dwTime_Life < Device.dwTimeGlobal) + owner.Born(one, kSourceRadius); + + one.P.mad(one.D, one.fSpeed * Device.fTimeDelta); + + if ((one.P.y - vEye.y) < kSinkOffset) + { + owner.Hit(one.P); + one.invalidate(); + owner.Born(one, kSourceRadius); + continue; + } - float dt = Device.fTimeDelta; - one.P.mad(one.D, one.fSpeed * dt); Fvector wdir; wdir.set(one.P.x - vEye.x, 0, one.P.z - vEye.z); float wlen = wdir.square_magnitude(); @@ -216,21 +329,44 @@ void FGRainRender::Render(CEffect_Rain& owner) else one.invalidate(); } } + } +} + +void FGRainRender::BuildCPUStreaks(CEffect_Rain& owner) +{ + const u32 desired = m_desiredParticles; + SimulateRainItems(owner); + static Fvector2 UV[2][4] = {{{0,1},{0,0},{1,1},{1,0}},{{1,0},{1,1},{0,0},{0,1}}}; + const Fvector& vEye = Device.vCameraPosition; + const u32 quadIndexStart = static_cast(m_indices.size()); + + for (u32 I = 0; I < desired; I++) + { + CEffect_Rain::Item& one = owner.items[I]; Fvector& pos_head = one.P; Fvector pos_trail; - pos_trail.mad(pos_head, one.D, -kDropLength * factor_visual); + pos_trail.mad(pos_head, one.D, -kDropLength * m_factorVisual); Fvector sC, lineD; - float sR; sC.sub(pos_head, pos_trail); lineD.normalize(sC); sC.mul(.5f); - sR = sC.magnitude(); sC.add(pos_trail); - if (!RImplementation.ViewBase.testSphere_dirty(sC, sR)) + if (!RImplementation.ViewBase.testSphere_dirty(sC, sC.magnitude())) continue; + Fvector sky = pos_head; + sky.y += 80.f; + Fvector down(0.f, -1.f, 0.f); + float coverRange = 80.f; + if (owner.RayPick(sky, down, coverRange, collide::rqtBoth)) + { + const float hitY = sky.y - coverRange; + if (hitY > pos_head.y + 0.35f) + continue; + } + Fvector lineTop, camDir; camDir.sub(sC, vEye); camDir.normalize(); @@ -241,13 +377,13 @@ void FGRainRender::Render(CEffect_Rain& owner) const u32 base = static_cast(m_vertices.size()); P.mad(pos_trail, lineTop, -w); - m_vertices.push_back({P.x, P.y, P.z, col, UV[s][0].x, UV[s][0].y}); + m_vertices.push_back({P.x, P.y, P.z, m_rainColor, UV[s][0].x, UV[s][0].y}); P.mad(pos_trail, lineTop, w); - m_vertices.push_back({P.x, P.y, P.z, col, UV[s][1].x, UV[s][1].y}); + m_vertices.push_back({P.x, P.y, P.z, m_rainColor, UV[s][1].x, UV[s][1].y}); P.mad(pos_head, lineTop, -w); - m_vertices.push_back({P.x, P.y, P.z, col, UV[s][2].x, UV[s][2].y}); + m_vertices.push_back({P.x, P.y, P.z, m_rainColor, UV[s][2].x, UV[s][2].y}); P.mad(pos_head, lineTop, w); - m_vertices.push_back({P.x, P.y, P.z, col, UV[s][3].x, UV[s][3].y}); + m_vertices.push_back({P.x, P.y, P.z, m_rainColor, UV[s][3].x, UV[s][3].y}); m_indices.push_back(static_cast(base + 0)); m_indices.push_back(static_cast(base + 1)); @@ -267,6 +403,32 @@ void FGRainRender::Render(CEffect_Rain& owner) } } +void FGRainRender::Render(CEffect_Rain& owner) +{ + Clear(); + + const float factor = g_pGamePersistent->Environment().CurrentEnv.rain_density; + m_rainDensity = factor; + if (factor < EPS_L) + return; + + m_desiredParticles = iFloor(0.5f * (1.f + factor) * float(kMaxDesired)); + m_factorVisual = factor / 2.f + .5f; + const Fvector3 fc = g_pGamePersistent->Environment().CurrentEnv.rain_color; + m_rainColor = color_rgba_f(fc.x, fc.y, fc.z, m_factorVisual); + + // GPU streaks + rain-SM cover; CPU keeps item sim for splash particles. + if (m_gpuSimReady) + SimulateRainItems(owner); + else if (m_dropModel) + BuildCPUStreaks(owner); + else + SimulateRainItems(owner); + + if (m_dropModel) + AppendSplashParticles(owner, m_rainColor); +} + void FGRainRender::EnsureGeometryCapacity(size_t vertexCount, size_t indexCount) { if (vertexCount > m_vertexCapacity || !m_vertexBuffer) @@ -289,32 +451,111 @@ void FGRainRender::EnsureGeometryCapacity(size_t vertexCount, size_t indexCount) } } -void FGRainRender::Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer) +void FGRainRender::DrawGPUStreaks(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, + nvrhi::ITexture* sceneWorldPos) { - EnsureGeometryCapacity(m_vertices.size(), m_indices.size()); + if (!m_streakTexture || !sceneWorldPos || !m_pipeline || !m_bindingLayout) + return; - if (!m_vertices.empty()) - cmdList->writeBuffer(m_vertexBuffer, m_vertices.data(), m_vertices.size() * sizeof(Vertex)); - if (!m_indices.empty()) - cmdList->writeBuffer(m_indexBuffer, m_indices.data(), m_indices.size() * sizeof(u16)); + nvrhi::IBuffer* vb = m_gpuSim.GetVertexBuffer(); + nvrhi::IBuffer* drawArgs = m_gpuSim.GetDrawArgsBuffer(); + if (!vb || !drawArgs) + return; - passes::DynamicTransforms cb{}; - passes::FillDynamicTransforms(cb); - cmdList->writeBuffer(m_constantBuffer, &cb, sizeof(cb)); + nvrhi::BindingSetDesc bindingSetDesc; + bindingSetDesc.bindings = { + nvrhi::BindingSetItem::ConstantBuffer(0, m_constantBuffer), + nvrhi::BindingSetItem::Texture_SRV(0, m_streakTexture), + nvrhi::BindingSetItem::Texture_SRV(1, sceneWorldPos), + nvrhi::BindingSetItem::Sampler(0, m_sampler), + }; + nvrhi::BindingSetHandle bindingSet = m_device->createBindingSet(bindingSetDesc, m_bindingLayout); + if (!bindingSet) + return; + + cmdList->setBufferState(vb, nvrhi::ResourceStates::VertexBuffer); + cmdList->setBufferState(drawArgs, nvrhi::ResourceStates::IndirectArgument); - cmdList->setBufferState(m_vertexBuffer, nvrhi::ResourceStates::VertexBuffer); - cmdList->setBufferState(m_indexBuffer, nvrhi::ResourceStates::IndexBuffer); + nvrhi::VertexBufferBinding vertexBinding; + vertexBinding.buffer = vb; + vertexBinding.slot = 0; + vertexBinding.offset = 0; + + const auto& fbInfo = framebuffer->getFramebufferInfo(); + + nvrhi::GraphicsState state; + state.pipeline = m_pipeline; + state.framebuffer = framebuffer; + state.bindings = { bindingSet }; + state.vertexBuffers = { vertexBinding }; + state.indexBuffer = {}; + state.indirectParams = drawArgs; + state.viewport = nvrhi::ViewportState().addViewportAndScissorRect( + nvrhi::Viewport(static_cast(fbInfo.width), static_cast(fbInfo.height))); + cmdList->setGraphicsState(state); + cmdList->drawIndirect(0); +} + +void FGRainRender::Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, + nvrhi::ITexture* sceneWorldPos, + const passes::RainHeightmapInfo& heightmap) +{ + passes::SoftFXConstants cb{}; + passes::FillSoftFXConstants(cb); + cmdList->writeBuffer(m_constantBuffer, &cb, sizeof(cb)); cmdList->setBufferState(m_constantBuffer, nvrhi::ResourceStates::ConstantBuffer); - DrawBatches(cmdList, framebuffer, m_quadBatches, m_streakTexture); + (void)heightmap; + + if (m_gpuSimReady && m_desiredParticles > 0) + { + m_gpuSim.Dispatch(cmdList, m_rainShadow, m_rainSampleVP, m_rainSMValid, + m_desiredParticles, m_rainDensity, m_factorVisual, m_rainColor); + DrawGPUStreaks(cmdList, framebuffer, sceneWorldPos); + if (!m_splashBatches.empty()) + { + EnsureGeometryCapacity(m_vertices.size(), m_indices.size()); + if (!m_vertices.empty()) + cmdList->writeBuffer(m_vertexBuffer, m_vertices.data(), m_vertices.size() * sizeof(Vertex)); + if (!m_indices.empty()) + cmdList->writeBuffer(m_indexBuffer, m_indices.data(), m_indices.size() * sizeof(u16)); + cmdList->setBufferState(m_vertexBuffer, nvrhi::ResourceStates::VertexBuffer); + cmdList->setBufferState(m_indexBuffer, nvrhi::ResourceStates::IndexBuffer); + DrawBatches(cmdList, framebuffer, m_splashBatches, m_splashTexture, sceneWorldPos); + } + Clear(); + return; + } + + EnsureGeometryCapacity(m_vertices.size(), m_indices.size()); + if (!m_vertices.empty()) + cmdList->writeBuffer(m_vertexBuffer, m_vertices.data(), m_vertices.size() * sizeof(Vertex)); + if (!m_indices.empty()) + cmdList->writeBuffer(m_indexBuffer, m_indices.data(), m_indices.size() * sizeof(u16)); + cmdList->setBufferState(m_vertexBuffer, nvrhi::ResourceStates::VertexBuffer); + cmdList->setBufferState(m_indexBuffer, nvrhi::ResourceStates::IndexBuffer); + DrawBatches(cmdList, framebuffer, m_quadBatches, m_streakTexture, sceneWorldPos); + DrawBatches(cmdList, framebuffer, m_splashBatches, m_splashTexture, sceneWorldPos); Clear(); } void FGRainRender::DrawBatches(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, - const xr_vector& batches, nvrhi::ITexture* texture) + const xr_vector& batches, nvrhi::ITexture* colorTex, + nvrhi::ITexture* sceneWorldPos) { - if (!texture || batches.empty() || !m_bindingSet) + if (!colorTex || !sceneWorldPos || batches.empty() || !m_pipeline || !m_bindingLayout) + return; + + nvrhi::BindingSetDesc bindingSetDesc; + bindingSetDesc.bindings = { + nvrhi::BindingSetItem::ConstantBuffer(0, m_constantBuffer), + nvrhi::BindingSetItem::Texture_SRV(0, colorTex), + nvrhi::BindingSetItem::Texture_SRV(1, sceneWorldPos), + nvrhi::BindingSetItem::Sampler(0, m_sampler), + }; + nvrhi::BindingSetHandle bindingSet = m_device->createBindingSet(bindingSetDesc, m_bindingLayout); + if (!bindingSet) return; nvrhi::VertexBufferBinding vertexBinding; @@ -327,7 +568,7 @@ void FGRainRender::DrawBatches(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer nvrhi::GraphicsState state; state.pipeline = m_pipeline; state.framebuffer = framebuffer; - state.bindings = { m_bindingSet }; + state.bindings = { bindingSet }; state.vertexBuffers = { vertexBinding }; state.indexBuffer.buffer = m_indexBuffer; state.indexBuffer.format = nvrhi::Format::R16_UINT; diff --git a/src/Layers/xrRender/fgRainRender.h b/src/Layers/xrRender/fgRainRender.h index 1c3de80f349..549664bba6d 100644 --- a/src/Layers/xrRender/fgRainRender.h +++ b/src/Layers/xrRender/fgRainRender.h @@ -3,6 +3,7 @@ #include "Include/xrRender/RainRender.h" #include "xrEngine/Rain.h" #include "Layers/xrRender/IRenderDetailModel.h" +#include "Layers/xrRender/FrameGraphPasses/RainGPUSimManager.h" #include namespace xray::render::fg @@ -16,6 +17,7 @@ class FGRainRender : public IRainRender u32 color; float u, v; }; + static_assert(sizeof(Vertex) == sizeof(IRender_DetailModel::fvfVertexOut)); struct Batch { @@ -30,31 +32,54 @@ class FGRainRender : public IRainRender void Render(CEffect_Rain& owner) override; const Fsphere& GetDropBounds() const override; - bool HasWork() const { return !m_quadBatches.empty(); } + bool HasWork() const; void Clear(); - void Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer); + void SetRainShadowInputs(nvrhi::ITexture* rainSM, const Fmatrix& sampleVP, bool valid); + + void Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, + nvrhi::ITexture* sceneWorldPos, + const passes::RainHeightmapInfo& heightmap); private: void InitResources(); void EnsureGeometryCapacity(size_t vertexCount, size_t indexCount); void DrawBatches(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, - const xr_vector& batches, nvrhi::ITexture* texture); + const xr_vector& batches, nvrhi::ITexture* colorTex, + nvrhi::ITexture* sceneWorldPos); + void AppendSplashParticles(CEffect_Rain& owner, u32 rainColor); + void SimulateRainItems(CEffect_Rain& owner); + void BuildCPUStreaks(CEffect_Rain& owner); + void DrawGPUStreaks(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, + nvrhi::ITexture* sceneWorldPos); IRender_DetailModel* m_dropModel = nullptr; xr_vector m_vertices; xr_vector m_indices; xr_vector m_quadBatches; + xr_vector m_splashBatches; + + passes::RainGPUSimManager m_gpuSim; + bool m_gpuSimReady = false; + + u32 m_desiredParticles = 0; + u32 m_rainColor = 0; + float m_factorVisual = 1.f; + float m_rainDensity = 0.f; + + nvrhi::ITexture* m_rainShadow = nullptr; + Fmatrix m_rainSampleVP; + bool m_rainSMValid = false; nvrhi::IDevice* m_device = nullptr; nvrhi::TextureHandle m_streakTexture; + nvrhi::TextureHandle m_splashTexture; nvrhi::ShaderHandle m_vs; nvrhi::ShaderHandle m_ps; nvrhi::InputLayoutHandle m_inputLayout; nvrhi::SamplerHandle m_sampler; nvrhi::BindingLayoutHandle m_bindingLayout; - nvrhi::BindingSetHandle m_bindingSet; nvrhi::BufferHandle m_constantBuffer; nvrhi::BufferHandle m_vertexBuffer; nvrhi::BufferHandle m_indexBuffer; diff --git a/src/Layers/xrRender/fgThunderboltRender.cpp b/src/Layers/xrRender/fgThunderboltRender.cpp index d2660679ed2..1adcaa0d917 100644 --- a/src/Layers/xrRender/fgThunderboltRender.cpp +++ b/src/Layers/xrRender/fgThunderboltRender.cpp @@ -31,12 +31,12 @@ void FGThunderboltRender::InitResources() auto* shaderLoader = RImplementation.GetShaderLoader(); R_ASSERT(shaderLoader); - auto vsResult = shaderLoader->LoadVertexShader("effects_world_textured", "main"); - R_ASSERT2(vsResult.handle, "FGThunderboltRender: failed to load effects_world_textured.vs"); + auto vsResult = shaderLoader->LoadVertexShader("effects_world_soft", "main"); + R_ASSERT2(vsResult.handle, "FGThunderboltRender: failed to load effects_world_soft.vs"); m_vs = vsResult.handle; - auto psResult = shaderLoader->LoadPixelShader("effects_world_textured", "main"); - R_ASSERT2(psResult.handle, "FGThunderboltRender: failed to load effects_world_textured.ps"); + auto psResult = shaderLoader->LoadPixelShader("effects_world_soft", "main"); + R_ASSERT2(psResult.handle, "FGThunderboltRender: failed to load effects_world_soft.ps"); m_ps = psResult.handle; nvrhi::VertexAttributeDesc vertexAttrs[] = { @@ -48,11 +48,11 @@ void FGThunderboltRender::InitResources() R_ASSERT2(m_inputLayout, "FGThunderboltRender: createInputLayout failed"); nvrhi::BufferDesc cbDesc; - cbDesc.byteSize = sizeof(passes::DynamicTransforms); + cbDesc.byteSize = sizeof(passes::SoftFXConstants); cbDesc.isConstantBuffer = true; cbDesc.isVolatile = true; cbDesc.maxVersions = 16; - cbDesc.debugName = "FGThunderboltRender_CB"; + cbDesc.debugName = "FGThunderboltRender_SoftCB"; m_constantBuffer = m_device->createBuffer(cbDesc); R_ASSERT2(m_constantBuffer, "FGThunderboltRender: createBuffer(CB) failed"); @@ -67,6 +67,7 @@ void FGThunderboltRender::InitResources() bindingLayoutDesc.bindings = { nvrhi::BindingLayoutItem::VolatileConstantBuffer(0), nvrhi::BindingLayoutItem::Texture_SRV(0), + nvrhi::BindingLayoutItem::Texture_SRV(1), nvrhi::BindingLayoutItem::Sampler(0), }; m_bindingLayout = m_device->createBindingLayout(bindingLayoutDesc); @@ -96,6 +97,7 @@ void FGThunderboltRender::InitResources() m_pipeline = m_device->createGraphicsPipeline(pipelineDesc, fbInfo); R_ASSERT2(m_pipeline, "FGThunderboltRender: createGraphicsPipeline failed"); + Msg("* [FGThunderboltRender] Soft-depth thunderbolt pipeline ready"); } void FGThunderboltRender::Copy(IThunderboltRender& _in) @@ -282,9 +284,10 @@ void FGThunderboltRender::EnsureGeometryCapacity(size_t vertexCount, size_t inde } } -void FGThunderboltRender::Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer) +void FGThunderboltRender::Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, + nvrhi::ITexture* sceneWorldPos) { - if (m_batches.empty()) + if (m_batches.empty() || !sceneWorldPos || !m_pipeline || !m_bindingLayout) return; EnsureGeometryCapacity(m_vertices.size(), m_indices.size()); @@ -294,8 +297,8 @@ void FGThunderboltRender::Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer if (!m_indices.empty()) cmdList->writeBuffer(m_indexBuffer, m_indices.data(), m_indices.size() * sizeof(u16)); - passes::DynamicTransforms cb{}; - passes::FillDynamicTransforms(cb); + passes::SoftFXConstants cb{}; + passes::FillSoftFXConstants(cb); cmdList->writeBuffer(m_constantBuffer, &cb, sizeof(cb)); cmdList->setBufferState(m_vertexBuffer, nvrhi::ResourceStates::VertexBuffer); @@ -304,9 +307,6 @@ void FGThunderboltRender::Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer const auto& fbInfo = framebuffer->getFramebufferInfo(); - nvrhi::ITexture* currentTexture = nullptr; - nvrhi::BindingSetHandle currentBindingSet; - nvrhi::VertexBufferBinding vertexBinding; vertexBinding.buffer = m_vertexBuffer; vertexBinding.slot = 0; @@ -317,34 +317,27 @@ void FGThunderboltRender::Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer if (b.indexCount == 0 || !b.texture) continue; - if (b.texture != currentTexture) - { - auto it = m_bindingSetCache.find(b.texture); - if (it == m_bindingSetCache.end()) - { - nvrhi::BindingSetDesc bindingSetDesc; - bindingSetDesc.bindings = { - nvrhi::BindingSetItem::ConstantBuffer(0, m_constantBuffer), - nvrhi::BindingSetItem::Texture_SRV(0, b.texture), - nvrhi::BindingSetItem::Sampler(0, m_sampler), - }; - nvrhi::BindingSetHandle bs = m_device->createBindingSet(bindingSetDesc, m_bindingLayout); - R_ASSERT2(bs, "FGThunderboltRender: createBindingSet failed"); - it = m_bindingSetCache.emplace(b.texture, std::move(bs)).first; - } - currentBindingSet = it->second; - currentTexture = b.texture; - } + nvrhi::BindingSetDesc bindingSetDesc; + bindingSetDesc.bindings = { + nvrhi::BindingSetItem::ConstantBuffer(0, m_constantBuffer), + nvrhi::BindingSetItem::Texture_SRV(0, b.texture), + nvrhi::BindingSetItem::Texture_SRV(1, sceneWorldPos), + nvrhi::BindingSetItem::Sampler(0, m_sampler), + }; + nvrhi::BindingSetHandle bindingSet = m_device->createBindingSet(bindingSetDesc, m_bindingLayout); + if (!bindingSet) + continue; nvrhi::GraphicsState state; state.pipeline = m_pipeline; state.framebuffer = framebuffer; - state.bindings = { currentBindingSet }; + state.bindings = { bindingSet }; state.vertexBuffers = { vertexBinding }; state.indexBuffer.buffer = m_indexBuffer; state.indexBuffer.format = nvrhi::Format::R16_UINT; state.indexBuffer.offset = 0; - state.viewport = nvrhi::ViewportState().addViewportAndScissorRect(nvrhi::Viewport(static_cast(fbInfo.width), static_cast(fbInfo.height))); + state.viewport = nvrhi::ViewportState().addViewportAndScissorRect( + nvrhi::Viewport(static_cast(fbInfo.width), static_cast(fbInfo.height))); cmdList->setGraphicsState(state); nvrhi::DrawArguments args; diff --git a/src/Layers/xrRender/fgThunderboltRender.h b/src/Layers/xrRender/fgThunderboltRender.h index 93fa335ef5f..6277db72da7 100644 --- a/src/Layers/xrRender/fgThunderboltRender.h +++ b/src/Layers/xrRender/fgThunderboltRender.h @@ -37,7 +37,8 @@ class FGThunderboltRender : public IThunderboltRender void Clear(); - void Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer); + void Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, + nvrhi::ITexture* sceneWorldPos); private: void InitResources(); @@ -48,7 +49,6 @@ class FGThunderboltRender : public IThunderboltRender xr_vector m_indices; xr_vector m_batches; xr_map m_textureCache; - xr_unordered_map m_bindingSetCache; nvrhi::IDevice* m_device = nullptr; nvrhi::ShaderHandle m_vs; diff --git a/src/Layers/xrRender/fgUIRender.cpp b/src/Layers/xrRender/fgUIRender.cpp index 21b9d50beda..dbb571bff79 100644 --- a/src/Layers/xrRender/fgUIRender.cpp +++ b/src/Layers/xrRender/fgUIRender.cpp @@ -104,7 +104,11 @@ LPCSTR FGUIRender::UpdateShaderName(LPCSTR tex_name, LPCSTR sh_name) u32 v_dev = CAP_VERSION(caps.raster_major, caps.raster_minor); u32 v_need = CAP_VERSION(2, 0); - if ((v_dev >= v_need) && FS.exist(buff, "$game_textures$", tex_name, ".ogm")) + // Only switch to YUV movie shader when Theora packs YUV for the GPU path. + // If HWSupportsShaderYUV2RGB is false, frames are already RGB — using movie + // would mis-decode them as YUV (classic green cast). + if ((v_dev >= v_need) && GEnv.Render->HWSupportsShaderYUV2RGB() && + FS.exist(buff, "$game_textures$", tex_name, ".ogm")) return "hud" DELIMITER "movie"; return sh_name; } diff --git a/src/Layers/xrRender/light.cpp b/src/Layers/xrRender/light.cpp index 6dd7f2821aa..cc94c8e91e1 100644 --- a/src/Layers/xrRender/light.cpp +++ b/src/Layers/xrRender/light.cpp @@ -271,6 +271,38 @@ void light::xform_calc() Fvector cmNorm[6] = { { 0.f, 1.f, 0.f }, { 0.f, 1.f, 0.f }, { 0.f, 0.f, -1.f }, { 0.f, 0.f, 1.f }, { 0.f, 1.f, 0.f }, { 0.f, 1.f, 0.f } }; Fvector cmDir [6] = { { 1.f, 0.f, 0.f }, {-1.f, 0.f, 0.f }, { 0.f, 1.f, 0.f }, { 0.f,-1.f, 0.f }, { 0.f, 0.f, 1.f }, { 0.f, 0.f,-1.f } }; +void light::EnsureOmniparts() +{ + if (flags.type != IRender_Light::POINT) + return; + + if (nullptr == omnipart[0]) + for (auto& p_light : omnipart) + p_light = xr_new(); + + for (int f = 0; f < 6; f++) + { + light* L = omnipart[f]; + Fvector R; + R.crossproduct(cmNorm[f], cmDir[f]); + L->set_type(IRender_Light::OMNIPART); + L->set_shadow(flags.bShadow); + L->set_position(position); + L->set_rotation(cmDir[f], R); + L->set_cone(PI_DIV_2); + L->set_range(range); + L->set_virtual_size(virtual_size); + L->set_color(color); + L->spatial.sector_id = spatial.sector_id; + L->s_spot = s_spot; + L->s_point = s_point; + L->set_volumetric(flags.bVolumetric); + L->set_volumetric_quality(m_volumetric_quality); + L->set_volumetric_intensity(m_volumetric_intensity); + L->set_volumetric_distance(m_volumetric_distance); + } +} + void light::Export(light_Package& package) { if (flags.bShadow) @@ -279,52 +311,9 @@ void light::Export(light_Package& package) { case IRender_Light::POINT: { - // tough: create/update 6 shadowed lights - if (nullptr == omnipart[0]) - for (auto& p_light : omnipart) - p_light = xr_new(); + EnsureOmniparts(); for (int f = 0; f < 6; f++) - { - light* L = omnipart[f]; - Fvector R; - R.crossproduct(cmNorm[f], cmDir[f]); - L->set_type(IRender_Light::OMNIPART); - L->set_shadow(true); - L->set_position(position); - L->set_rotation(cmDir[f], R); - L->set_cone(PI_DIV_2); - L->set_range(range); - L->set_virtual_size(virtual_size); - L->set_color(color); - L->spatial.sector_id = spatial.sector_id; //. dangerous? - L->s_spot = s_spot; - L->s_point = s_point; - -#if 0 - if (RImplementation.o.msaa) - { - int bound = 1; - - if (!RImplementation.o.msaa_opt) - bound = RImplementation.o.msaa_samples; - - for (int i = 0; i < bound; ++i) - { - L->s_point_msaa[i] = s_point_msaa[i]; - L->s_spot_msaa[i] = s_spot_msaa[i]; - // L->s_volumetric_msaa[i] = s_volumetric_msaa[i]; - } - } -#endif // (RENDER==R_R3) || (RENDER==R_R4) || (RENDER==R_GL) - - // Igor: add volumetric support - L->set_volumetric(flags.bVolumetric); - L->set_volumetric_quality(m_volumetric_quality); - L->set_volumetric_intensity(m_volumetric_intensity); - L->set_volumetric_distance(m_volumetric_distance); - - package.v_shadowed.push_back(L); - } + package.v_shadowed.push_back(omnipart[f]); } break; case IRender_Light::SPOT: package.v_shadowed.push_back(this); break; diff --git a/src/Layers/xrRender/light.h b/src/Layers/xrRender/light.h index cffe58863e4..a015696be4d 100644 --- a/src/Layers/xrRender/light.h +++ b/src/Layers/xrRender/light.h @@ -159,6 +159,8 @@ class light : public IRender_Light, public SpatialBase void gi_generate(); void xform_calc(); void Export(light_Package& dest); + /// Ensure omnipart[6] faces exist and match this POINT light (for local shadow atlas). + void EnsureOmniparts(); void set_attenuation_params(float a0, float a1, float a2, float fo); #endif // (RENDER==R_R2) || (RENDER==R_R3) || (RENDER==R_R4) || (RENDER==R_GL) diff --git a/src/Layers/xrRender/r_FrameGraphRenderer.cpp b/src/Layers/xrRender/r_FrameGraphRenderer.cpp index 8e0ee308982..06ee7037732 100644 --- a/src/Layers/xrRender/r_FrameGraphRenderer.cpp +++ b/src/Layers/xrRender/r_FrameGraphRenderer.cpp @@ -25,12 +25,15 @@ #include "xrEngine/CustomHUD.h" #include "ImGuiRendererNVRHI.h" #include "xrEngine/device.h" +#include "xrCore/Threading/TaskManager.hpp" #include // Lambda-based pass setup functions #include "FrameGraphPasses/DebugDrawPassSetup.h" #include "FrameGraphPasses/HiZBuildPassSetup.h" // Phase 3.5: Hi-Z pyramid for GPU culling #include "FrameGraphPasses/ForwardColorPassSetup.h" // Phase 1: Single-RT forward rendering + pipeline init +#include "FrameGraphPasses/DepthPrepassSetup.h" +#include "FrameGraphPasses/PassCommon.h" #include "GPUCullingManager.h" // Phase 3.5: GPU frustum/occlusion culling #include "FGDetailManager.h" // Detail system (grass/vegetation) #include "FrameGraphPasses/DetailCullPassSetup.h" // Detail culling (async compute) @@ -52,6 +55,10 @@ #include "FrameGraphPasses/UIPassSetup.h" #include "FrameGraphPasses/FontPassSetup.h" #include "FrameGraphPasses/TonemapPassSetup.h" // Tonemap pass: HDR→LDR conversion +#include "FrameGraphPasses/VolumetricPassSetup.h" +#include "Volumetrics/VolumetricRenderer.h" +#include "FrameGraphPasses/ShadowPassSetup.h" +#include "FrameGraphPasses/LocalShadowPassSetup.h" #include "FrameGraphPasses/SmokeTrailPassSetup.h" #include "FrameGraphPasses/ClusterLightPassSetup.h" #include "ClusteredLightManager.h" @@ -63,9 +70,21 @@ #include "Layers/xrRender/FrameGraph/Blackboard.h" #include "FrameGraphPasses/ImGuiPassSetup.h" #include "FrameGraphPasses/RainPassSetup.h" +#include "FrameGraphPasses/WetSurfacesPassSetup.h" +#include "FrameGraphPasses/SceneReflectionPassSetup.h" +#include "FrameGraphPasses/RainShadowPassSetup.h" +#include "FrameGraphPasses/AmbientOcclusionPassSetup.h" +#include "FrameGraphPasses/SunShaftsPassSetup.h" #include "FrameGraphPasses/ThunderboltPassSetup.h" #include "FrameGraphPasses/LensFlarePassSetup.h" #include "FrameGraphPasses/PathTracerPassSetup.h" +#include "FrameGraphPasses/SSRPassSetup.h" +#include "FrameGraphPasses/SSGIPassSetup.h" +#include "FrameGraphPasses/DofPassSetup.h" +#include "FrameGraphPasses/TAAPassSetup.h" +#include "FrameGraphPasses/ContactShadowsPassSetup.h" +#include "FrameGraphPasses/BloomPassSetup.h" +#include "FrameGraphPasses/CASPassSetup.h" #include "Layers/xrRender/fgRainRender.h" #include "Layers/xrRender/fgThunderboltRender.h" #include "Layers/xrRender/fgLensFlareRender.h" @@ -175,6 +194,12 @@ extern ENGINE_API int ps_r_rt_gi; extern ENGINE_API float ps_r_rt_gi_intensity; extern ENGINE_API int ps_r_path_tracer; extern ENGINE_API int ps_r_path_tracer_bounces; +extern ENGINE_API int ps_r_taa; +extern ENGINE_API int ps_r_bloom; +extern ENGINE_API int ps_r_cas; +extern ENGINE_API int ps_r_ssr; +extern ENGINE_API int ps_r_ssgi; +extern ENGINE_API int ps_r_contact_shadows; namespace xray::render { @@ -249,6 +274,7 @@ bool FrameGraphRenderer::Initialize(fg::RenderDevice* device) { m_overlayManager = xr_make_unique(); m_rtAccelMgr = xr_make_unique(); m_smokeTrailManager = xr_make_unique(); + m_volumetricRenderer = xr_make_unique(); bindless::MaterialBuffer::Instance().Initialize(m_device); @@ -263,6 +289,7 @@ bool FrameGraphRenderer::Initialize(fg::RenderDevice* device) { m_overlayManager->Initialize(device); m_rtAccelMgr->Initialize(device); m_smokeTrailManager->Initialize(device); + m_volumetricRenderer->Initialize(device); // Create RenderContext for execution m_renderContext.reset(device->CreateContext()); @@ -273,6 +300,12 @@ bool FrameGraphRenderer::Initialize(fg::RenderDevice* device) { } m_blackboard = xr_make_unique(); + if (m_volumetricRenderer->IsReady()) + { + passes::InitializeVolumetricPass( + device->GetNVRHIDevice(), + m_blackboard->get_or_add()); + } m_gpuProfiler = xr_make_unique(); m_statsOverlay = xr_make_unique(); @@ -331,7 +364,11 @@ void FrameGraphRenderer::Shutdown() { m_uiMaterialCache = nullptr; m_uiVCBPool = nullptr; m_cachedStaticBatches.clear(); - m_staticBatchesCached = false; + m_sectorStaticBatchIds.clear(); + m_sectorCacheReady.clear(); + m_visualCacheBatchIds.clear(); + m_staticCacheInitialized = false; + m_portalTraverseActive = false; if (m_gpuCullingManager) { m_gpuCullingManager->InvalidateStaticCullingData(); m_gpuCullingManager = nullptr; @@ -358,6 +395,11 @@ void FrameGraphRenderer::Shutdown() { m_smokeTrailManager = nullptr; } + if (m_volumetricRenderer) { + m_volumetricRenderer->Shutdown(); + m_volumetricRenderer = nullptr; + } + fg::ClusteredLightManager::Instance().Shutdown(); passes::ShutdownPathTracer(); @@ -368,6 +410,14 @@ void FrameGraphRenderer::Shutdown() { if (m_blackboard) { if (auto* tonemap = m_blackboard->try_get()) passes::ShutdownTonemapPass(*tonemap); + if (auto* vol = m_blackboard->try_get()) + passes::ShutdownVolumetricPass(*vol); + if (auto* shadow = m_blackboard->try_get()) + passes::ShutdownShadowPass(m_device, *shadow); + if (auto* localShadow = m_blackboard->try_get()) + passes::ShutdownLocalShadowPass(m_device, *localShadow); + if (auto* taa = m_blackboard->try_get()) + passes::ShutdownTAAPass(*taa); m_blackboard.reset(); } @@ -523,7 +573,7 @@ void FrameGraphRenderer::Render() { } m_hasPrevFrameData = true; - m_prevViewProj = Device.mFullTransform; + m_prevViewProj = passes::g_taa_unjittered_full_transform; m_prevCameraPos = Device.vCameraPosition; m_pingPongIndex = 1 - m_pingPongIndex; @@ -589,48 +639,31 @@ void FrameGraphRenderer::RenderMenu() { backbufferHandle = m_framegraph->ImportTexture("Backbuffer", backbufferTexture, backbufferDesc); } - framegraph::ResourceDesc bgDesc; - bgDesc.type = framegraph::ResourceDesc::Type::Texture2D; - bgDesc.width = width; - bgDesc.height = height; - bgDesc.format = nvrhi::Format::RGBA8_UNORM; - bgDesc.isRenderTarget = true; - bgDesc.debugName = "rt_MenuBackground"; + if (!backbufferHandle.is_valid()) + return; - auto backgroundTarget = m_framegraph->CreateTexture("rt_MenuBackground", bgDesc); - + // Menu / loading-only path: draw UI straight to backbuffer (no ACES/post) framegraph::PassHandle clearPass = m_framegraph->AddPass("ClearBackground"); - m_framegraph->PassWrite(clearPass, backgroundTarget, framegraph::ResourceState::RenderTarget); + m_framegraph->PassWrite(clearPass, backbufferHandle, framegraph::ResourceState::RenderTarget); m_framegraph->SetPassCallback(clearPass, - [backgroundTarget](fg::RenderContext& ctx, const framegraph::FrameGraph& fg) { - auto* bgRT = fg.GetPhysicalTexture(backgroundTarget); - if (bgRT) { + [backbufferHandle](fg::RenderContext& ctx, const framegraph::FrameGraph& fg) { + auto* bb = fg.GetPhysicalTexture(backbufferHandle); + if (bb) { nvrhi::ICommandList* cmdList = ctx.GetCommandList(); - cmdList->clearTextureFloat(bgRT, nvrhi::AllSubresources, nvrhi::Color(0.0f)); + cmdList->clearTextureFloat(bb, nvrhi::AllSubresources, nvrhi::Color(0.0f)); } } ); - auto sceneWithUI = passes::setupUIPass(*m_framegraph, backgroundTarget, width, height); + auto sceneWithUI = passes::setupUIPass(*m_framegraph, backbufferHandle, width, height); sceneWithUI = passes::setupFontPass(*m_framegraph, sceneWithUI); sceneWithUI = passes::setupCursorPass(*m_framegraph, sceneWithUI, width, height); sceneWithUI = passes::setupDebugDrawPass(*m_framegraph, sceneWithUI, width, height); - auto ldrOutput = passes::setupTonemapPass( - *m_framegraph, - m_device, - sceneWithUI, // HDR input (RGBA16_FLOAT) - framegraph::VirtualResourceHandle(), // No exposure for menu - backbufferHandle, // Output directly to imported backbuffer - width, - height, - m_blackboard->get_or_add() - ); - fg::ImGuiRendererNVRHI* imguiRenderer = GEnv.Render->GetImGuiRendererNVRHI(); auto finalOutput = passes::setupImGuiPass( *m_framegraph, - ldrOutput, // LDR input (RGBA8_UNORM) + sceneWithUI, imguiRenderer, width, height @@ -893,7 +926,7 @@ void FrameGraphRenderer::SetupFrame() { fg::ClusteredLightManager::Instance().BeginFrame(); } - if (levelLoaded) { + if (levelLoaded && !g_pGamePersistent->IsLoadingScreenShown()) { ZoneScopedN("SetupFrame::CollectVisibleGeometry"); CollectVisibleGeometry(); } @@ -925,6 +958,9 @@ framegraph::VirtualResourceHandle FrameGraphRenderer::CreateRT( } void FrameGraphRenderer::SetupFrameGraphPasses() { + // Subpixel Halton jitter for TAA (must run after camera projection is final) + passes::ApplyTAAJitter(); + const u32 width = Device.dwWidth; const u32 height = Device.dwHeight; @@ -1027,10 +1063,17 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { framegraph::VirtualResourceHandle worldPosBuffer = m_framegraph->ImportTexture("rt_WorldPos", m_worldPos[writeIdx], worldPosImportDesc); // ═══════════════════════════════════════════════════════ - // TEMPORAL HI-Z PYRAMID BUILD (From Previous Frame) + // HI-Z + GPU CULL + // r_depth_prepass ON: frustum cull first → depth → same-frame Hi-Z → + // occlusion compact (skipUpload) → Cluster → Forward. + // r_depth_prepass OFF: temporal Hi-Z (prev depth) → one cull → Forward. + // Detail/skinned/particle never use Hi-Z occlusion (flicker / self-occ). // ═══════════════════════════════════════════════════════ + const bool useDepthPrepass = ps_r_depth_prepass != 0; + const bool useHizOcclusion = ps_r_hiz_occlusion != 0; + passes::HiZPyramidOutput hizOutput; - hizOutput.pyramid = framegraph::VirtualResourceHandle(); // Invalid by default + hizOutput.pyramid = framegraph::VirtualResourceHandle(); hizOutput.mipLevels = 0; hizOutput.width = width / 2; hizOutput.height = height / 2; @@ -1038,7 +1081,8 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { bool hasPrevDepth = m_hasPrevFrameData && m_prevFrameDepth && m_prevFrameWidth == width && m_prevFrameHeight == height; - if (hasPrevDepth) { + // Temporal Hi-Z only when depth prepass is OFF (fallback path). + if (!useDepthPrepass && hasPrevDepth) { framegraph::ResourceDesc prevDepthDesc; prevDepthDesc.type = framegraph::ResourceDesc::Type::Texture2D; prevDepthDesc.debugName = "rt_PrevDepth"; @@ -1096,13 +1140,13 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } // ═══════════════════════════════════════════════════════ - // PHASE 3.5: GPU CULLING PASS (Frustum + Occlusion) + // GPU CULLING — pass 1: frustum (+ temporal Hi-Z if depth prepass OFF) // ═══════════════════════════════════════════════════════ - framegraph::VirtualResourceHandle drawArgsBuffer; // Will be passed to forward pass - framegraph::VirtualResourceHandle skinnedDrawArgsBuffer; // Will be passed to skinning pass + framegraph::VirtualResourceHandle drawArgsBuffer; + framegraph::VirtualResourceHandle skinnedDrawArgsBuffer; - if (m_gpuCullingManager && hizOutput.pyramid.is_valid()) { + if (m_gpuCullingManager) { m_gpuCullingManager->Initialize(m_device); if (m_detailManager && !m_detailManager->computePipeline) { @@ -1128,27 +1172,36 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { if (m_rtAccelMgr && m_rtAccelMgr->IsSupported()) m_gpuCullingManager->SetRTAccelStructManager(m_rtAccelMgr.get()); + // Depth-prepass path: frustum-only first; occlusion comes after same-frame Hi-Z. + // Fallback path: use temporal pyramid when available. + const bool forceFrustumOnly = useDepthPrepass || !hizOutput.pyramid.is_valid(); + if (m_gpuCullingManager->IsEnabled()) { auto cullOutput = m_gpuCullingManager->SetupCullingPass( *m_framegraph, - m_hizPyramid, - hizOutput.width, - hizOutput.height, - hizOutput.mipLevels, - m_geometryCollector.get(), // Geometry is uploaded during execute - m_prevViewProj // Previous frame's viewProj for temporal Hi-Z + forceFrustumOnly ? framegraph::VirtualResourceHandle() : m_hizPyramid, + forceFrustumOnly ? 1u : hizOutput.width, + forceFrustumOnly ? 1u : hizOutput.height, + forceFrustumOnly ? 1u : hizOutput.mipLevels, + m_geometryCollector.get(), + m_prevViewProj, + forceFrustumOnly, + false ); drawArgsBuffer = cullOutput.drawArgsBuffer; } + // Skinned/particle always take the dummy Hi-Z (all-visible) path: NPCs and + // effects move between frames, so Hi-Z occlusion makes them flicker. + const framegraph::VirtualResourceHandle noHiz; if (m_gpuCullingManager->IsParticleCullingEnabled() && !m_worldParticleBatches.empty()) { m_gpuCullingManager->SetupParticleCullingPass( *m_framegraph, - m_hizPyramid, - hizOutput.width, - hizOutput.height, - hizOutput.mipLevels, + noHiz, + 1, + 1, + 1, &m_worldParticleBatches ); } @@ -1156,10 +1209,10 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { if (m_gpuCullingManager->IsSkinnedCullingEnabled()) { skinnedDrawArgsBuffer = m_gpuCullingManager->SetupSkinnedCullingPass( *m_framegraph, - m_hizPyramid, - hizOutput.width, - hizOutput.height, - hizOutput.mipLevels, + noHiz, + 1, + 1, + 1, m_geometryCollector.get(), m_prevViewProj, m_overlayManager.get() @@ -1193,17 +1246,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { height ); - // ═══════════════════════════════════════════════════════ - // SUN PASS (Sun disc with additive blending) - // ═══════════════════════════════════════════════════════ - - auto sunOutput = passes::setupSunPass( - *m_framegraph, - skyOutput, - fgEnv, - width, - height - ); + // Sun is drawn AFTER opaque forward (with depth test) — see below. // ═══════════════════════════════════════════════════════ // FORWARD COLOR PASS (Single-RT, Reuses Depth) @@ -1218,6 +1261,14 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { bindlessConfig.staticSet.compactCountBuffer = m_gpuCullingManager->GetStaticCompactCountBuffer(); bindlessConfig.staticSet.instanceBuffer = m_gpuCullingManager->GetStaticInstanceBuffer(); bindlessConfig.staticSet.totalObjectCount = m_gpuCullingManager->GetStaticObjectCount(); + bindlessConfig.staticSet.castAllDrawArgsBuffer = m_gpuCullingManager->GetStaticDrawArgsBuffer(); + bindlessConfig.staticSet.castAllBatchIndicesBuffer = m_gpuCullingManager->GetStaticShadowIndicesBuffer(); + bindlessConfig.staticSet.castAllMaterialIDBuffer = m_gpuCullingManager->GetStaticFullMaterialIDBuffer(); + bindlessConfig.staticSet.castAllCountBuffer = m_gpuCullingManager->GetStaticShadowCountBuffer(); + bindlessConfig.staticSet.lightCullDrawArgsBuffer = m_gpuCullingManager->GetStaticShadowCompactDrawArgsBuffer(); + bindlessConfig.staticSet.lightCullBatchIndicesBuffer = m_gpuCullingManager->GetStaticShadowIndicesBuffer(); + bindlessConfig.staticSet.lightCullMaterialIDBuffer = m_gpuCullingManager->GetStaticShadowCompactMaterialIDBuffer(); + bindlessConfig.staticSet.lightCullCountBuffer = m_gpuCullingManager->GetStaticShadowCountBuffer(); bindlessConfig.dynamicSet.compactDrawArgsBuffer = m_gpuCullingManager->GetDynamicCompactDrawArgsBuffer(); bindlessConfig.dynamicSet.compactMaterialIDBuffer = m_gpuCullingManager->GetDynamicCompactMaterialIDBuffer(); @@ -1225,6 +1276,36 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { bindlessConfig.dynamicSet.compactCountBuffer = m_gpuCullingManager->GetDynamicCompactCountBuffer(); bindlessConfig.dynamicSet.instanceBuffer = m_gpuCullingManager->GetDynamicInstanceBuffer(); bindlessConfig.dynamicSet.totalObjectCount = m_gpuCullingManager->GetDynamicObjectCount(); + bindlessConfig.dynamicSet.castAllDrawArgsBuffer = m_gpuCullingManager->GetDynamicDrawArgsBuffer(); + bindlessConfig.dynamicSet.castAllBatchIndicesBuffer = m_gpuCullingManager->GetDynamicShadowIndicesBuffer(); + bindlessConfig.dynamicSet.castAllMaterialIDBuffer = m_gpuCullingManager->GetDynamicFullMaterialIDBuffer(); + bindlessConfig.dynamicSet.castAllCountBuffer = m_gpuCullingManager->GetDynamicShadowCountBuffer(); + bindlessConfig.dynamicSet.lightCullDrawArgsBuffer = m_gpuCullingManager->GetDynamicShadowCompactDrawArgsBuffer(); + bindlessConfig.dynamicSet.lightCullBatchIndicesBuffer = m_gpuCullingManager->GetDynamicShadowIndicesBuffer(); + bindlessConfig.dynamicSet.lightCullMaterialIDBuffer = m_gpuCullingManager->GetDynamicShadowCompactMaterialIDBuffer(); + bindlessConfig.dynamicSet.lightCullCountBuffer = m_gpuCullingManager->GetDynamicShadowCountBuffer(); + + // Transparent set as foliage shadow caster (trees/bushes). Only cast-all buffers + // are needed here; the transparent forward render uses its own compact config. + bindlessConfig.transparentCasterSet.instanceBuffer = m_gpuCullingManager->GetTransparentInstanceBuffer(); + bindlessConfig.transparentCasterSet.totalObjectCount = m_gpuCullingManager->GetTransparentObjectCount(); + bindlessConfig.transparentCasterSet.castAllDrawArgsBuffer = m_gpuCullingManager->GetTransparentDrawArgsBuffer(); + bindlessConfig.transparentCasterSet.castAllBatchIndicesBuffer = m_gpuCullingManager->GetTransparentShadowIndicesBuffer(); + bindlessConfig.transparentCasterSet.castAllMaterialIDBuffer = m_gpuCullingManager->GetTransparentFullMaterialIDBuffer(); + bindlessConfig.transparentCasterSet.castAllCountBuffer = m_gpuCullingManager->GetTransparentShadowCountBuffer(); + bindlessConfig.transparentCasterSet.lightCullDrawArgsBuffer = m_gpuCullingManager->GetTransparentShadowCompactDrawArgsBuffer(); + bindlessConfig.transparentCasterSet.lightCullBatchIndicesBuffer = m_gpuCullingManager->GetTransparentShadowIndicesBuffer(); + bindlessConfig.transparentCasterSet.lightCullMaterialIDBuffer = m_gpuCullingManager->GetTransparentShadowCompactMaterialIDBuffer(); + bindlessConfig.transparentCasterSet.lightCullCountBuffer = m_gpuCullingManager->GetTransparentShadowCountBuffer(); + + if (m_gpuCullingManager->GetTessObjectCount() > 0) { + bindlessConfig.tessMaterialIDBuffer = m_gpuCullingManager->GetTessMaterialIDBuffer(); + bindlessConfig.tessBatchIndicesBuffer = m_gpuCullingManager->GetTessBatchIndicesBuffer(); + bindlessConfig.tessInstanceBuffer = m_gpuCullingManager->GetTessInstanceBuffer(); + bindlessConfig.tessDrawArgs = m_gpuCullingManager->GetTessDrawArgsData().data(); + bindlessConfig.tessObjects = m_gpuCullingManager->GetTessObjectData().data(); + bindlessConfig.tessObjectCount = m_gpuCullingManager->GetTessObjectCount(); + } // ═══════════════════════════════════════════════════════ // MEGA-BUFFER CONFIGURATION (GPU-Driven Rendering) @@ -1254,29 +1335,242 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { bindlessConfig.variantPartition = m_gpuCullingManager->GetStaticPartition().ToConfig(); } - auto& clmSetup = fg::ClusteredLightManager::Instance(); - if (clmSetup.IsReady() && clmSetup.GetLightCount() > 0) { - passes::setupClusterLightPass( + // Cluster assign runs after Hi-Z is final (see below, post DepthPrepass). + + // ═══════════════════════════════════════════════════════ + // PERLIN4D NOISE (async compute — overlaps CSM / Forward) + // ═══════════════════════════════════════════════════════ + framegraph::VirtualResourceHandle perlinReadyHandle; + if (m_detailManager && m_detailManager->perlin4dPipeline) + { + framegraph::ResourceDesc perlinSyncDesc; + perlinSyncDesc.type = framegraph::ResourceDesc::Type::Buffer; + perlinSyncDesc.bufferSize = 16; + perlinSyncDesc.isUAV = true; + perlinSyncDesc.allowUAV = true; + perlinSyncDesc.debugName = "buf_PerlinReady"; + perlinReadyHandle = m_framegraph->CreateBuffer("buf_PerlinReady", perlinSyncDesc); + + struct Perlin4DGenData { + FGDetailManager* dm = nullptr; + framegraph::VirtualResourceHandle ready; + }; + m_framegraph->addCallbackPass( + "Perlin4DGen", + [&, perlinReadyHandle](framegraph::FrameGraph& builder, framegraph::PassHandle passHandle, Perlin4DGenData& data) + { + framegraph::RenderPassBuilder passBuilder(builder, passHandle); + data.ready = passBuilder.write(perlinReadyHandle, framegraph::ResourceState::UnorderedAccess); + passBuilder.asyncCompute(); + data.dm = m_detailManager.get(); + }, + [](const Perlin4DGenData& data, const framegraph::FrameGraph&, fg::RenderContext* ctx) + { + auto* cmdList = ctx->GetCommandList(); + auto* device = cmdList->getDevice(); + data.dm->DispatchPerlin4DCompute(cmdList, device, Device.fTimeGlobal); + } + ); + } + + // DetailCull: frustum / all-visible only (dummy Hi-Z cleared to far). + // Same-frame pyramid is built after DepthPrepass — too late for grass CSM. + if (!m_hizPyramid.is_valid() && m_gpuCullingManager && m_gpuCullingManager->GetDummyHiZTexture()) + { + framegraph::ResourceDesc dummyDesc; + dummyDesc.type = framegraph::ResourceDesc::Type::Texture2D; + dummyDesc.debugName = "rt_DummyHiZ"; + dummyDesc.width = 1; + dummyDesc.height = 1; + dummyDesc.format = nvrhi::Format::R32_FLOAT; + dummyDesc.isImported = true; + dummyDesc.isTransient = false; + m_hizPyramid = m_framegraph->ImportTexture( + "rt_DummyHiZ_Early", m_gpuCullingManager->GetDummyHiZTexture(), dummyDesc); + hizOutput.pyramid = m_hizPyramid; + hizOutput.width = 1; + hizOutput.height = 1; + hizOutput.mipLevels = 1; + } + + // When depth-prepass path: force DetailCull onto cleared dummy (no occlusion). + framegraph::VirtualResourceHandle detailHiz = m_hizPyramid; + u32 detailHizW = hizOutput.width; + u32 detailHizH = hizOutput.height; + u32 detailHizMips = hizOutput.mipLevels; + if (useDepthPrepass && m_gpuCullingManager && m_gpuCullingManager->GetDummyHiZTexture()) + { + framegraph::ResourceDesc dummyDesc; + dummyDesc.type = framegraph::ResourceDesc::Type::Texture2D; + dummyDesc.debugName = "rt_DummyHiZ_Detail"; + dummyDesc.width = 1; + dummyDesc.height = 1; + dummyDesc.format = nvrhi::Format::R32_FLOAT; + dummyDesc.isImported = true; + dummyDesc.isTransient = false; + detailHiz = m_framegraph->ImportTexture( + "rt_DummyHiZ_Detail", m_gpuCullingManager->GetDummyHiZTexture(), dummyDesc); + detailHizW = 1; + detailHizH = 1; + detailHizMips = 1; + } + + // ═══════════════════════════════════════════════════════ + // DETAIL CULL PASS (before CSM so grass can cast with r2_sun_details) + // ═══════════════════════════════════════════════════════ + passes::setupDetailCullPass( + *m_framegraph, + m_device, + m_detailManager.get(), + detailHiz, + detailHizW, + detailHizH, + detailHizMips, + (useDepthPrepass || !m_hasPrevFrameData) ? nullptr : &m_prevViewProj, + m_gpuProfiler.get(), + &m_blackboard->get_or_add() + ); + + // ═══════════════════════════════════════════════════════ + // CASCADED SHADOW MAPS (sun CSM, before Forward) + // ═══════════════════════════════════════════════════════ + { + Fvector sunDir(0.3f, 0.8f, 0.2f); + if (g_pGamePersistent) + { + const auto& env = g_pGamePersistent->Environment().CurrentEnv; + // toward sun (same as volumetric / lighting) + sunDir.set(-env.sun_dir.x, -env.sun_dir.y, -env.sun_dir.z); + } + auto shadowOut = passes::setupCascadedShadowPass( + *m_framegraph, + m_device, + bindlessConfig, + m_materialCache.get(), + m_detailManager.get(), + sunDir, + &m_hudBatches, + &m_geometryCollector->GetBatches(), + m_gpuCullingManager.get(), + m_blackboard->get_or_add()); + if (shadowOut.valid) + { + auto& shadowState = m_blackboard->get_or_add(); + bindlessConfig.shadowMapArray = shadowState.shadowCascades[0]; + for (u32 i = 0; i < 3; ++i) + bindlessConfig.shadowCascades[i] = shadowState.shadowCascades[i]; + bindlessConfig.shadowMapHandle = shadowOut.shadowArray; + bindlessConfig.hudShadowMap = shadowState.hudShadowMap; + m_framegraph->GetRTRegistry().RegisterRT("rt_ShadowMap", shadowOut.shadowArray); + } + } + + // Local spot/OMNIPART shadow atlas (indoor lamps, flashlight, etc.) + { + auto& clmLocal = fg::ClusteredLightManager::Instance(); + clmLocal.AssignLocalShadowTiles(Device.vCameraPosition); + if (!clmLocal.GetLocalShadowTiles().empty()) + { + auto localOut = passes::setupLocalShadowPass( + *m_framegraph, + m_device, + bindlessConfig, + m_blackboard->get_or_add(), + m_blackboard->get_or_add(), + &m_geometryCollector->GetBatches(), + m_gpuCullingManager.get()); + if (localOut.valid) + { + bindlessConfig.localShadowAtlas = localOut.atlasTex; + bindlessConfig.localShadowHandle = localOut.atlas; + m_framegraph->GetRTRegistry().RegisterRT("rt_LocalShadowAtlas", localOut.atlas); + } + } + } + + passes::ResolveEnvSkyCubes(m_device, bindlessConfig.envSky0, bindlessConfig.envSky1); + + // Contact shadows: prev-frame depth + temporal history (cannot sample current DSV while attached) + if (ps_r_contact_shadows && m_hasPrevFrameData && m_prevFrameDepth) + bindlessConfig.contactDepth = m_prevFrameDepth; + { + auto& contactState = m_blackboard->get_or_add(); + bindlessConfig.contactHistory = passes::GetContactShadowHistory(contactState); + } + + // Opaque depth fill → early-Z for Forward (LEQ, no depth clear). + // Same-frame Hi-Z is built from this depth when r_depth_prepass is on. + if (useDepthPrepass) + { + depthBuffer = passes::setupDepthPrepass( *m_framegraph, m_device, - &clmSetup, + depthBuffer, + m_geometryCollector.get(), + m_materialCache.get(), width, height, - &m_blackboard->get_or_add(), - hizOutput.pyramid, - hizOutput.width, - hizOutput.height, - hizOutput.mipLevels, - m_prevViewProj, - m_hasPrevFrameData + bindlessConfig, + &m_blackboard->get_or_add()); + + // Same-frame Hi-Z from current depth → occlusion compact for static. + hizOutput = passes::setupHiZBuildPass( + *m_framegraph, + m_device, + depthBuffer, + width, + height, + m_blackboard->get_or_add() ); + m_hizPyramid = hizOutput.pyramid; + if (m_hizPyramid.is_valid()) + m_framegraph->GetRTRegistry().RegisterRT("rt_HiZ", m_hizPyramid); + + if (useHizOcclusion && m_gpuCullingManager && m_gpuCullingManager->IsEnabled() && + hizOutput.pyramid.is_valid()) + { + auto cullHiz = m_gpuCullingManager->SetupCullingPass( + *m_framegraph, + m_hizPyramid, + hizOutput.width, + hizOutput.height, + hizOutput.mipLevels, + m_geometryCollector.get(), + Device.mFullTransform, // same-frame: prev == current for Hi-Z UV + false, + true // skipUpload — reuse object buffers, re-compact with occlusion + ); + if (cullHiz.drawArgsBuffer.is_valid()) + drawArgsBuffer = cullHiz.drawArgsBuffer; + } + } + + // Cluster assign: no Hi-Z light cull (temporal/same-frame both false-cull indoor lights). + { + auto& clmSetup = fg::ClusteredLightManager::Instance(); + if (clmSetup.IsReady() && clmSetup.GetLightCount() > 0) { + passes::setupClusterLightPass( + *m_framegraph, + m_device, + &clmSetup, + width, + height, + &m_blackboard->get_or_add(), + framegraph::VirtualResourceHandle(), + 0, + 0, + 0, + m_prevViewProj, + false + ); + } } auto forwardOutputs = passes::setupForwardColorPass( *m_framegraph, m_device, depthBuffer, - sunOutput, + skyOutput, normalBuffer, baseColorBuffer, worldPosBuffer, @@ -1320,48 +1614,18 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { m_gpuCullingManager.get(), skinnedDrawArgsBuffer, &m_blackboard->get_or_add(), - m_overlayManager.get() + m_overlayManager.get(), + bindlessConfig.shadowMapArray, + bindlessConfig.shadowMapHandle, + bindlessConfig.contactDepth, + bindlessConfig.contactHistory, + bindlessConfig.envSky0, + bindlessConfig.envSky1, + bindlessConfig.hudShadowMap, + bindlessConfig.shadowCascades, + bindlessConfig.localShadowAtlas ); - // ═══════════════════════════════════════════════════════ - // DETAIL CULL PASS (Async Compute) - // ═══════════════════════════════════════════════════════ - passes::setupDetailCullPass( - *m_framegraph, - m_device, - m_detailManager.get(), - hizOutput.pyramid, - hizOutput.width, - hizOutput.height, - hizOutput.mipLevels, - m_hasPrevFrameData ? &m_prevViewProj : nullptr, - m_gpuProfiler.get(), - &m_blackboard->get_or_add() - ); - - // ═══════════════════════════════════════════════════════ - // PERLIN4D NOISE GENERATION (Compute — updates shared noise texture) - // ═══════════════════════════════════════════════════════ - if (m_detailManager && m_detailManager->perlin4dPipeline) - { - struct Perlin4DGenData { FGDetailManager* dm = nullptr; }; - m_framegraph->addCallbackPass( - "Perlin4DGen", - [&](framegraph::FrameGraph& builder, framegraph::PassHandle passHandle, Perlin4DGenData& data) - { - framegraph::RenderPassBuilder passBuilder(builder, passHandle); - passBuilder.sideEffects(); - data.dm = m_detailManager.get(); - }, - [](const Perlin4DGenData& data, const framegraph::FrameGraph&, fg::RenderContext* ctx) - { - auto* cmdList = ctx->GetCommandList(); - auto* device = cmdList->getDevice(); - data.dm->DispatchPerlin4DCompute(cmdList, device, Device.fTimeGlobal); - } - ); - } - // ═══════════════════════════════════════════════════════ // DETAIL DRAW PASS (Graphics) // ═══════════════════════════════════════════════════════ @@ -1372,7 +1636,14 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { hudOutputs, width, height, - m_gpuProfiler.get() + m_gpuProfiler.get(), + bindlessConfig.shadowMapArray, + bindlessConfig.shadowMapHandle, + bindlessConfig.contactDepth, + bindlessConfig.contactHistory, + perlinReadyHandle, + bindlessConfig.shadowCascades, + bindlessConfig.localShadowAtlas ); // ═══════════════════════════════════════════════════════ @@ -1388,6 +1659,14 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { transparentConfig.compactMaterialIDBuffer = m_gpuCullingManager->GetTransparentCompactMaterialIDBuffer(); transparentConfig.compactCountBuffer = m_gpuCullingManager->GetTransparentCompactCountBuffer(); transparentConfig.objectCount = m_gpuCullingManager->GetTransparentObjectCount(); + transparentConfig.shadowMapArray = bindlessConfig.shadowMapArray; + for (int i = 0; i < 3; ++i) + transparentConfig.shadowCascades[i] = bindlessConfig.shadowCascades[i]; + transparentConfig.localShadowAtlas = bindlessConfig.localShadowAtlas; + transparentConfig.envSky0 = bindlessConfig.envSky0; + transparentConfig.envSky1 = bindlessConfig.envSky1; + transparentConfig.contactDepth = bindlessConfig.contactDepth; + transparentConfig.contactHistory = bindlessConfig.contactHistory; if (m_gpuCullingManager->IsVariantPartitionEnabled()) transparentConfig.variantPartition = m_gpuCullingManager->GetTransparentPartition().ToConfig(); @@ -1425,7 +1704,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { motionOutput = passes::setupMotionVectorPass( *m_framegraph, m_device, transparentOutputs.depth, - Device.mInvFullTransform, m_prevViewProj, + passes::g_taa_unjittered_inv_full_transform, m_prevViewProj, width, height, m_blackboard->get_or_add() ); @@ -1451,39 +1730,42 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { ); // ═══════════════════════════════════════════════════════ - // RIBBON PASS (test quad, after particles) + // RIBBON + TRAIL TEST PASSES (debug quads, off by default) + // Not wired to any emitter — kept behind r_test_trails. // ═══════════════════════════════════════════════════════ - auto ribbonOutputs = passes::setupRibbonPass( - *m_framegraph, - m_device, - particleOutputs.layout, - width, - height, - &m_blackboard->get_or_add() - ); + auto trailChainLayout = particleOutputs.layout; + if (ps_r_test_trails) + { + auto ribbonOutputs = passes::setupRibbonPass( + *m_framegraph, + m_device, + particleOutputs.layout, + width, + height, + &m_blackboard->get_or_add() + ); - // ═══════════════════════════════════════════════════════ - // TRAIL PASS (after ribbon, stored-direction width) - // ═══════════════════════════════════════════════════════ - auto trailOutputs = passes::setupTrailPass( - *m_framegraph, - m_device, - ribbonOutputs.layout, - width, - height, - &m_blackboard->get_or_add() - ); + auto trailOutputs = passes::setupTrailPass( + *m_framegraph, + m_device, + ribbonOutputs.layout, + width, + height, + &m_blackboard->get_or_add() + ); + trailChainLayout = trailOutputs.layout; + } // ═══════════════════════════════════════════════════════ // SMOKE TRAIL PASS (GPU-simulated weapon muzzle smoke) // ═══════════════════════════════════════════════════════ - auto smokeOutputs = trailOutputs.layout; - if (m_smokeTrailManager && m_smokeTrailManager->IsReady()) + auto smokeOutputs = trailChainLayout; + if (ps_r_smoke_trail_enabled && m_smokeTrailManager && m_smokeTrailManager->IsReady()) { smokeOutputs = passes::setupSmokeTrailPass( *m_framegraph, m_device, - trailOutputs.layout, + trailChainLayout, m_smokeTrailManager.get(), width, height, @@ -1492,6 +1774,9 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { ); } + // Wet surfaces applied later (after lighting composites) — see below. + auto gbufferForWet = smokeOutputs; + auto sceneColor = smokeOutputs.albedo; if (particleOutputs.distortionRT.is_valid()) { @@ -1513,7 +1798,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { prevNormalsHandle, prevWorldPosHandle, motionOutput.motionVectors, sceneColor, - Device.mInvFullTransform, m_prevViewProj, + passes::g_taa_unjittered_inv_full_transform, m_prevViewProj, Device.vCameraPosition, ps_r_rt_gi_intensity, width, height, m_blackboard->get_or_add(), m_hasPrevFrameData @@ -1633,6 +1918,33 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } } + passes::RainShadowOutputs rainShadowOut{}; + { + const float rainDensity = g_pGamePersistent + ? g_pGamePersistent->Environment().CurrentEnv.rain_density + : 0.f; + const bool needRainSM = (rainDensity > 0.001f) || ps_r2_ls_flags.test(R3FLAG_DYN_WET_SURF); + if (needRainSM) + { + rainShadowOut = passes::setupRainShadowPass( + *m_framegraph, + m_device, + bindlessConfig, + m_blackboard->get_or_add(), + m_blackboard->get_or_add()); + } + } + + passes::RainHeightmapInfo rainHeightmap{}; + if (m_detailManager && m_detailManager->HasHeightmapGPU()) + { + rainHeightmap.texture = m_detailManager->GetHeightmapTexture(); + rainHeightmap.worldMinX = m_detailManager->GetHeightmapWorldMinX(); + rainHeightmap.worldMinZ = m_detailManager->GetHeightmapWorldMinZ(); + rainHeightmap.texelSize = m_detailManager->GetHeightmapTexelSize(); + rainHeightmap.valid = true; + } + if (g_pGamePersistent && g_pGamePersistent->Environment().eff_Rain) { auto* effRain = g_pGamePersistent->Environment().eff_Rain; @@ -1641,11 +1953,22 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { { if (fgRain->HasWork()) { + auto rainDepth = transparentOutputs.depth.is_valid() + ? transparentOutputs.depth + : depthBuffer; + auto rainWorldPos = transparentOutputs.worldPos.is_valid() + ? transparentOutputs.worldPos + : gbufferForWet.worldPos; sceneColor = passes::setupRainPass( *m_framegraph, sceneColor, - transparentOutputs.depth, - fgRain); + rainDepth, + rainWorldPos, + rainShadowOut.valid ? rainShadowOut.rainSM : framegraph::VirtualResourceHandle{}, + fgRain, + rainShadowOut.sampleVP, + rainShadowOut.valid, + rainHeightmap); } } } @@ -1658,19 +1981,119 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { { if (fgTB->HasWork()) { + auto tbDepth = transparentOutputs.depth.is_valid() + ? transparentOutputs.depth + : depthBuffer; + auto tbWorldPos = transparentOutputs.worldPos.is_valid() + ? transparentOutputs.worldPos + : gbufferForWet.worldPos; sceneColor = passes::setupThunderboltPass( *m_framegraph, sceneColor, - transparentOutputs.depth, + tbDepth, + tbWorldPos, fgTB); } } } + // Wet surfaces AFTER lighting (variant C + wet SSR from SceneReflection). + framegraph::VirtualResourceHandle ssrReflectionSource = sceneColor; + { + ssrReflectionSource = passes::setupSceneReflectionCapture( + *m_framegraph, sceneColor, width, height); + m_framegraph->GetRTRegistry().RegisterRT("rt_SceneReflection", ssrReflectionSource); + } + framegraph::VirtualResourceHandle wetNormal = gbufferForWet.normal; + { + passes::WetSurfacesExtras wetExtras{}; + if (rainShadowOut.rainSMTex) + { + wetExtras.rainSM = rainShadowOut.rainSM; + wetExtras.rainSMTex = rainShadowOut.rainSMTex; + wetExtras.rainSampleVP = rainShadowOut.sampleVP; + wetExtras.rainSMValid = rainShadowOut.valid; + } + if (ssrReflectionSource.is_valid()) + { + wetExtras.sceneReflection = ssrReflectionSource; + wetExtras.sceneReflectionValid = true; + } + auto wetIn = gbufferForWet; + wetIn.albedo = sceneColor; + if (transparentOutputs.normal.is_valid()) + wetIn.normal = transparentOutputs.normal; + else if (normalBuffer.is_valid()) + wetIn.normal = normalBuffer; + if (transparentOutputs.worldPos.is_valid()) + wetIn.worldPos = transparentOutputs.worldPos; + else if (worldPosBuffer.is_valid()) + wetIn.worldPos = worldPosBuffer; + if (transparentOutputs.baseColor.is_valid()) + wetIn.baseColor = transparentOutputs.baseColor; + else if (baseColorBuffer.is_valid()) + wetIn.baseColor = baseColorBuffer; + auto wetOut = passes::setupWetSurfacesPass( + *m_framegraph, + m_device, + wetIn, + width, + height, + m_blackboard->get_or_add(), + wetExtras); + sceneColor = wetOut.albedo; + if (wetOut.normal.is_valid()) + wetNormal = wetOut.normal; + } + + // Screen-space AO (SSAO / HBAO / HDAO / GTAO) after lighting composites. + { + auto aoDepth = transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer; + sceneColor = passes::setupAmbientOcclusionPass( + *m_framegraph, + m_device, + sceneColor, + aoDepth, + wetNormal, + gbufferForWet.worldPos, + width, + height, + m_blackboard->get_or_add()); + } + + // Classic sunshafts (shadow-mapped god rays) — CoP volumetric sun look + { + auto shaftDepth = transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer; + sceneColor = passes::setupSunShaftsPass( + *m_framegraph, + m_device, + sceneColor, + shaftDepth, + gbufferForWet.worldPos, + bindlessConfig.shadowMapArray, + bindlessConfig.shadowMapHandle, + width, + height, + m_blackboard->get_or_add(), + bindlessConfig.shadowCascades); + } + + // Sun disc AFTER opaque/transparent depth is ready — depth-tested so geometry occludes it. + // (Drawn before lens flares; flares do not redraw the sun source.) + sceneColor = passes::setupSunPass( + *m_framegraph, + sceneColor, + transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer, + fgEnv, + width, + height + ); + if (g_pGamePersistent && g_pGamePersistent->Environment().eff_LensFlare) { auto* effLF = g_pGamePersistent->Environment().eff_LensFlare; - effLF->Render(true, true, true); + // bSun=false: sun disc is drawn by setupSunPass with depth test + effLF->Render(false, true, true); if (auto* fgLF = dynamic_cast(effLF->GetRenderer())) { if (fgLF->HasWork()) @@ -1685,8 +2108,99 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } // ═══════════════════════════════════════════════════════ - // EXPOSURE PASS (Auto-Exposure / Eye Adaptation) + // FROXEL VOLUMETRIC FOG (optional; off by default for classic look) // ═══════════════════════════════════════════════════════ + if (ps_r2_ls_flags.test(R3FLAG_VOLUMETRIC_SMOKE) && m_volumetricRenderer && m_volumetricRenderer->IsReady()) + { + sceneColor = passes::setupVolumetricPass( + *m_framegraph, + m_device, + m_volumetricRenderer.get(), + sceneColor, + transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer, + width, + height, + m_blackboard->get_or_add(), + &m_worldParticleBatches); + } + + // ═══════════════════════════════════════════════════════ + // MODERN POST: Contact temporal → SSGI → SSR → TAA → Bloom + // ═══════════════════════════════════════════════════════ + auto postDepth = transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer; + auto postNormal = wetNormal.is_valid() ? wetNormal + : (transparentOutputs.normal.is_valid() ? transparentOutputs.normal : gbufferForWet.normal); + auto postBase = transparentOutputs.baseColor.is_valid() ? transparentOutputs.baseColor : baseColorBuffer; + + // Update contact-shadow history for next frame (sampled as g_ContactHistory @ t28) + if (ps_r_contact_shadows) + { + passes::setupContactShadowsPass( + *m_framegraph, + m_device, + sceneColor, + postDepth, + motionOutput.motionVectors, + width, + height, + m_hasPrevFrameData, + m_blackboard->get_or_add()); + } + + if (ps_r_ssgi && postNormal.is_valid() && postBase.is_valid()) + { + sceneColor = passes::setupSSGIPass( + *m_framegraph, + m_device, + sceneColor, + postDepth, + postNormal, + postBase, + motionOutput.motionVectors, + width, + height, + m_hasPrevFrameData, + m_blackboard->get_or_add()); + m_framegraph->GetRTRegistry().RegisterRT("rt_SSGI", sceneColor); + } + + if (ps_r_ssr && postNormal.is_valid() && postBase.is_valid()) + { + sceneColor = passes::setupSSRPass( + *m_framegraph, + m_device, + sceneColor, + ssrReflectionSource, + postDepth, + postNormal, + postBase, + transparentOutputs.worldPos.is_valid() ? transparentOutputs.worldPos + : (gbufferForWet.worldPos.is_valid() ? gbufferForWet.worldPos : worldPosBuffer), + motionOutput.motionVectors, + width, + height, + m_hasPrevFrameData, + m_blackboard->get_or_add()); + m_framegraph->GetRTRegistry().RegisterRT("rt_SSR", sceneColor); + } + + // TAA after SSR (restored — SSR hard-gate fixed in wet normals) + if (ps_r_taa) + { + sceneColor = passes::setupTAAPass( + *m_framegraph, + m_device, + sceneColor, + postDepth, + motionOutput.motionVectors, + width, + height, + m_hasPrevFrameData, + m_blackboard->get_or_add()); + m_framegraph->GetRTRegistry().RegisterRT("rt_TAA", sceneColor); + } + + // Classic CoP MiddleGray exposure (r2_tonemap*); NOT photographic histogram EV passes::ExposureConfig exposureConfig = passes::GetDefaultExposureConfig(); auto exposureOutput = passes::setupExposurePass( *m_framegraph, @@ -1698,19 +2212,73 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { height, m_blackboard->get_or_add() ); - m_exposureTexture = exposureOutput.exposureTexture; - auto sceneWithUI = passes::setupUIPass( + if (ps_r_bloom) + { + sceneColor = passes::setupBloomPass( + *m_framegraph, + m_device, + sceneColor, + width, + height, + m_blackboard->get_or_add()); + m_framegraph->GetRTRegistry().RegisterRT("rt_Bloom", sceneColor); + } + + // DOF on HDR (classic CoP combine-time blur), gated by r2_dof_enable + if (ps_r2_ls_flags.test(R2FLAG_DOF)) + { + sceneColor = passes::setupDofPass( + *m_framegraph, + m_device, + sceneColor, + postDepth, + width, + height, + m_blackboard->get_or_add()); + m_framegraph->GetRTRegistry().RegisterRT("rt_DOF", sceneColor); + } + + // Tonemap/CAS on scene HDR only — UI/menu/loading must stay LDR and unfiltered + framegraph::VirtualResourceHandle tonemapTarget = backbufferHandle; + if (ps_r_cas) + tonemapTarget = framegraph::VirtualResourceHandle{}; // creates internal rt_Final + + auto ldrOutput = passes::setupTonemapPass( *m_framegraph, + m_device, sceneColor, + exposureOutput.exposureTexture, + tonemapTarget, + width, + height, + m_blackboard->get_or_add(), + &m_blackboard->get_or_add() + ); + + if (ps_r_cas && backbufferHandle.is_valid()) + { + ldrOutput = passes::setupCASPass( + *m_framegraph, + m_device, + ldrOutput, + backbufferHandle, + width, + height, + m_blackboard->get_or_add()); + } + + // UI / fonts / cursor on LDR after post (menu drawn last inside UIPass) + auto sceneWithUI = passes::setupUIPass( + *m_framegraph, + ldrOutput, width, height ); sceneWithUI = passes::setupFontPass(*m_framegraph, sceneWithUI); - // 5. Cursor Pass - Renders cursor on top of UI+Text sceneWithUI = passes::setupCursorPass( *m_framegraph, sceneWithUI, @@ -1720,18 +2288,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { sceneWithUI = passes::setupDebugDrawPass(*m_framegraph, sceneWithUI, width, height); - // 6. Tonemap Pass - Convert HDR to LDR using ACES filmic tonemap - auto ldrOutput = passes::setupTonemapPass( - *m_framegraph, - m_device, - sceneWithUI, - exposureOutput.exposureTexture, - backbufferHandle, - width, - height, - m_blackboard->get_or_add(), - &m_blackboard->get_or_add() - ); + ldrOutput = sceneWithUI; // ═══════════════════════════════════════════════════════ // DEBUG PREVIEW PASS (Render Inspector RT visualization) @@ -1742,6 +2299,22 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { m_framegraph->GetRTRegistry().RegisterRT("rt_BaseColor", baseColorBuffer); m_framegraph->GetRTRegistry().RegisterRT("rt_WorldPos", worldPosBuffer); m_framegraph->GetRTRegistry().RegisterRT("rt_Exposure", exposureOutput.exposureTexture); + // Legacy r2/r3 sampler aliases (vanilla materials / particles) → FrameGraph RTs + m_framegraph->GetRTRegistry().RegisterAliases(worldPosBuffer, { + "$user$position", "s_position", "s_pos", "rt_Position" + }); + m_framegraph->GetRTRegistry().RegisterAliases(transparentOutputs.normal, { + "$user$normal", "s_normal", "rt_Normal" + }); + m_framegraph->GetRTRegistry().RegisterAliases(baseColorBuffer, { + "$user$albedo", "s_albedo", "rt_Albedo" + }); + m_framegraph->GetRTRegistry().RegisterAliases(depthBuffer, { + "$user$base_depth" + }); + m_framegraph->GetRTRegistry().RegisterAliases(skyColorHandle, { + "$user$generic0", "rt_Generic_0" + }); if (motionOutput.motionVectors.is_valid()) m_framegraph->GetRTRegistry().RegisterRT("rt_MotionVectors", motionOutput.motionVectors); if (ps_r_rt_gi) @@ -1952,6 +2525,10 @@ void FrameGraphRenderer::PrintStats() const { bool FrameGraphRenderer::ProcessVisualGeometry(dxRender_Visual* visual, const Fmatrix& worldTransform, IRenderable* renderable, bool isStatic) { if (!visual) return false; + + // Classic HOM reject (static filtered at sector submit; dynamics here) + if (!isStatic && !occ_visible(visual->vis)) + return false; IRender_Mesh* meshVisual = nullptr; switch (visual->getType()) { @@ -2238,6 +2815,39 @@ static u8 QueryParticleBlendMode(LPCSTR shaderName) return (id < passes::PARTICLE_BLEND_COUNT) ? (u8)id : passes::PARTICLE_BLEND_BLEND; } +// Particle defs store classic multi-slot texture lists as "s_base[,s_distort]". +// Loading the whole string fails; particle_distort samples the 2nd slot. +static shared_str ResolveParticleTextureName(const shared_str& texList, bool preferDistortSlot) +{ + if (!texList.size() || !texList[0]) + return texList; + + const char* p = texList.c_str(); + const char* comma = strchr(p, ','); + if (!comma) + return texList; + + auto trimCopy = [](const char* begin, const char* end) -> shared_str { + while (begin < end && (*begin == ' ' || *begin == '\t')) + ++begin; + while (end > begin && (end[-1] == ' ' || end[-1] == '\t')) + --end; + if (begin >= end) + return shared_str(); + return shared_str(xr_string(begin, end - begin).c_str()); + }; + + shared_str first = trimCopy(p, comma); + const char* secondStart = comma + 1; + while (*secondStart == ' ' || *secondStart == '\t') + ++secondStart; + shared_str second = (*secondStart) ? shared_str(secondStart) : shared_str(); + + if (preferDistortSlot && second.size()) + return second; + return first.size() ? first : texList; +} + void FrameGraphRenderer::ProcessSingleParticleEffect( fg::PS::CParticleEffect* pEffect, const Fmatrix& worldTransform, @@ -2267,11 +2877,17 @@ void FrameGraphRenderer::ProcessSingleParticleEffect( batch.particleCount = particleCount; batch.blendMode = QueryParticleBlendMode(pDef->m_ShaderName.c_str()); - if (strstr(pDef->m_ShaderName.c_str(), "distort")) + const bool isDistort = pDef->m_ShaderName.c_str() && + strstr(pDef->m_ShaderName.c_str(), "distort") != nullptr; + if (isDistort) batch.shaderVariant = passes::ParticleShaderVariant::Distort; if (m_materialCache && pDef->m_TextureName.size()) - batch.bindlessMaterialID = m_materialCache->PreRegisterParticleMaterial(pDef->m_TextureName); + { + shared_str tex = ResolveParticleTextureName(pDef->m_TextureName, isDistort); + batch.bindlessMaterialID = m_materialCache->PreRegisterParticleMaterial( + tex, batch.blendMode); + } if (isHUDParticle) m_hudParticleBatches.push_back(batch); @@ -2396,68 +3012,186 @@ void FrameGraphRenderer::ExtractStaticLeafVisuals(dxRender_Visual* pVisual, xr_v ForEachLeafVisual(pVisual, [&outLeafs](dxRender_Visual* leaf) { outLeafs.push_back(leaf); }); } -void FrameGraphRenderer::CollectVisibleGeometry() { - if (!g_pGamePersistent) - return; - - const auto& sectors = scene_info::GetSceneSectors(); - u32 submittedStatic = 0; +bool FrameGraphRenderer::EnsureSectorStaticCache(size_t sectorIndex, const xr_vector& sectors) +{ + if (sectorIndex >= sectors.size()) + return false; + if (sectorIndex >= m_sectorCacheReady.size()) + return false; + if (m_sectorCacheReady[sectorIndex]) + return false; - if (!m_staticBatchesCached && !sectors.empty()) { - Msg("* [GeomCache] Building static geometry cache from %zu sectors...", sectors.size()); + CSector* sector = sectors[sectorIndex]; + if (!sector || !sector->root()) + { + m_sectorCacheReady[sectorIndex] = 1; + return false; + } - xr_vector staticVisuals; - xr_set uniqueVisuals; + ZoneScopedN("EnsureSectorStaticCache"); - for (CSector* sector : sectors) { - if (sector && sector->root()) { - ExtractStaticLeafVisuals(sector->root(), staticVisuals); - } - } + xr_vector staticVisuals; + ExtractStaticLeafVisuals(sector->root(), staticVisuals); - for (dxRender_Visual* v : staticVisuals) { - uniqueVisuals.insert(v); - } + auto& sectorIds = m_sectorStaticBatchIds[sectorIndex]; + sectorIds.clear(); - u32 batchCountBefore = static_cast(m_geometryCollector->GetBatches().size()); + for (dxRender_Visual* visual : staticVisuals) + { + if (!visual) + continue; - for (dxRender_Visual* visual : uniqueVisuals) { + auto it = m_visualCacheBatchIds.find(visual); + if (it == m_visualCacheBatchIds.end()) + { Fmatrix xform = Fidentity; - - switch (visual->getType()) { + switch (visual->getType()) + { case MT_TREE_ST: case MT_TREE_PM: { - FTreeVisual* treeVisual = static_cast(visual); + auto* treeVisual = static_cast(visual); xform = treeVisual->xform; break; } default: - xform = Fidentity; break; } - if (ProcessVisualGeometry(visual, xform, nullptr, true)) { - submittedStatic++; + const u32 before = static_cast(m_geometryCollector->GetBatches().size()); + if (!ProcessVisualGeometry(visual, xform, nullptr, true)) + continue; + const u32 after = static_cast(m_geometryCollector->GetBatches().size()); + + xr_vector ids; + ids.reserve(after - before); + const auto& allBatches = m_geometryCollector->GetBatches(); + for (u32 bi = before; bi < after; ++bi) + { + const u32 cacheId = static_cast(m_cachedStaticBatches.size()); + m_cachedStaticBatches.push_back(allBatches[bi]); + ids.push_back(cacheId); } + it = m_visualCacheBatchIds.emplace(visual, std::move(ids)).first; } - const auto& allBatches = m_geometryCollector->GetBatches(); - m_cachedStaticBatches.assign(allBatches.begin() + batchCountBefore, allBatches.end()); - m_staticBatchesCached = true; + for (u32 bi : it->second) + sectorIds.push_back(bi); + } + + m_sectorCacheReady[sectorIndex] = 1; + return true; +} + +void FrameGraphRenderer::CollectVisibleGeometry() { + if (!g_pGamePersistent) + return; + + // Classic: wait HOM raster before portal traverse / occ_visible + if (m_pProcessHOMTask) + { + ZoneScopedN("CollectVisibleGeometry::WaitHOM"); + TaskScheduler->Wait(*m_pProcessHOMTask); + m_pProcessHOMTask = nullptr; + } + if (!ps_r_portal_cull) + m_HOM.Disable(); + + const auto& sectors = scene_info::GetSceneSectors(); + u32 submittedStatic = 0; + bool justBuiltStaticCache = false; + + // Build full unique static cache once (lazy sector fill caused GPU static + // upload to freeze after the first incomplete frame → see-through indoors). + if (!m_staticCacheInitialized && !sectors.empty()) + { + ZoneScopedN("CollectVisibleGeometry::BuildStaticCache"); + m_sectorStaticBatchIds.assign(sectors.size(), {}); + m_sectorCacheReady.assign(sectors.size(), 0); + m_cachedStaticBatches.clear(); + m_visualCacheBatchIds.clear(); + + for (size_t si = 0; si < sectors.size(); ++si) + EnsureSectorStaticCache(si, sectors); + + m_staticCacheInitialized = true; + justBuiltStaticCache = true; + if (m_gpuCullingManager) + m_gpuCullingManager->InvalidateStaticCullingData(); + Msg("* [GeomCache] Static cache built: %zu unique batches across %zu sectors", + m_cachedStaticBatches.size(), sectors.size()); + } - Msg("* [GeomCache] Cached %zu static batches from %zu unique visuals (total sectors: %zu)", - m_cachedStaticBatches.size(), uniqueVisuals.size(), sectors.size()); + // Portal/sector traverse — optional indoor filter (off by default) + m_portalTraverseActive = false; + if (ps_r_portal_cull) + { + ZoneScopedN("CollectVisibleGeometry::PortalTraverse"); + CFrustum viewFrustum; + viewFrustum.CreateFromMatrix(Device.mFullTransform, FRUSTUM_P_LRTB | FRUSTUM_P_FAR); + + IRender_Sector::sector_id_t sid = Scene.detect_sector(Device.vCameraPosition); + if (sid == IRender_Sector::INVALID_SECTOR_ID) + sid = m_last_sector_id; + else + m_last_sector_id = sid; + + if (sid != IRender_Sector::INVALID_SECTOR_ID && sid < sectors.size() && sectors[sid]) + { + // HOM is an opt-in refinement (r_hom). The CPU raster can false-cull + // portal polys → whole rooms vanish. Frustum-only traversal is the + // stable PVS: it only skips sectors unreachable through visible portals. + const bool useHom = ps_r_hom != 0; + const u32 traverseOptions = useHom ? CPortalTraverser::VQ_HOM : 0u; + if (useHom) + m_HOM.Enable(); + else + m_HOM.Disable(); + PortalTraverser.traverse( + sectors[sid], + viewFrustum, + Device.vCameraPosition, + Device.mFullTransform, + traverseOptions, + useHom ? &m_HOM : nullptr); + m_portalTraverseActive = !PortalTraverser.r_sectors.empty(); + } } - else if (m_staticBatchesCached) { - for (const auto& batch : m_cachedStaticBatches) { + + // Build frame already filled the collector via ProcessVisualGeometry. + if (!justBuiltStaticCache && m_staticCacheInitialized) + { + ZoneScopedN("CollectVisibleGeometry::StaticSubmit"); + xr_vector submitted(m_cachedStaticBatches.size(), 0); + + auto submitBatchId = [&](u32 bi) { + if (bi >= m_cachedStaticBatches.size() || (bi < submitted.size() && submitted[bi])) + return; + if (bi < submitted.size()) + submitted[bi] = 1; + const auto& batch = m_cachedStaticBatches[bi]; + if (batch.visual && !occ_visible(batch.visual->vis)) + return; m_geometryCollector->Submit(batch); submittedStatic++; + }; + + if (m_portalTraverseActive) + { + for (CSector* s : PortalTraverser.r_sectors) + { + if (!s || s->unique_id >= m_sectorStaticBatchIds.size()) + continue; + for (u32 bi : m_sectorStaticBatchIds[s->unique_id]) + submitBatchId(bi); + } + } + else + { + for (u32 bi = 0; bi < m_cachedStaticBatches.size(); ++bi) + submitBatchId(bi); } } - // ═══════════════════════════════════════════════════════ - // PROCESS DYNAMIC GEOMETRY - // ═══════════════════════════════════════════════════════ u32 submittedDynamic = 0; u32 notRenderable = 0; @@ -2475,6 +3209,14 @@ void FrameGraphRenderer::CollectVisibleGeometry() { continue; } + if (m_portalTraverseActive && data.sector_id != IRender_Sector::INVALID_SECTOR_ID && + data.sector_id < sectors.size()) + { + CSector* sec = sectors[data.sector_id]; + if (sec && sec->r_marker != PortalTraverser.i_marker) + continue; + } + IRenderable* renderable = spatial->dcast_Renderable(); if (!renderable) { notRenderable++; @@ -2488,12 +3230,8 @@ void FrameGraphRenderer::CollectVisibleGeometry() { if (!collectedLights.empty()) fg::ClusteredLightManager::Instance().CollectLightsParallel(collectedLights); - // ═══════════════════════════════════════════════════════ - // HUD RENDERING (after dynamic objects) - // ═══════════════════════════════════════════════════════ - if (g_pGameLevel && g_pGameLevel->pHUD) { - g_pGameLevel->pHUD->Render_Last(0); // context_id = 0 (not using legacy contexts) + g_pGameLevel->pHUD->Render_Last(0); } } @@ -2878,7 +3616,16 @@ void FrameGraphRenderer::OnCameraUpdated() ViewBase.CreateFromMatrix(Device.mFullTransform, FRUSTUM_P_LRTB + FRUSTUM_P_FAR); if (g_pGamePersistent->MainMenuActiveOrLevelNotExist()) return; - m_pProcessHOMTask = &m_HOM.DispatchMTRender(); + // Don't run HOM during save/level load — blocks workers and races with loading + if (g_pGamePersistent->IsLoadingScreenShown()) + return; + // HOM only when explicitly opted in (r_hom) alongside portal traversal. + // Without it, portal PVS is frustum-only (stable) and occ_visible() no-ops + // (bEnabled=false → visible), so nothing is HOM-rejected. + if (ps_r_portal_cull && ps_r_hom) + m_pProcessHOMTask = &m_HOM.DispatchMTRender(); + else + m_HOM.Disable(); } namespace @@ -3009,6 +3756,19 @@ void FrameGraphRenderer::create() o.minmax_sm_screenarea_threshold = 1600 * 1200; o.tessellation = ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION); +#if defined(XR_PLATFORM_APPLE) + // Metal tessellation allocates temporary patch buffers in IOGPU. Over-subscription + // (many NPC/level patches × factor) has caused hard kernel panics: + // IOGPUGroupMemory::remove_memory_object() memory object not found + // Parallax (r2_steep_parallax) is the supported relief path on macOS. + if (o.tessellation || ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION)) + { + Msg("! [Tessellation] Forcibly disabled on Apple/MoltenVK — Metal tess temp " + "buffers can panic the kernel (IOGPUGroupMemory). Use r2_steep_parallax for relief."); + ps_r2_ls_flags_ext.set(R2FLAGEXT_ENABLE_TESSELLATION, FALSE); + o.tessellation = false; + } +#endif o.support_rt_arrays = true; if (o.minmax_sm == FrameGraphRenderer::MMSM_AUTODETECT) @@ -3172,6 +3932,13 @@ void FrameGraphRenderer::RequestGrassInteraction(const Fvector& world_pos, float { } +bool FrameGraphRenderer::SampleTerrainHeight(float x, float z, float& outY) +{ + if (!m_detailManager) + return false; + return m_detailManager->SampleHeight(x, z, outY); +} + void FrameGraphRenderer::DumpStatistics(IGameFont& font, IPerformanceAlert* alert) { FGRenderBase::DumpStatistics(font, alert); diff --git a/src/Layers/xrRender/r_FrameGraphRenderer.h b/src/Layers/xrRender/r_FrameGraphRenderer.h index a416c29f090..a57578df049 100644 --- a/src/Layers/xrRender/r_FrameGraphRenderer.h +++ b/src/Layers/xrRender/r_FrameGraphRenderer.h @@ -27,6 +27,7 @@ struct ImDrawData; namespace xray::render::fg { class IRender_DetailModel; +class CSector; struct ShaderMacro { @@ -43,6 +44,7 @@ namespace xray::render::fg { class RTAccelStructManager; class CRenderTarget; class light; + class VolumetricRenderer; namespace PS { class CParticleEffect; } @@ -225,6 +227,7 @@ class FrameGraphRenderer: public xray::render::fg::FGRenderBase { void Screenshot(IRender::ScreenshotMode mode, pcstr name) override; void SetPostProcessParams(const SPPInfo&) override; void RequestGrassInteraction(const Fvector&, float, float, uint8_t) override; + bool SampleTerrainHeight(float x, float z, float& outY) override; // Initialize bool Initialize(fg::RenderDevice* device); @@ -529,6 +532,9 @@ class FrameGraphRenderer: public xray::render::fg::FGRenderBase { // Smoke Trail Manager (GPU weapon muzzle smoke) xr_unique_ptr m_smokeTrailManager; + // Froxel volumetric fog (MVP: world fog + sun) + xr_unique_ptr m_volumetricRenderer; + // Ray Tracing acceleration structures (for path tracer) xr_unique_ptr m_rtAccelMgr; u32 m_ptSampleIndex = 0; @@ -552,10 +558,17 @@ class FrameGraphRenderer: public xray::render::fg::FGRenderBase { // ═══════════════════════════════════════════════════════ // STATIC GEOMETRY CACHE (collected once, reused every frame) // ═══════════════════════════════════════════════════════ - // Static geometry from sector hierarchies doesn't change - cache it! - // Only dynamic objects (from spatial DB) need per-frame collection + // Static geometry: unique batches + per-sector index lists (built once after load) xr_vector m_cachedStaticBatches; - bool m_staticBatchesCached = false; + xr_vector> m_sectorStaticBatchIds; + xr_vector m_sectorCacheReady; + xr_map> m_visualCacheBatchIds; + bool m_staticCacheInitialized = false; + bool m_portalTraverseActive = false; + + // Ensure sector static batches exist. Returns true if built this call + // (batches already pushed into the live collector via ProcessVisualGeometry). + bool EnsureSectorStaticCache(size_t sectorIndex, const xr_vector& sectors); // RenderContext for execution xr_unique_ptr m_renderContext; diff --git a/src/Layers/xrRender/r_FrameGraphRenderer_Loader.cpp b/src/Layers/xrRender/r_FrameGraphRenderer_Loader.cpp index 88b58677d3f..3f89bb3c4cd 100644 --- a/src/Layers/xrRender/r_FrameGraphRenderer_Loader.cpp +++ b/src/Layers/xrRender/r_FrameGraphRenderer_Loader.cpp @@ -164,14 +164,15 @@ void FrameGraphRenderer::level_Load(IReader* fs) g_pGamePersistent->LoadTitle("st_loading_sectors_portals"); LoadSectors(fs); - // HOM - Skip if using FrameGraph renderer (GPU Hi-Z culling replaces CPU HOM) - if (!true) + // HOM - classic occlusion map (GPU Hi-Z complements, does not replace) + if (!strstr(Core.Params, "-no_hom")) { m_HOM.Load(); + m_HOM.Enable(); } else { - Msg("* [FrameGraph] Skipping HOM load - using GPU Hi-Z culling instead"); + Msg("* [FrameGraph] Skipping HOM load (-no_hom)"); } // Lights diff --git a/src/Layers/xrRender/r__pixel_calculator.cpp b/src/Layers/xrRender/r__pixel_calculator.cpp index 06467f338c9..feb98e97466 100644 --- a/src/Layers/xrRender/r__pixel_calculator.cpp +++ b/src/Layers/xrRender/r__pixel_calculator.cpp @@ -28,8 +28,7 @@ r_aabb_ssa r_pixel_calculator::calculate(dxRender_Visual* V) void r_pixel_calculator::run() { - // TODO: Implement via NVRHI - Log("----- ssa build not yet implemented for FrameGraph -----"); + // SSA pixel calculator not ported to FrameGraph yet; no-op (avoid log spam). } } // namespace xray::render::fg diff --git a/src/Layers/xrRender/r__scene.cpp b/src/Layers/xrRender/r__scene.cpp index f74da2b2dfa..c4572be3d37 100644 --- a/src/Layers/xrRender/r__scene.cpp +++ b/src/Layers/xrRender/r__scene.cpp @@ -30,9 +30,7 @@ void R_scene_geometry::load(const xr_vector& secto for (u32 idx = 0; idx < sectors_count; ++idx) { auto* sector = xr_new(); - sector->unique_id = static_cast(idx); - sector->setup(sectors_data[idx]); Sectors[idx] = sector; } @@ -41,6 +39,10 @@ void R_scene_geometry::load(const xr_vector& secto auto* portal = static_cast(Portals[idx]); portal->setup(portals_data[idx], Sectors); } + + // Link portal pointers into sectors after portals exist + for (u32 idx = 0; idx < sectors_count; ++idx) + Sectors[idx]->setup(sectors_data[idx], Portals); } void R_scene_geometry::unload() diff --git a/src/Layers/xrRender/r__sector.cpp b/src/Layers/xrRender/r__sector.cpp index c13b50c8a6f..e93cb35a127 100644 --- a/src/Layers/xrRender/r__sector.cpp +++ b/src/Layers/xrRender/r__sector.cpp @@ -89,6 +89,8 @@ void CPortal::setup(const level_portal_data_t& data, const xr_vector& poly.assign(V, vcnt); pFace = face; pBack = back; + marker = 0xffffffff; + bDualRender = FALSE; Fvector N, T; N.set(0, 0, 0); @@ -114,8 +116,19 @@ void CPortal::setup(const level_portal_data_t& data, const xr_vector& */ } -void CSector::setup(const level_sector_data_t& data) +void CSector::setup(const level_sector_data_t& data, const xr_vector& portals) { + const auto num_portals = data.portals_id.size(); + m_portals.clear(); + m_portals.reserve(num_portals); + for (u32 idx = 0; idx < num_portals; ++idx) + { + const auto ID = data.portals_id[idx]; + if (ID >= portals.size() || !portals[ID]) + continue; + m_portals.push_back(portals[ID]); + } + if (GEnv.isDedicatedServer) m_root = nullptr; else diff --git a/src/Layers/xrRender/r__sector.h b/src/Layers/xrRender/r__sector.h index a523db463fb..53571dbd056 100644 --- a/src/Layers/xrRender/r__sector.h +++ b/src/Layers/xrRender/r__sector.h @@ -4,6 +4,7 @@ #pragma once #include "xrCore/_fbox2.h" +#include "xrCDB/Frustum.h" namespace CDB { class MODEL; } class xrXRC; @@ -12,7 +13,12 @@ namespace xray::render::fg { class CPortal; class CSector; +class CHOM; +struct _scissor : public Fbox2 +{ + float depth = 0.f; +}; class CPortal : public IRender_Portal #ifdef DEBUG @@ -36,9 +42,15 @@ class CPortal : public IRender_Portal public: Fplane P; Fsphere S; + u32 marker = 0xffffffff; + BOOL bDualRender = FALSE; void setup(const level_portal_data_t& data, const xr_vector& sectors); + Poly& getPoly() { return poly; } + CSector* Back() { return pBack; } + CSector* Front() { return pFace; } + CSector* getSector(CSector* pFrom) { return pFrom == pFace ? pBack : pFace; } CSector* getSectorFacing(const Fvector& V) { if (P.classify(V) > 0) @@ -46,6 +58,15 @@ class CPortal : public IRender_Portal else return pBack; } + CSector* getSectorBack(const Fvector& V) + { + if (P.classify(V) > 0) + return pBack; + else + return pFace; + } + float distance(const Fvector& V) { return _abs(P.classify(V)); } + CPortal(); virtual ~CPortal(); @@ -69,10 +90,49 @@ class CSector : public IRender_Sector dxRender_Visual* m_root; public: + xr_vector m_portals; + xr_vector r_frustums; + xr_vector<_scissor> r_scissors; + _scissor r_scissor_merged; + u32 r_marker = 0; + dxRender_Visual* root() { return m_root; } - void setup(const level_sector_data_t& data); + void setup(const level_sector_data_t& data, const xr_vector& portals); CSector() { m_root = nullptr; } virtual ~CSector() = default; }; + +class CPortalTraverser +{ +public: + enum + { + VQ_HOM = (1 << 0), + VQ_SSA = (1 << 1), + VQ_SCISSOR = (1 << 2), + VQ_FADE = (1 << 3), + }; + +public: + u32 i_marker = 0xffffffff; + u32 i_options = 0; + Fvector i_vBase; + Fmatrix i_mXFORM; + Fmatrix i_mXFORM_01; + CSector* i_start = nullptr; + CHOM* i_hom = nullptr; + u32 i_recurseDepth = 0; + static constexpr u32 kMaxPortalRecurse = 256; + xr_vector r_sectors; + xr_vector> f_portals; + +public: + CPortalTraverser(); + void traverse(IRender_Sector* start, CFrustum& F, Fvector& vBase, Fmatrix& mXFORM, u32 options, CHOM* hom = nullptr); + void traverse_sector(CSector* sector, CFrustum& F, _scissor& R); + void fade_portal(CPortal* _p, float ssa); +}; + +extern CPortalTraverser PortalTraverser; } // namespace xray::render::fg diff --git a/src/Layers/xrRender/r__sector_traversal.cpp b/src/Layers/xrRender/r__sector_traversal.cpp new file mode 100644 index 00000000000..31cea3f4b35 --- /dev/null +++ b/src/Layers/xrRender/r__sector_traversal.cpp @@ -0,0 +1,191 @@ +#include "stdafx.h" +#include "r__sector.h" +#include "Layers/xrRender/HOM.h" + +namespace xray::render::fg +{ +CPortalTraverser PortalTraverser; + +CPortalTraverser::CPortalTraverser() { i_marker = 0xffffffff; } + +void CPortalTraverser::traverse(IRender_Sector* start, CFrustum& F, Fvector& vBase, Fmatrix& mXFORM, u32 options, CHOM* hom) +{ + ZoneScopedN("PortalTraverser::traverse"); + + Fmatrix m_viewport_01 = { + 1.f / 2.f, 0.0f, 0.0f, 0.0f, + 0.0f, -1.f / 2.f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 1.f / 2.f, 1.f / 2.f, 0.0f, 1.0f}; + + if (options & VQ_FADE) + { + f_portals.clear(); + f_portals.reserve(16); + } + + if (!start) + { + r_sectors.clear(); + return; + } + + i_marker++; + if (i_marker == 0) + i_marker = 1; + i_options = options; + i_vBase = vBase; + i_mXFORM = mXFORM; + i_mXFORM_01.mul(m_viewport_01, mXFORM); + i_start = static_cast(start); + i_hom = hom; + i_recurseDepth = 0; + r_sectors.clear(); + + _scissor scissor; + scissor.set(0, 0, 1, 1); + scissor.depth = 0; + traverse_sector(i_start, F, scissor); + + if (options & VQ_SCISSOR) + { + for (u32 s = 0; s < r_sectors.size(); s++) + { + CSector* S = r_sectors[s]; + S->r_scissor_merged.invalidate(); + S->r_scissor_merged.depth = flt_max; + for (u32 it = 0; it < S->r_scissors.size(); it++) + { + S->r_scissor_merged.merge(S->r_scissors[it]); + if (S->r_scissors[it].depth < S->r_scissor_merged.depth) + S->r_scissor_merged.depth = S->r_scissors[it].depth; + } + } + } +} + +void CPortalTraverser::fade_portal(CPortal* _p, float ssa) { f_portals.emplace_back(_p, ssa); } + +void CPortalTraverser::traverse_sector(CSector* sector, CFrustum& F, _scissor& R_scissor) +{ + if (!sector) + return; + if (i_recurseDepth >= kMaxPortalRecurse) + return; + ++i_recurseDepth; + + if (sector->r_marker != i_marker) + { + sector->r_marker = i_marker; + r_sectors.push_back(sector); + sector->r_frustums.clear(); + sector->r_scissors.clear(); + } + sector->r_frustums.push_back(F); + sector->r_scissors.push_back(R_scissor); + + sPoly S, D; + for (u32 I = 0; I < sector->m_portals.size(); I++) + { + if (!sector->m_portals[I]) + continue; + if (sector->m_portals[I]->marker == i_marker) + continue; + + CPortal* PORTAL = sector->m_portals[I]; + CSector* pSector = nullptr; + + if (PORTAL->bDualRender) + { + pSector = PORTAL->getSector(sector); + } + else + { + pSector = PORTAL->getSectorBack(i_vBase); + if (pSector == sector) + continue; + if (pSector == i_start) + continue; + } + + if (!F.testSphere_dirty(PORTAL->S.P, PORTAL->S.R)) + continue; + + CPortal::Poly& POLY = PORTAL->getPoly(); + S.assign(&*POLY.begin(), POLY.size()); + D.clear(); + sPoly* P = F.ClipPoly(S, D); + if (!P) + continue; + + _scissor scissor; + if ((i_options & CPortalTraverser::VQ_SCISSOR) && (!PORTAL->bDualRender)) + { + Fbox2 bb; + bb.invalidate(); + float depth = flt_max; + sPoly& p = *P; + for (u32 vit = 0; vit < p.size(); vit++) + { + Fvector4 t; + Fmatrix& M = i_mXFORM_01; + Fvector& v = p[vit]; + + t.x = v.x * M._11 + v.y * M._21 + v.z * M._31 + M._41; + t.y = v.x * M._12 + v.y * M._22 + v.z * M._32 + M._42; + t.z = v.x * M._13 + v.y * M._23 + v.z * M._33 + M._43; + t.w = v.x * M._14 + v.y * M._24 + v.z * M._34 + M._44; + t.mul(1.f / t.w); + + if (t.x < bb.min.x) + bb.min.x = t.x; + if (t.x > bb.max.x) + bb.max.x = t.x; + if (t.y < bb.min.y) + bb.min.y = t.y; + if (t.y > bb.max.y) + bb.max.y = t.y; + if (t.z < depth) + depth = t.z; + } + + if (depth < EPS) + { + scissor = R_scissor; + if ((i_options & CPortalTraverser::VQ_HOM) && i_hom && (!i_hom->visible(*P))) + continue; + } + else + { + scissor.min.x = _max(bb.min.x, R_scissor.min.x); + scissor.min.y = _max(bb.min.y, R_scissor.min.y); + scissor.max.x = _min(bb.max.x, R_scissor.max.x); + scissor.max.y = _min(bb.max.y, R_scissor.max.y); + scissor.depth = depth; + + if (scissor.min.x >= scissor.max.x) + continue; + if (scissor.min.y >= scissor.max.y) + continue; + + if ((i_options & CPortalTraverser::VQ_HOM) && i_hom && + !i_hom->visible(scissor, depth)) + continue; + } + } + else + { + scissor = R_scissor; + if ((i_options & CPortalTraverser::VQ_HOM) && i_hom && (!i_hom->visible(*P))) + continue; + } + + CFrustum Clip; + Clip.CreateFromPortal(P, PORTAL->P.n, i_vBase, i_mXFORM); + PORTAL->marker = i_marker; + PORTAL->bDualRender = FALSE; + traverse_sector(pSector, Clip, scissor); + } + --i_recurseDepth; +} +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/xrRender_console.cpp b/src/Layers/xrRender/xrRender_console.cpp index fe4fa10eb65..f85b153c7cf 100644 --- a/src/Layers/xrRender/xrRender_console.cpp +++ b/src/Layers/xrRender/xrRender_console.cpp @@ -45,7 +45,7 @@ const xr_token qpreset_token[] = { nullptr, 0 } }; -u32 ps_r2_smapsize = 2048; +u32 ps_r2_smapsize = 1024; const xr_token qsmapsize_token[] = { #if !defined(MASTER_GOLD) || RENDER == R_R1 @@ -75,17 +75,19 @@ const xr_token qsmapsize_token[] = { nullptr, 0 } }; -u32 ps_r_ssao_mode = ssao_mode_default; +u32 ps_r_ssao_mode = ssao_mode_gtao; const xr_token qssao_mode_token[] = { { "disabled", ssao_mode_off }, { "default", ssao_mode_default }, { "hdao", ssao_mode_hdao }, { "hbao", ssao_mode_hbao }, + { "gtao", ssao_mode_gtao }, { nullptr, 0 } }; u32 ps_r_sun_shafts = 2; +float ps_r_sun_shafts_scale = 1.0f; // brightness multiplier for volumetric sun (1 = faithful to original) const xr_token qsun_shafts_token[] = {{"st_opt_off", 0}, {"st_opt_low", 1}, {"st_opt_medium", 2}, {"st_opt_high", 3}, {nullptr, 0}}; u32 ps_r_ssao = 3; @@ -94,6 +96,21 @@ const xr_token qssao_token[] = {{"st_opt_off", 0}, {"st_opt_low", 1}, {"st_opt_m {nullptr, 0}}; u32 ps_r_sun_quality = 1; // = 0; +int ps_r_shadow_debug = 0; // 0=off,1=cascade idx,2=CSM term,3=cascade UV,4=N.L +float ps_r2_sun_normal_bias = 0.04f; // world-space normal offset (meters) for CSM +int ps_r_shadow_cast_all = 0; // 1=legacy cast-all near; 0=camera compact / light-frustum +int ps_r_skinned_shadows = 1; // 1=NPCs/mutants cast into the sun CSM (self-shadowing), 0=off +int ps_r_depth_prepass = 1; // early-Z; enables same-frame Hi-Z path when r_hiz_occlusion +int ps_r_hiz_occlusion = 1; // 1=static occlusion compact after same-frame Hi-Z (depth prepass) +int ps_r_shadow_light_cull = 1; // 1=CPU light-frustum CSM casters (DRAWINDEX compact) +int ps_r_local_shadows = 1; // 1=spot/OMNIPART local shadow atlas (expensive: N× BuildLightFrustumCasters) +int ps_r_local_shadow_tiles = 12; // max atlas slices (hard cap MAX_LOCAL_SHADOW_TILES=16) +int ps_r_shadow_indoor_near_only = 1; // 1=few portal sectors → only cascade 0 +int ps_r_portal_cull = 1; // 1=portal/sector PVS traversal (frustum) for static submit +int ps_r_hom = 0; // 1=add CPU HOM occlusion on top of portal traversal (opt-in refinement) +float ps_r2_sun_soft = 6.0f; // PCSS max penumbra (texels): larger = softer shadows far from occluder +float ps_r2_sun_blocker = 2.5f; // PCSS blocker-search spacing (texels): larger = detects distant occluders +float ps_r2_sun_contact = 1.0f; // PCSS min penumbra (texels): smaller = sharper contact shadow const xr_token qsun_quality_token[] = {{"st_opt_low", 0}, {"st_opt_medium", 1}, {"st_opt_high", 2}, #if defined(USE_DX11) // TODO: OGL: fix ultra and extreme settings {"st_opt_ultra", 3}, {"st_opt_extreme", 4}, @@ -187,15 +204,19 @@ float ps_r2_ssaLOD_B = 48.f; Flags32 ps_r2_ls_flags = {R2FLAG_SUN //| R2FLAG_SUN_IGNORE_PORTALS | R2FLAG_EXP_DONT_TEST_UNSHADOWED | R2FLAG_USE_NVSTENCIL | R2FLAG_EXP_SPLIT_SCENE | R2FLAG_EXP_MT_CALC | - R3FLAG_DYN_WET_SURF | R3FLAG_VOLUMETRIC_SMOKE + R3FLAG_DYN_WET_SURF + // R3FLAG_VOLUMETRIC_SMOKE — froxel sun shafts; off by default (classic CoP has no volumetric rays) //| R3FLAG_MSAA //| R3FLAG_MSAA_OPT | R3FLAG_GBUFFER_OPT | R2FLAG_DETAIL_BUMP | R2FLAG_DOF | R2FLAG_SOFT_PARTICLES | R2FLAG_SOFT_WATER | - R2FLAG_STEEP_PARALLAX | R2FLAG_SUN_FOCUS | R2FLAG_SUN_TSM | R2FLAG_TONEMAP | R2FLAG_VOLUMETRIC_LIGHTS}; // r2-only + R2FLAG_STEEP_PARALLAX | R2FLAG_SUN_FOCUS | R2FLAG_SUN_TSM | R2FLAG_TONEMAP}; // r2-only + // R2FLAG_VOLUMETRIC_LIGHTS also off by default (same reason) Flags32 ps_r2_ls_flags_ext = { - /*R2FLAGEXT_SSAO_OPT_DATA |*/ R2FLAGEXT_SSAO_HALF_DATA | R2FLAGEXT_ENABLE_TESSELLATION | R3FLAGEXT_SSR_HALF_DEPTH | - R3FLAGEXT_SSR_JITTER}; + /*R2FLAGEXT_SSAO_OPT_DATA |*/ R2FLAGEXT_SSAO_HALF_DATA | + /* R2FLAGEXT_ENABLE_TESSELLATION — off by default: Apple Metal/MoltenVK tess + temp buffers can kernel-panic (IOGPUGroupMemory::remove_memory_object). */ + R3FLAGEXT_SSR_HALF_DEPTH | R3FLAGEXT_SSR_JITTER}; float ps_r2_df_parallax_h = 0.02f; float ps_r2_df_parallax_range = 75.f; @@ -252,8 +273,8 @@ Fvector3 ps_r2_dof = Fvector3().set(-1.25f, 1.4f, 600.f); float ps_r2_dof_sky = 30; // distance to sky float ps_r2_dof_kernel_size = 5.0f; // 7.0f -float ps_r3_dyn_wet_surf_near = 5.f; // 10.0f -float ps_r3_dyn_wet_surf_far = 20.f; // 30.0f +float ps_r3_dyn_wet_surf_near = 10.f; // view-space start falloff +float ps_r3_dyn_wet_surf_far = 50.f; // view-space end falloff (was 20 — too short) int ps_r3_dyn_wet_surf_sm_res = 256; // 256 // R4 Debug @@ -274,6 +295,8 @@ const xr_token fg_render_mode_token[] = { // Smoke Trail (weapon muzzle smoke) int ps_r_smoke_trail_enabled = 1; +// Ribbon/Trail test passes (debug quads) — off by default, they emit no geometry +int ps_r_test_trails = 0; float ps_r_smoke_max_emit_rate = 45.f; float ps_r_smoke_point_lifetime = 2.5f; float ps_r_smoke_max_width = 0.04f; @@ -283,7 +306,7 @@ float ps_r_smoke_turbulence = 0.8f; u32 ps_steep_parallax = 0; int ps_r__detail_radius = 49; -int ps_r__detail_gpu = 1; // 0=Vanilla CPU path, 1=GPU compute path (default GPU) +int ps_r__detail_gpu = 0; // 0=CoP textured billboards (GPU cull), 1=procedural green blades u32 dm_size = 24; u32 dm_cache1_line = 12; //dm_size*2/dm_cache1_count @@ -453,6 +476,18 @@ class CCC_SSAO_Mode : public CCC_Token ps_r2_ls_flags_ext.set(R2FLAGEXT_SSAO_OPT_DATA, 1); break; } + case ssao_mode_gtao: + { + if (ps_r_ssao == 0) + { + ps_r_ssao = 1; + } + ps_r2_ls_flags_ext.set(R2FLAGEXT_SSAO_HBAO, 0); + ps_r2_ls_flags_ext.set(R2FLAGEXT_SSAO_HDAO, 0); + ps_r2_ls_flags_ext.set(R2FLAGEXT_SSAO_OPT_DATA, 0); + ps_r2_ls_flags_ext.set(R2FLAGEXT_SSAO_HALF_DATA, 0); + break; + } } } }; @@ -729,7 +764,7 @@ void xrRender_initconsole() CMD4(CCC_Float, "r__detail_density", &ps_current_detail_density/*&ps_r__Detail_density*/, 0.04f, 0.99f); CMD4(CCC_detail_radius, "r__detail_radius", &ps_r__detail_radius, 49, 600); CMD4(CCC_Float, "r__detail_height", &ps_r__Detail_height, 1, 2); - CMD4(CCC_Integer, "r__detail_gpu", &ps_r__detail_gpu, 0, 1); // Toggle GPU compute path + CMD4(CCC_Integer, "r__detail_gpu", &ps_r__detail_gpu, 0, 1); // 0=CoP billboards+GPU cull, 1=procedural blades #ifdef DEBUG CMD4(CCC_Float, "r__detail_l_ambient", &ps_r__Detail_l_ambient, .5f, .95f); @@ -817,7 +852,7 @@ void xrRender_initconsole() CMD4(CCC_Float, "r2_sun_tsm_bias", &ps_r2_sun_tsm_bias, -0.5, +0.5); CMD4(CCC_Float, "r2_sun_near", &ps_r2_sun_near, 1.f, 150.f); //AVO: extended from 50.f to 150.f #if RENDER != R_R1 - CMD4(CCC_Float, "r2_sun_far", &ps_r2_sun_far, 51.f, 180.f); + CMD4(CCC_Float, "r2_sun_far", &ps_r2_sun_far, 51.f, 400.f); #endif CMD4(CCC_Float, "r2_sun_near_border", &ps_r2_sun_near_border, .5f, 1.0f); CMD4(CCC_Float, "r2_sun_depth_far_scale", &ps_r2_sun_depth_far_scale, 0.5, 1.5); @@ -893,6 +928,7 @@ void xrRender_initconsole() CMD3(CCC_Mask, "r2_volumetric_lights", &ps_r2_ls_flags, R2FLAG_VOLUMETRIC_LIGHTS); //CMD3(CCC_Mask, "r2_sun_shafts", &ps_r2_ls_flags, R2FLAG_SUN_SHAFTS); CMD3(CCC_Token, "r2_sun_shafts", &ps_r_sun_shafts, qsun_shafts_token); + CMD4(CCC_Float, "r2_sun_shafts_scale", &ps_r_sun_shafts_scale, 0.0f, 4.0f); CMD3(CCC_SSAO_Mode, "r2_ssao_mode", &ps_r_ssao_mode, qssao_mode_token); CMD3(CCC_Token, "r2_ssao", &ps_r_ssao, qssao_token); CMD3(CCC_Mask, "r2_ssao_blur", &ps_r2_ls_flags_ext, R2FLAGEXT_SSAO_BLUR); // Need restart @@ -900,7 +936,7 @@ void xrRender_initconsole() CMD3(CCC_Mask, "r2_ssao_half_data", &ps_r2_ls_flags_ext, R2FLAGEXT_SSAO_HALF_DATA); // Need restart CMD3(CCC_Mask, "r2_ssao_hbao", &ps_r2_ls_flags_ext, R2FLAGEXT_SSAO_HBAO); // Need restart CMD3(CCC_Mask, "r2_ssao_hdao", &ps_r2_ls_flags_ext, R2FLAGEXT_SSAO_HDAO); // Need restart - CMD3(CCC_Mask, "r4_enable_tessellation", &ps_r2_ls_flags_ext, R2FLAGEXT_ENABLE_TESSELLATION); // Need restart + CMD3(CCC_Mask, "r4_enable_tessellation", &ps_r2_ls_flags_ext, R2FLAGEXT_ENABLE_TESSELLATION); // Need restart; UNSAFE on Apple/MoltenVK (IOGPU kernel panic) CMD3(CCC_Mask, "r4_wireframe", &ps_r2_ls_flags_ext, R2FLAGEXT_WIREFRAME); // Need restart CMD3(CCC_Mask, "r2_steep_parallax", &ps_r2_ls_flags, R2FLAG_STEEP_PARALLAX); CMD3(CCC_Mask, "r2_detail_bump", &ps_r2_ls_flags, R2FLAG_DETAIL_BUMP); @@ -959,8 +995,42 @@ void xrRender_initconsole() CMD4(CCC_Integer, "r_rt_gi", &ps_r_rt_gi, 0, 1); CMD4(CCC_Float, "r_rt_gi_intensity", &ps_r_rt_gi_intensity, 0.0f, 4.0f); + CMD4(CCC_Integer, "r_taa", &ps_r_taa, 0, 1); + CMD4(CCC_Float, "r_taa_sharpness", &ps_r_taa_sharpness, 0.0f, 1.0f); + CMD4(CCC_Integer, "r_bloom", &ps_r_bloom, 0, 1); + CMD4(CCC_Integer, "r_cas", &ps_r_cas, 0, 1); + CMD4(CCC_Float, "r_cas_sharpness", &ps_r_cas_sharpness, 0.0f, 1.0f); + CMD4(CCC_Integer, "r_shadow_debug", &ps_r_shadow_debug, 0, 4); + CMD4(CCC_Float, "r2_sun_normal_bias", &ps_r2_sun_normal_bias, 0.0f, 0.5f); + CMD4(CCC_Integer, "r_shadow_cast_all", &ps_r_shadow_cast_all, 0, 1); + CMD4(CCC_Integer, "r_skinned_shadows", &ps_r_skinned_shadows, 0, 1); + CMD4(CCC_Integer, "r_depth_prepass", &ps_r_depth_prepass, 0, 1); + CMD4(CCC_Integer, "r_hiz_occlusion", &ps_r_hiz_occlusion, 0, 1); + CMD4(CCC_Integer, "r_shadow_light_cull", &ps_r_shadow_light_cull, 0, 1); + CMD4(CCC_Integer, "r_local_shadows", &ps_r_local_shadows, 0, 1); + CMD4(CCC_Integer, "r_local_shadow_tiles", &ps_r_local_shadow_tiles, 1, 16); + CMD4(CCC_Integer, "r_shadow_indoor_near_only", &ps_r_shadow_indoor_near_only, 0, 1); + CMD4(CCC_Integer, "r_portal_cull", &ps_r_portal_cull, 0, 1); + CMD4(CCC_Integer, "r_hom", &ps_r_hom, 0, 1); + CMD4(CCC_Float, "r2_sun_soft", &ps_r2_sun_soft, 0.5f, 24.0f); + CMD4(CCC_Float, "r2_sun_blocker", &ps_r2_sun_blocker, 0.5f, 8.0f); + CMD4(CCC_Float, "r2_sun_contact", &ps_r2_sun_contact, 0.1f, 4.0f); + CMD4(CCC_Integer, "r_contact_shadows", &ps_r_contact_shadows, 0, 1); + CMD4(CCC_Float, "r_contact_shadows_length", &ps_r_contact_shadows_length, 0.05f, 2.0f); + CMD4(CCC_Integer, "r_ssr", &ps_r_ssr, 0, 1); + CMD4(CCC_Integer, "r_ssr_quality", &ps_r_ssr_quality, 1, 4); + + CMD4(CCC_Integer, "r_sky_ibl", &ps_r_sky_ibl, 0, 1); + CMD4(CCC_Float, "r_sky_ibl_intensity", &ps_r_sky_ibl_intensity, 0.0f, 4.0f); + CMD4(CCC_Integer, "r_foliage_sss", &ps_r_foliage_sss, 0, 1); + CMD4(CCC_Float, "r_foliage_sss_intensity", &ps_r_foliage_sss_intensity, 0.0f, 4.0f); + CMD4(CCC_Integer, "r_ssgi", &ps_r_ssgi, 0, 1); + CMD4(CCC_Integer, "r_ssgi_quality", &ps_r_ssgi_quality, 1, 3); + CMD4(CCC_Float, "r_ssgi_intensity", &ps_r_ssgi_intensity, 0.0f, 4.0f); + // Smoke Trail (weapon muzzle smoke) CMD4(CCC_Integer, "r_smoke_trail", &ps_r_smoke_trail_enabled, 0, 1); + CMD4(CCC_Integer, "r_test_trails", &ps_r_test_trails, 0, 1); CMD4(CCC_Float, "r_smoke_emit_rate", &ps_r_smoke_max_emit_rate, 1.0f, 120.0f); CMD4(CCC_Float, "r_smoke_lifetime", &ps_r_smoke_point_lifetime, 0.5f, 10.0f); CMD4(CCC_Float, "r_smoke_width", &ps_r_smoke_max_width, 0.005f, 0.2f); diff --git a/src/Layers/xrRender/xrRender_console.h b/src/Layers/xrRender/xrRender_console.h index aef0ed2cd86..24c6cd60137 100644 --- a/src/Layers/xrRender/xrRender_console.h +++ b/src/Layers/xrRender/xrRender_console.h @@ -6,6 +6,7 @@ namespace xray::render::fg { // Common extern ECORE_API u32 ps_r_sun_shafts; //= 0; +extern ECORE_API float ps_r_sun_shafts_scale; //= 1.0f (brightness multiplier) extern ECORE_API const xr_token qsun_shafts_token[]; extern ECORE_API u32 ps_r2_smapsize; @@ -23,9 +24,25 @@ enum ssao_mode_default, ssao_mode_hdao, ssao_mode_hbao, + ssao_mode_gtao, }; extern ECORE_API u32 ps_r_sun_quality; // = 0; +extern ECORE_API int ps_r_shadow_debug; // 0=off,1=cascade idx,2=CSM term,3=cascade UV,4=N.L +extern ECORE_API float ps_r2_sun_normal_bias; // world-space normal offset (meters) for CSM +extern ECORE_API int ps_r_shadow_cast_all; // 1=legacy cast-all near+mid +extern ECORE_API int ps_r_skinned_shadows; // 1=NPCs/mutants cast into the sun CSM +extern ECORE_API int ps_r_depth_prepass; // 1=opaque depth prepass for early-Z in Forward+ +extern ECORE_API int ps_r_hiz_occlusion; // 1=Hi-Z occlusion in GPU cull, 0=frustum only +extern ECORE_API int ps_r_shadow_light_cull; // 1=light-frustum CSM caster cull +extern ECORE_API int ps_r_local_shadows; // 1=spot/omni local shadow atlas +extern ECORE_API int ps_r_local_shadow_tiles; // max atlas slices (1..16) +extern ECORE_API int ps_r_shadow_indoor_near_only; // 1=indoor portal sets use cascade 0 only +extern ECORE_API int ps_r_portal_cull; // 1=portal/sector visibility filter +extern ECORE_API int ps_r_hom; // 1=CPU HOM occlusion on top of portal traversal +extern ECORE_API float ps_r2_sun_soft; // PCSS max penumbra (texels) +extern ECORE_API float ps_r2_sun_blocker; // PCSS blocker-search spacing (texels) +extern ECORE_API float ps_r2_sun_contact; // PCSS min penumbra (texels) — contact sharpness extern ECORE_API u32 ps_r_water_reflection; // = 0; extern ECORE_API const xr_token qsun_quality_token[]; extern ECORE_API const xr_token qwater_reflection_quality_token[]; @@ -235,6 +252,7 @@ enum // Smoke Trail (weapon muzzle smoke) extern ECORE_API int ps_r_smoke_trail_enabled; +extern ECORE_API int ps_r_test_trails; extern ECORE_API float ps_r_smoke_max_emit_rate; extern ECORE_API float ps_r_smoke_point_lifetime; extern ECORE_API float ps_r_smoke_max_width; diff --git a/src/xrCore/CMakeLists.txt b/src/xrCore/CMakeLists.txt index aadc4604b93..7936614294e 100644 --- a/src/xrCore/CMakeLists.txt +++ b/src/xrCore/CMakeLists.txt @@ -518,6 +518,19 @@ set_target_properties(xrCore PROPERTIES PREFIX "" ) +if (XRAY_ENABLE_TRACY) + target_sources(xrCore PRIVATE + "${CMAKE_SOURCE_DIR}/Externals/tracy/public/TracyClient.cpp" + ) + # TracyClient must not use xrCore PCH / Unity (DEBUG_INFO macro clash) + set_source_files_properties( + "${CMAKE_SOURCE_DIR}/Externals/tracy/public/TracyClient.cpp" + PROPERTIES + SKIP_PRECOMPILE_HEADERS ON + SKIP_UNITY_BUILD_INCLUSION ON + ) +endif() + target_precompile_headers(xrCore PRIVATE stdafx.h diff --git a/src/xrCore/Profiler/Profiler.cpp b/src/xrCore/Profiler/Profiler.cpp index 9681dfcd4cd..ecacaad2a67 100644 --- a/src/xrCore/Profiler/Profiler.cpp +++ b/src/xrCore/Profiler/Profiler.cpp @@ -1,6 +1,9 @@ #include "stdafx.h" #include "Profiler.h" +#include +#include + namespace xray::profiler { @@ -14,6 +17,13 @@ void Initialize() void Shutdown() { +#if XRAY_TRACY_ENABLED + // Stop Tracy worker before static teardown — otherwise libc++ throws + // "mutex lock failed: Invalid argument" when joining a destroyed mutex. + tracy::GetProfiler().RequestShutdown(); + for (int i = 0; i < 200 && !tracy::GetProfiler().HasShutdownFinished(); ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); +#endif // Note: CPUProfiler singleton cleanup happens at program exit Msg("* [Profiler] Shutdown"); } diff --git a/src/xrEngine/Environment_editor.cpp b/src/xrEngine/Environment_editor.cpp index 15189df7a9a..d52c6c2ccce 100644 --- a/src/xrEngine/Environment_editor.cpp +++ b/src/xrEngine/Environment_editor.cpp @@ -20,8 +20,8 @@ ENGINE_API u32 ps_r3_grass_wind_octaves = 5; // FBM octave count ENGINE_API Fvector3 ps_r3_grass_color_tip = {0.35f, 0.45f, 0.18f}; // Blade tip color (vibrant green) ENGINE_API Fvector3 ps_r3_grass_color_base = {0.28f, 0.38f, 0.15f}; // Blade base color (duller brown-green) ENGINE_API float ps_r3_grass_color_variation = 0.15f; // Per-blade color variation (±%) -ENGINE_API Fvector3 ps_r3_grass_sss_color = {0.5f, 0.7f, 0.3f}; // Subsurface scattering tint -ENGINE_API float ps_r3_grass_sss_intensity = 0.25f; // SSS strength +ENGINE_API Fvector3 ps_r3_grass_sss_color = {0.55f, 0.85f, 0.25f}; // Subsurface scattering tint +ENGINE_API float ps_r3_grass_sss_intensity = 0.9f; // SSS strength (CoP billboards) // Per-object-ID color tints (64 grass types max) // Default all to white (1,1,1) = no tint diff --git a/src/xrEngine/IRenderBackend.h b/src/xrEngine/IRenderBackend.h index c53dfb35adc..5a4028380fb 100644 --- a/src/xrEngine/IRenderBackend.h +++ b/src/xrEngine/IRenderBackend.h @@ -107,6 +107,10 @@ class ENGINE_API IRenderBackend bool meshShaders = false; bool rayTracing = false; bool variableRateShading = false; + bool drawIndirectCount = false; // vkCmdDrawIndexedIndirectCount / ExecuteIndirect count + bool multiDrawIndirect = false; + bool descriptorIndexing = false; + bool shaderDrawParameters = false; u32 maxBindlessResources = 0; u32 shaderModel = 50; // 50 = SM5.0, 60 = SM6.0, etc. diff --git a/src/xrEngine/Rain.cpp b/src/xrEngine/Rain.cpp index 19e706b1ae1..3ddcafa3041 100644 --- a/src/xrEngine/Rain.cpp +++ b/src/xrEngine/Rain.cpp @@ -93,6 +93,60 @@ bool CEffect_Rain::RayPick(const Fvector& s, const Fvector& d, float& range, col bRes = g_pGameLevel->ObjectSpace.RayPick(s, d, range, tgt, RQ, E); if (bRes) range = RQ.range; + + // ObjectSpace often misses terrain (heightmap-only). Prefer nearer heightmap hit. + if (GEnv.Render && d.y < -0.01f) + { + const float maxR = range; + float best = maxR; + bool hit = false; + for (int i = 1; i <= 8; ++i) + { + const float t = maxR * (float(i) / 8.f); + Fvector p; + p.mad(s, d, t); + float hy = 0.f; + if (!GEnv.Render->SampleTerrainHeight(p.x, p.z, hy)) + continue; + if (s.y >= hy && p.y <= hy) + { + const float t0 = maxR * (float(i - 1) / 8.f); + Fvector p0; + p0.mad(s, d, t0); + const float denom = (p0.y - p.y); + float frac = (denom > 1e-4f) ? (p0.y - hy) / denom : 1.f; + clamp(frac, 0.f, 1.f); + const float th = t0 + (t - t0) * frac; + if (th > 0.f && th < best) + { + best = th; + hit = true; + } + } + } + if (!hit) + { + float hy = 0.f; + // Walk XZ along the ray to the estimated ground distance + const float guess = _min(maxR, (s.y + 50.f) / _max(-d.y, 0.01f)); + Fvector p; + p.mad(s, d, guess); + if (GEnv.Render->SampleTerrainHeight(p.x, p.z, hy) && s.y > hy) + { + const float th = (s.y - hy) / (-d.y); + if (th > 0.f && th < best) + { + best = th; + hit = true; + } + } + } + if (hit) + { + range = best; + bRes = true; + } + } #endif return bRes; } @@ -168,13 +222,13 @@ void CEffect_Rain::OnFrame() break; } - // ambient sound + // ambient sound — follow camera; keep audible even before hemi probe warms up if (snd_Ambient._feedback()) { - // Fvector sndP; - // sndP.mad (Device.vCameraPosition,Fvector().set(0,1,0),source_offset); - // snd_Ambient.set_position(sndP); - snd_Ambient.set_volume(_max(0.1f, factor) * hemi_factor); + snd_Ambient.set_position(Device.vCameraPosition); + float vol = _max(0.1f, factor); + vol *= _max(0.35f, hemi_factor); + snd_Ambient.set_volume(vol); } } diff --git a/src/xrEngine/Render.h b/src/xrEngine/Render.h index f03d656bc3a..550abec9fc1 100644 --- a/src/xrEngine/Render.h +++ b/src/xrEngine/Render.h @@ -408,6 +408,9 @@ class ENGINE_API IRender // Phase 3: A-Life grass interaction (thread-safe wrapper for calls from A-Life thread) virtual void RequestGrassInteraction(const Fvector& world_pos, float radius, float strength, uint8_t type = 0) = 0; + // Rain splash / wet: sample baked detail heightmap (XZ → Y). Default: unavailable. + virtual bool SampleTerrainHeight(float /*x*/, float /*z*/, float& /*outY*/) { return false; } + virtual void RenderStatsOverlay() {} virtual void RenderPBRConversionUI() {} virtual void SetEnabled(bool) {} diff --git a/src/xrEngine/thunderbolt.cpp b/src/xrEngine/thunderbolt.cpp index 31167677791..a2c174fba13 100644 --- a/src/xrEngine/thunderbolt.cpp +++ b/src/xrEngine/thunderbolt.cpp @@ -171,7 +171,33 @@ bool CEffect_Thunderbolt::RayPick(const Fvector& s, const Fvector& d, float& ran bRes = g_pGameLevel->ObjectSpace.RayPick(s, d, range, collide::rqtBoth, RQ, E); if (bRes) range = RQ.range; - else + + // Prefer nearer terrain heightmap hit (ObjectSpace often misses terrain). + if (GEnv.Render && d.y < -0.01f) + { + const float maxR = range; + float best = maxR; + bool hit = false; + Fvector p; + p.mad(s, d, _min(maxR, (s.y + 80.f) / _max(-d.y, 0.01f))); + float hy = 0.f; + if (GEnv.Render->SampleTerrainHeight(p.x, p.z, hy) && s.y > hy) + { + const float th = (s.y - hy) / (-d.y); + if (th > 0.f && th < best) + { + best = th; + hit = true; + } + } + if (hit) + { + range = best; + bRes = true; + } + } + + if (!bRes) { Fvector N = {0.f, -1.f, 0.f}; Fvector P = {0.f, 0.f, 0.f}; @@ -183,8 +209,7 @@ bool CEffect_Thunderbolt::RayPick(const Fvector& s, const Fvector& d, float& ran range = dst; return true; } - else - return false; + return false; } #endif return bRes; diff --git a/src/xrEngine/xr_ioc_cmd.cpp b/src/xrEngine/xr_ioc_cmd.cpp index f8ef549a4bf..d31dadf968e 100644 --- a/src/xrEngine/xr_ioc_cmd.cpp +++ b/src/xrEngine/xr_ioc_cmd.cpp @@ -26,6 +26,24 @@ ENGINE_API int ps_r_path_tracer = 0; ENGINE_API int ps_r_path_tracer_bounces = 8; ENGINE_API int ps_r_rt_gi = 0; ENGINE_API float ps_r_rt_gi_intensity = 1.0f; +ENGINE_API int ps_r_taa = 1; +ENGINE_API float ps_r_taa_sharpness = 0.65f; +ENGINE_API int ps_r_bloom = 1; +ENGINE_API int ps_r_cas = 1; +ENGINE_API float ps_r_cas_sharpness = 0.6f; +ENGINE_API int ps_r_contact_shadows = 0; +ENGINE_API float ps_r_contact_shadows_length = 0.35f; +ENGINE_API int ps_r_ssr = 1; +ENGINE_API int ps_r_ssr_quality = 2; +ENGINE_API int ps_r_ssr_debug = 0; // 0=off 1=weight 2=mirrorUV 3=confHeat + +ENGINE_API int ps_r_sky_ibl = 1; +ENGINE_API float ps_r_sky_ibl_intensity = 1.0f; +ENGINE_API int ps_r_foliage_sss = 1; +ENGINE_API float ps_r_foliage_sss_intensity = 0.45f; +ENGINE_API int ps_r_ssgi = 1; +ENGINE_API int ps_r_ssgi_quality = 2; +ENGINE_API float ps_r_ssgi_intensity = 0.65f; ENGINE_API Fvector4 ps_dev_param_1 = {0, 0, 0, 0}; ENGINE_API Fvector4 ps_dev_param_2 = {0, 0, 0, 0}; ENGINE_API Fvector4 ps_dev_param_3 = {0, 0, 0, 0}; diff --git a/src/xrEngine/xr_ioc_cmd.h b/src/xrEngine/xr_ioc_cmd.h index db35c9c6b68..8353d12ec2f 100644 --- a/src/xrEngine/xr_ioc_cmd.h +++ b/src/xrEngine/xr_ioc_cmd.h @@ -35,6 +35,23 @@ extern ENGINE_API int ps_r_path_tracer; extern ENGINE_API int ps_r_path_tracer_bounces; extern ENGINE_API int ps_r_rt_gi; extern ENGINE_API float ps_r_rt_gi_intensity; +extern ENGINE_API int ps_r_taa; +extern ENGINE_API float ps_r_taa_sharpness; +extern ENGINE_API int ps_r_bloom; +extern ENGINE_API int ps_r_cas; +extern ENGINE_API float ps_r_cas_sharpness; +extern ENGINE_API int ps_r_contact_shadows; +extern ENGINE_API float ps_r_contact_shadows_length; +extern ENGINE_API int ps_r_ssr; +extern ENGINE_API int ps_r_ssr_quality; +extern ENGINE_API int ps_r_ssr_debug; +extern ENGINE_API int ps_r_sky_ibl; +extern ENGINE_API float ps_r_sky_ibl_intensity; +extern ENGINE_API int ps_r_foliage_sss; +extern ENGINE_API float ps_r_foliage_sss_intensity; +extern ENGINE_API int ps_r_ssgi; +extern ENGINE_API int ps_r_ssgi_quality; +extern ENGINE_API float ps_r_ssgi_intensity; extern ENGINE_API Fvector4 ps_dev_param_1; extern ENGINE_API Fvector4 ps_dev_param_2; extern ENGINE_API Fvector4 ps_dev_param_3; diff --git a/src/xr_3da/entry_point.cpp b/src/xr_3da/entry_point.cpp index 47fd09682a7..c72d64e94ba 100644 --- a/src/xr_3da/entry_point.cpp +++ b/src/xr_3da/entry_point.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #endif // Always request high performance GPU @@ -92,7 +93,8 @@ int main(int argc, char *argv[]) result = entry_point(commandLine); - xr_free(commandLine); + // Process is exiting; skip xr_free — Core._destroy already tore down allocators. + commandLine = nullptr; } else result = entry_point(""); @@ -102,6 +104,12 @@ int main(int argc, char *argv[]) _resetstkoflw(); FATAL_F("stack overflow: %s", e.what()); } + catch (const std::system_error& e) + { + // Tracy / mutex teardown races on exit — don't abort the process noisily. + fprintf(stderr, "! exit system_error: %s\n", e.what()); + result = EXIT_SUCCESS; + } catch (const std::runtime_error& e) { FATAL_F("runtime error: %s", e.what()); From f831b5d31e046ab37555876779f1ec32527561fc Mon Sep 17 00:00:00 2001 From: FoxConED Date: Thu, 23 Jul 2026 22:39:19 +0300 Subject: [PATCH 247/261] gitignore: ignore local WIP status doc and .DS_Store --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 358c57496e4..92a8cf3f294 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,11 @@ Externals/rafx/ Externals/rafx-proj/ .tmp_classic_sun/ +# Local WIP / machine-specific (not for the framegraph branch) +framegraph_docs/STATUS.md +.DS_Store +**/.DS_Store + # User-specific files *.user *.user.props From c46f3875069b60114f22a26fe452d83b801b9144 Mon Sep 17 00:00:00 2001 From: FoxConED Date: Wed, 29 Jul 2026 10:21:55 +0300 Subject: [PATCH 248/261] xrRender: sync lighting RT and upscaling stack --- .gitmodules | 4 +- CMakeLists.txt | 3 + Externals/CMakeLists.txt | 71 + .../FORWARD_PLUS_MIGRATION_PLAN.md | 12 + res/gamedata/configs/text/eng/openxray.xml | 36 + res/gamedata/configs/text/rus/openxray.xml | 124 +- res/gamedata/configs/ui/ui_mm_opt.xml | 92 + res/gamedata/configs/ui/ui_mm_opt_16.xml | 92 + .../scripts/ui_mm_opt_video_adv.script | 60 + res/gamedata/shaders/r5/batch_compact.cs | 1 - .../shaders/r5/batch_compact_count.cs | 1 - res/gamedata/shaders/r5/batch_compact_scan.cs | 1 - res/gamedata/shaders/r5/bindless_common.h | 82 +- res/gamedata/shaders/r5/bindless_depth.ps | Bin 609 -> 827 bytes .../shaders/r5/bindless_depth_opaque.ps | Bin 232 -> 367 bytes .../shaders/r5/bindless_depth_tess.ps | Bin 0 -> 708 bytes res/gamedata/shaders/r5/bindless_forward.ps | Bin 5979 -> 5101 bytes res/gamedata/shaders/r5/bindless_lod.ps | Bin 0 -> 2091 bytes res/gamedata/shaders/r5/bindless_lod.vs | 42 + res/gamedata/shaders/r5/bindless_particle.ps | Bin 1878 -> 2355 bytes .../shaders/r5/bindless_particle_lit.ps | Bin 0 -> 4735 bytes .../shaders/r5/bindless_particle_sixway.ps | Bin 0 -> 4202 bytes res/gamedata/shaders/r5/bindless_skinned.ps | Bin 2627 -> 3419 bytes .../shaders/r5/bindless_skinned_depth.ps | Bin 0 -> 762 bytes .../shaders/r5/bindless_skinned_hud.ps | Bin 2796 -> 4205 bytes .../shaders/r5/bindless_skinned_mdi.ps | Bin 2380 -> 3542 bytes .../shaders/r5/bindless_skinned_scope.ps | Bin 0 -> 3126 bytes .../shaders/r5/bindless_skinned_tess.ds | 105 +- .../shaders/r5/bindless_skinned_tess.hs | 25 +- res/gamedata/shaders/r5/bindless_terrain.ps | Bin 9569 -> 11141 bytes res/gamedata/shaders/r5/bindless_tess.ds | 114 +- res/gamedata/shaders/r5/bindless_tess.hs | 26 +- res/gamedata/shaders/r5/bloom/composite.ps | Bin 607 -> 815 bytes res/gamedata/shaders/r5/camera_model.ps | Bin 0 -> 1617 bytes res/gamedata/shaders/r5/clouds_forward.ps | Bin 0 -> 676 bytes res/gamedata/shaders/r5/clouds_forward.vs | 36 + .../shaders/r5/cluster_light_assign.cs | 158 +- res/gamedata/shaders/r5/common_functions.h | 114 +- res/gamedata/shaders/r5/common_iostructs.h | 6 + res/gamedata/shaders/r5/cull_utils.h | 7 +- res/gamedata/shaders/r5/decal_box.ps | Bin 1666 -> 2488 bytes .../shaders/r5/depth/depth_prepass.ps | Bin 1866 -> 0 bytes .../shaders/r5/depth/depth_prepass.vs | 41 - res/gamedata/shaders/r5/detail_billboard.ps | Bin 1810 -> 1857 bytes res/gamedata/shaders/r5/detail_billboard.vs | 26 +- res/gamedata/shaders/r5/detail_cull.cs | 7 +- res/gamedata/shaders/r5/detail_decal.ps | Bin 1445 -> 1725 bytes res/gamedata/shaders/r5/detail_decal.vs | 23 +- res/gamedata/shaders/r5/detail_gpu.ps | Bin 7589 -> 7664 bytes res/gamedata/shaders/r5/detail_gpu.vs | 22 +- .../shaders/r5/detail_instance_gen.cs | 9 +- res/gamedata/shaders/r5/distortion_apply.ps | Bin 1619 -> 1671 bytes res/gamedata/shaders/r5/dlss_rr_guides.cs | 71 + res/gamedata/shaders/r5/effects_world_soft.ps | Bin 1239 -> 1148 bytes res/gamedata/shaders/r5/exposure_adapt.cs | 71 +- res/gamedata/shaders/r5/gbuffer.ps | Bin 5188 -> 0 bytes res/gamedata/shaders/r5/gbuffer.vs | 66 - res/gamedata/shaders/r5/hud/default.ps | Bin 217 -> 290 bytes res/gamedata/shaders/r5/ibl_brdf_lut.cs | 91 + .../shaders/r5/ibl_prefilter_specular.cs | 111 + res/gamedata/shaders/r5/ibl_probe_capture.cs | 91 + res/gamedata/shaders/r5/ibl_sh_project.cs | 72 + res/gamedata/shaders/r5/light_hiz_cull.cs | 7 +- res/gamedata/shaders/r5/light_shadow_cull.cs | 74 + res/gamedata/shaders/r5/lighting.ps | Bin 7420 -> 0 bytes .../shaders/r5/local_shadow_esm_blur.cs | 39 + .../shaders/r5/local_shadow_esm_convert.cs | 28 + res/gamedata/shaders/r5/motion_blur.ps | Bin 0 -> 1558 bytes res/gamedata/shaders/r5/nrd_composite.cs | 89 + res/gamedata/shaders/r5/nrd_pack_inputs.cs | 122 + res/gamedata/shaders/r5/pnv.h | 5 +- res/gamedata/shaders/r5/postprocess.ps | Bin 0 -> 3118 bytes .../shaders/r5/rain/rain_billboard.cs | 2 +- res/gamedata/shaders/r5/rain/rain_update.cs | 16 +- res/gamedata/shaders/r5/restir_di_common.h | 81 + res/gamedata/shaders/r5/restir_di_eval.h | 89 + res/gamedata/shaders/r5/restir_di_shade.cs | 150 ++ res/gamedata/shaders/r5/restir_di_spatial.cs | 170 ++ res/gamedata/shaders/r5/restir_di_temporal.cs | 126 + res/gamedata/shaders/r5/restir_gi_blur.cs | 129 ++ res/gamedata/shaders/r5/restir_gi_common.h | 27 +- .../shaders/r5/restir_gi_composite.cs | 90 +- res/gamedata/shaders/r5/restir_gi_initial.cs | 479 +++- res/gamedata/shaders/r5/restir_gi_spatial.cs | 215 ++ res/gamedata/shaders/r5/restir_gi_temporal.cs | 205 +- .../shaders/r5/restir_gi_temporal_filter.cs | 135 ++ .../shaders/r5/restir_motion_vectors.cs | 55 +- res/gamedata/shaders/r5/restir_water_rt.cs | 281 +++ res/gamedata/shaders/r5/ribbon.vs | 2 +- res/gamedata/shaders/r5/rt_common.h | 32 + res/gamedata/shaders/r5/rt_grass_alpha.h | 23 + res/gamedata/shaders/r5/rt_grass_vertices.cs | 4 + res/gamedata/shaders/r5/rt_irradiance_cache.h | 59 + res/gamedata/shaders/r5/rt_pathtrace.cs | 14 +- res/gamedata/shaders/r5/rt_shade_hit.h | 41 + res/gamedata/shaders/r5/rt_visibility.h | 180 ++ res/gamedata/shaders/r5/rt_volumetric.cs | 150 ++ .../shaders/r5/shadow/local_shadow_clear.vs | 12 + .../shaders/r5/shadow/shadow_cascade_rain.ps | Bin 0 -> 367 bytes .../shadow/shadow_cascade_skinned_2w_local.vs | 42 + .../shadow/shadow_cascade_skinned_2w_mdi.vs | 41 + .../shadow/shadow_cascade_skinned_3w_local.vs | 48 + .../shadow/shadow_cascade_skinned_3w_mdi.vs | 47 + .../shadow/shadow_cascade_skinned_4w_local.vs | 56 + .../shadow/shadow_cascade_skinned_4w_mdi.vs | 51 + .../shadow/shadow_cascade_skinned_hq_local.vs | 41 + .../shadow/shadow_cascade_skinned_hq_mdi.vs | 40 + .../r5/shadow/shadow_cascade_skinned_local.vs | 47 + .../r5/shadow/shadow_cascade_skinned_mdi.vs | 41 + res/gamedata/shaders/r5/shadow_hzb_build.cs | 26 + res/gamedata/shaders/r5/shadow_mask.cs | 100 + .../shaders/r5/shared/basecolor_pack.h | 21 + .../shaders/r5/shared/clustered_lighting.h | 95 +- res/gamedata/shaders/r5/shared/foliage_sss.h | 35 +- res/gamedata/shaders/r5/shared/glass_sss.h | 32 + .../shaders/r5/shared/lighting_common.h | 47 +- res/gamedata/shaders/r5/shared/nrd_helpers.h | 101 + .../shaders/r5/shared/object_parallax.h | 60 + res/gamedata/shaders/r5/shared/pbr_brdf.h | 28 +- .../shaders/r5/shared/shadow_sampling.h | 428 +++- res/gamedata/shaders/r5/shared/skin_sss.h | 94 + res/gamedata/shaders/r5/shared/sky_ibl.h | 238 +- res/gamedata/shaders/r5/shared/ssr.h | 205 +- .../shaders/r5/shared/surface_marks.h | 41 + res/gamedata/shaders/r5/shared/tess_pn.h | 75 + res/gamedata/shaders/r5/shared/wmark.h | 6 +- res/gamedata/shaders/r5/skinned_common.h | 58 +- res/gamedata/shaders/r5/smoke_trail.vs | 4 +- res/gamedata/shaders/r5/ssgi_apply.ps | Bin 1665 -> 1607 bytes res/gamedata/shaders/r5/ssgi_temporal.ps | Bin 1367 -> 1600 bytes res/gamedata/shaders/r5/ssgi_trace.ps | Bin 4000 -> 4559 bytes res/gamedata/shaders/r5/ssr_apply.ps | Bin 769 -> 2899 bytes res/gamedata/shaders/r5/ssr_blur.ps | Bin 2188 -> 5362 bytes res/gamedata/shaders/r5/ssr_resolve.ps | Bin 4713 -> 8434 bytes res/gamedata/shaders/r5/ssr_temporal.ps | Bin 0 -> 5008 bytes res/gamedata/shaders/r5/sssss_blur.ps | Bin 0 -> 2462 bytes res/gamedata/shaders/r5/taa.ps | Bin 7528 -> 7917 bytes res/gamedata/shaders/r5/tonemap.ps | Bin 1412 -> 1449 bytes res/gamedata/shaders/r5/trail.vs | 4 +- .../shaders/r5/volumetric/apply_lighting.cs | 59 +- .../shaders/r5/volumetric/light_shafts.cs | 28 + .../shaders/r5/volumetric/sunshafts.ps | Bin 2260 -> 2654 bytes .../r5/volumetric/sunshafts_combine.ps | Bin 1258 -> 904 bytes .../r5/volumetric/temporal_reproject.cs | 18 +- res/gamedata/shaders/r5/water.ps | Bin 4684 -> 5247 bytes res/gamedata/shaders/r5/water.vs | 2 + res/gamedata/shaders/r5/waterd.ps | Bin 0 -> 2336 bytes res/gamedata/shaders/r5/waterd.vs | 65 + res/gamedata/shaders/r5/wet/rain_apply.ps | Bin 4130 -> 6346 bytes .../shaders/r5/wet/rain_patch_normal.ps | Bin 3406 -> 5686 bytes .../shaders/r5/wet/rain_write_normal.ps | Bin 1313 -> 1377 bytes res/gamedata/shaders/r5/wet/ssfx_ssr.h | 1 - res/gamedata/textures/fx/fx_noise2.dds | Bin 0 -> 262272 bytes res/gamedata/textures/water/water_normal.dds | Bin 0 -> 699192 bytes src/Layers/xrRender/Backend/VulkanBackend.cpp | 673 +++++- src/Layers/xrRender/Backend/VulkanBackend.h | 47 +- src/Layers/xrRender/Bindless/BindlessTypes.h | 15 +- .../Bindless/TerrainMaterialBuffer.cpp | 2 +- src/Layers/xrRender/CMakeLists.txt | 69 + src/Layers/xrRender/ClusteredLightManager.cpp | 915 ++++++-- src/Layers/xrRender/ClusteredLightManager.h | 73 +- src/Layers/xrRender/ColorMapManager.h | 1 + src/Layers/xrRender/Decals/DecalManager.cpp | 9 +- src/Layers/xrRender/Decals/DecalManager.h | 15 +- .../xrRender/Denoising/DenoiseBackends.cpp | 157 ++ .../xrRender/Denoising/IDenoiseBackend.h | 71 + .../xrRender/Denoising/NRDDenoiseBackend.cpp | 734 ++++++ src/Layers/xrRender/ETextureParams.cpp | 3 +- src/Layers/xrRender/FGDetailManager.cpp | 54 +- src/Layers/xrRender/FGDetailManager.h | 10 +- src/Layers/xrRender/FGRenderBase.cpp | 25 +- src/Layers/xrRender/FGRenderHost.cpp | 2 +- .../xrRender/FrameGraph/BindingSetBuilder.cpp | 56 +- .../xrRender/FrameGraph/FGResourcePool.cpp | 18 +- src/Layers/xrRender/FrameGraph/FrameGraph.cpp | 11 +- .../xrRender/FrameGraph/PassResourceCache.cpp | 66 +- .../xrRender/FrameGraph/PassResourceCache.h | 6 + .../xrRender/FrameGraph/ShaderCache.cpp | 27 +- src/Layers/xrRender/FrameGraph/ShaderCache.h | 2 +- .../xrRender/FrameGraph/ShaderLoader.cpp | 91 +- .../xrRender/FrameGraph/ShaderReflection.cpp | 42 +- .../FrameGraphPasses/BloomPassSetup.cpp | 15 +- .../FrameGraphPasses/BloomPassSetup.h | 1 + .../FrameGraphPasses/CameraModelPassSetup.cpp | 343 +++ .../FrameGraphPasses/CameraModelPassSetup.h | 33 + .../ClusterLightPassSetup.cpp | 6 +- .../FrameGraphPasses/DecalPassSetup.cpp | 134 +- .../FrameGraphPasses/DecalPassSetup.h | 3 +- .../FrameGraphPasses/DepthPrepassSetup.cpp | 140 +- .../FrameGraphPasses/DepthPrepassSetup.h | 10 +- .../FrameGraphPasses/DetailCullPassSetup.cpp | 28 +- .../FrameGraphPasses/DetailPassSetup.cpp | 169 +- .../FrameGraphPasses/DetailPassSetup.h | 12 +- .../DistortionApplyPassSetup.cpp | 26 +- .../DistortionApplyPassSetup.h | 1 + .../FrameGraphPasses/ExposurePassSetup.cpp | 55 +- .../ForwardColorPassSetup.cpp | 200 +- .../FrameGraphPasses/ForwardColorPassSetup.h | 18 +- .../FrameGraphPasses/GlowPassSetup.cpp | 293 +++ .../xrRender/FrameGraphPasses/GlowPassSetup.h | 50 + .../FrameGraphPasses/HiZBuildPassSetup.cpp | 25 +- .../FrameGraphPasses/HiZBuildPassSetup.h | 4 +- .../IBLPrefilterPassSetup.cpp | 901 ++++++++ .../FrameGraphPasses/IBLPrefilterPassSetup.h | 126 + .../FrameGraphPasses/LocalShadowPassSetup.cpp | 1223 ++++++++-- .../FrameGraphPasses/LocalShadowPassSetup.h | 49 +- .../FrameGraphPasses/LodPassSetup.cpp | 335 +++ .../xrRender/FrameGraphPasses/LodPassSetup.h | 45 + .../MotionVectorPassSetup.cpp | 82 +- .../FrameGraphPasses/MotionVectorPassSetup.h | 6 +- .../FrameGraphPasses/ParticlePassSetup.cpp | 151 +- .../FrameGraphPasses/ParticlePassSetup.h | 9 + .../xrRender/FrameGraphPasses/PassCommon.cpp | 101 + .../xrRender/FrameGraphPasses/PassCommon.h | 12 + .../FrameGraphPasses/PassVertexFormats.h | 16 +- .../FrameGraphPasses/PathTracerPassSetup.cpp | 4 +- .../FrameGraphPasses/PostProcessPassSetup.cpp | 300 +++ .../FrameGraphPasses/PostProcessPassSetup.h | 32 + .../FrameGraphPasses/RainShadowPassSetup.cpp | 208 +- .../FrameGraphPasses/RainShadowPassSetup.h | 6 +- .../FrameGraphPasses/ReSTIRGIPassSetup.cpp | 2039 +++++++++++++++-- .../FrameGraphPasses/ReSTIRGIPassSetup.h | 59 +- .../FrameGraphPasses/RibbonPassSetup.cpp | 6 +- .../FrameGraphPasses/SSGIPassSetup.cpp | 241 +- .../xrRender/FrameGraphPasses/SSGIPassSetup.h | 7 + .../FrameGraphPasses/SSRPassSetup.cpp | 339 ++- .../xrRender/FrameGraphPasses/SSRPassSetup.h | 8 +- .../FrameGraphPasses/SSSSSPassSetup.cpp | 215 ++ .../FrameGraphPasses/SSSSSPassSetup.h | 32 + .../SceneReflectionPassSetup.cpp | 50 +- .../SceneReflectionPassSetup.h | 17 +- .../FrameGraphPasses/ShaderConstants.h | 84 +- .../FrameGraphPasses/ShadowHZBPassSetup.cpp | 238 ++ .../FrameGraphPasses/ShadowHZBPassSetup.h | 47 + .../FrameGraphPasses/ShadowMaskPassSetup.cpp | 257 +++ .../FrameGraphPasses/ShadowMaskPassSetup.h | 50 + .../FrameGraphPasses/ShadowPassSetup.cpp | 415 +++- .../FrameGraphPasses/ShadowPassSetup.h | 6 +- .../FrameGraphPasses/SkinningPassSetup.cpp | 680 +++++- .../FrameGraphPasses/SkinningPassSetup.h | 75 +- .../FrameGraphPasses/SkyPassSetup.cpp | 1 + .../FrameGraphPasses/SmokeTrailPassSetup.cpp | 6 +- .../FrameGraphPasses/SunShaftsPassSetup.cpp | 162 +- .../FrameGraphPasses/SunShaftsPassSetup.h | 11 +- .../FrameGraphPasses/TAAPassSetup.cpp | 71 +- .../xrRender/FrameGraphPasses/TAAPassSetup.h | 3 + .../FrameGraphPasses/TonemapPassSetup.cpp | 30 +- .../FrameGraphPasses/TonemapPassSetup.h | 1 + .../FrameGraphPasses/TrailPassSetup.cpp | 6 +- .../FrameGraphPasses/TransparentPassSetup.cpp | 504 +++- .../FrameGraphPasses/TransparentPassSetup.h | 17 +- .../FrameGraphPasses/VolumetricPassSetup.cpp | 195 +- .../FrameGraphPasses/VolumetricPassSetup.h | 31 +- .../FrameGraphPasses/WetSurfacesPassSetup.cpp | 133 +- .../FrameGraphPasses/WetSurfacesPassSetup.h | 7 +- src/Layers/xrRender/GPUCullingManager.cpp | 992 ++++++-- src/Layers/xrRender/GPUCullingManager.h | 67 +- .../xrRender/Geometry/GeometryBatch.cpp | 142 +- src/Layers/xrRender/Geometry/GeometryBatch.h | 34 +- .../xrRender/Geometry/MaterialCache.cpp | 324 ++- src/Layers/xrRender/Geometry/MaterialCache.h | 9 +- .../xrRender/Materials/MaterialSystem.cpp | 18 +- .../xrRender/Materials/MaterialSystem.h | 3 +- src/Layers/xrRender/Materials/ShaderInfo.h | 2 + src/Layers/xrRender/MaxRectsAllocator.h | 144 ++ src/Layers/xrRender/ParticleEffectDef.cpp | 26 + src/Layers/xrRender/ParticleEffectDef.h | 12 + src/Layers/xrRender/Profiler/StatsOverlay.cpp | 27 + src/Layers/xrRender/Profiler/StatsOverlay.h | 13 + .../RayTracing/RTAccelStructManager.cpp | 205 +- .../RayTracing/RTAccelStructManager.h | 5 + .../xrRender/RenderContext/RenderDevice.cpp | 21 +- .../xrRender/RenderContext/RenderDevice.h | 2 +- src/Layers/xrRender/ResourceManager.cpp | 10 +- .../ResourceManager/TextureManager.cpp | 234 +- .../xrRender/ResourceManager/TextureManager.h | 13 +- src/Layers/xrRender/SMAP_Allocator.h | 21 +- src/Layers/xrRender/ShaderInfo.cpp | 27 + .../ShaderVariant/VariantPSOCache.cpp | 35 +- .../xrRender/ShaderVariant/VariantPSOCache.h | 5 + src/Layers/xrRender/TextureDescrManager.cpp | 20 +- src/Layers/xrRender/TextureDescrManager.h | 2 + .../xrRender/Upscaling/DlssFgPassSetup.cpp | 143 ++ .../xrRender/Upscaling/DlssFgPassSetup.h | 26 + .../xrRender/Upscaling/IUpscaleBackend.h | 74 + src/Layers/xrRender/Upscaling/NgxDLSS.cpp | 810 +++++++ .../xrRender/Upscaling/StreamlineDLSS.h | 63 + .../xrRender/Upscaling/UpscaleBackends.cpp | 187 ++ .../xrRender/Upscaling/UpscalePassSetup.cpp | 352 +++ .../xrRender/Upscaling/UpscalePassSetup.h | 54 + .../xrRender/Upscaling/UpscaleState.cpp | 56 + src/Layers/xrRender/Upscaling/UpscaleState.h | 29 + .../xrRender/Volumetrics/LightShaftSource.cpp | 20 + .../xrRender/Volumetrics/LightShaftSource.h | 22 + .../Volumetrics/ParticleEmitterSource.cpp | 15 + .../Volumetrics/ParticleEmitterSource.h | 17 + .../Volumetrics/VolumetricRenderer.cpp | 12 +- .../xrRender/Volumetrics/VolumetricRenderer.h | 15 +- .../xrRender/blenders/blender_deffer_flat.h | 2 + .../xrRender/blenders/blender_deffer_model.h | 2 + src/Layers/xrRender/fgEnvironmentRender.cpp | 242 +- src/Layers/xrRender/fgEnvironmentRender.h | 14 + src/Layers/xrRender/fgFontRender.cpp | 2 +- src/Layers/xrRender/fgLensFlareRender.cpp | 2 +- src/Layers/xrRender/fgRainRender.cpp | 2 +- src/Layers/xrRender/fgThunderboltRender.cpp | 2 +- src/Layers/xrRender/fgUIShader.cpp | 7 + src/Layers/xrRender/fg_rendertarget_pp.cpp | 84 + src/Layers/xrRender/light.cpp | 17 + src/Layers/xrRender/light.h | 2 +- src/Layers/xrRender/r4_rendertarget.h | 14 + src/Layers/xrRender/r_FrameGraphRenderer.cpp | 1489 +++++++++--- src/Layers/xrRender/r_FrameGraphRenderer.h | 9 + .../xrRender/r_FrameGraphRenderer_Loader.cpp | 128 +- src/Layers/xrRender/xrRender_console.cpp | 179 +- src/Layers/xrRender/xrRender_console.h | 31 +- src/xrCore/FS.cpp | 5 +- src/xrCore/Profiler/Profiler.cpp | 3 + src/xrCore/log.cpp | 7 +- src/xrEngine/Environment_editor.cpp | 19 +- src/xrEngine/IRenderBackend.h | 19 + src/xrEngine/device.cpp | 22 +- src/xrEngine/xr_ioc_cmd.cpp | 43 +- src/xrEngine/xr_ioc_cmd.h | 41 + src/xrGame/GamePersistent.cpp | 3 - src/xrGame/Torch.cpp | 8 + src/xrGame/Weapon.cpp | 11 + src/xr_3da/CMakeLists.txt | 14 + 328 files changed, 25013 insertions(+), 3496 deletions(-) create mode 100644 res/gamedata/shaders/r5/bindless_depth_tess.ps create mode 100644 res/gamedata/shaders/r5/bindless_lod.ps create mode 100644 res/gamedata/shaders/r5/bindless_lod.vs create mode 100644 res/gamedata/shaders/r5/bindless_particle_lit.ps create mode 100644 res/gamedata/shaders/r5/bindless_particle_sixway.ps create mode 100644 res/gamedata/shaders/r5/bindless_skinned_depth.ps create mode 100644 res/gamedata/shaders/r5/bindless_skinned_scope.ps create mode 100644 res/gamedata/shaders/r5/camera_model.ps create mode 100644 res/gamedata/shaders/r5/clouds_forward.ps create mode 100644 res/gamedata/shaders/r5/clouds_forward.vs delete mode 100644 res/gamedata/shaders/r5/depth/depth_prepass.ps delete mode 100644 res/gamedata/shaders/r5/depth/depth_prepass.vs create mode 100644 res/gamedata/shaders/r5/dlss_rr_guides.cs delete mode 100644 res/gamedata/shaders/r5/gbuffer.ps delete mode 100644 res/gamedata/shaders/r5/gbuffer.vs create mode 100644 res/gamedata/shaders/r5/ibl_brdf_lut.cs create mode 100644 res/gamedata/shaders/r5/ibl_prefilter_specular.cs create mode 100644 res/gamedata/shaders/r5/ibl_probe_capture.cs create mode 100644 res/gamedata/shaders/r5/ibl_sh_project.cs create mode 100644 res/gamedata/shaders/r5/light_shadow_cull.cs delete mode 100644 res/gamedata/shaders/r5/lighting.ps create mode 100644 res/gamedata/shaders/r5/local_shadow_esm_blur.cs create mode 100644 res/gamedata/shaders/r5/local_shadow_esm_convert.cs create mode 100644 res/gamedata/shaders/r5/motion_blur.ps create mode 100644 res/gamedata/shaders/r5/nrd_composite.cs create mode 100644 res/gamedata/shaders/r5/nrd_pack_inputs.cs create mode 100644 res/gamedata/shaders/r5/postprocess.ps create mode 100644 res/gamedata/shaders/r5/restir_di_common.h create mode 100644 res/gamedata/shaders/r5/restir_di_eval.h create mode 100644 res/gamedata/shaders/r5/restir_di_shade.cs create mode 100644 res/gamedata/shaders/r5/restir_di_spatial.cs create mode 100644 res/gamedata/shaders/r5/restir_di_temporal.cs create mode 100644 res/gamedata/shaders/r5/restir_gi_blur.cs create mode 100644 res/gamedata/shaders/r5/restir_gi_spatial.cs create mode 100644 res/gamedata/shaders/r5/restir_gi_temporal_filter.cs create mode 100644 res/gamedata/shaders/r5/restir_water_rt.cs create mode 100644 res/gamedata/shaders/r5/rt_grass_alpha.h create mode 100644 res/gamedata/shaders/r5/rt_irradiance_cache.h create mode 100644 res/gamedata/shaders/r5/rt_shade_hit.h create mode 100644 res/gamedata/shaders/r5/rt_visibility.h create mode 100644 res/gamedata/shaders/r5/rt_volumetric.cs create mode 100644 res/gamedata/shaders/r5/shadow/local_shadow_clear.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_rain.ps create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_2w_local.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_2w_mdi.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_3w_local.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_3w_mdi.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_4w_local.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_4w_mdi.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_hq_local.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_hq_mdi.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_local.vs create mode 100644 res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_mdi.vs create mode 100644 res/gamedata/shaders/r5/shadow_hzb_build.cs create mode 100644 res/gamedata/shaders/r5/shadow_mask.cs create mode 100644 res/gamedata/shaders/r5/shared/basecolor_pack.h create mode 100644 res/gamedata/shaders/r5/shared/glass_sss.h create mode 100644 res/gamedata/shaders/r5/shared/nrd_helpers.h create mode 100644 res/gamedata/shaders/r5/shared/object_parallax.h create mode 100644 res/gamedata/shaders/r5/shared/skin_sss.h create mode 100644 res/gamedata/shaders/r5/shared/surface_marks.h create mode 100644 res/gamedata/shaders/r5/shared/tess_pn.h create mode 100644 res/gamedata/shaders/r5/ssr_temporal.ps create mode 100644 res/gamedata/shaders/r5/sssss_blur.ps create mode 100644 res/gamedata/shaders/r5/volumetric/light_shafts.cs create mode 100644 res/gamedata/shaders/r5/waterd.ps create mode 100644 res/gamedata/shaders/r5/waterd.vs create mode 100644 res/gamedata/textures/fx/fx_noise2.dds create mode 100644 res/gamedata/textures/water/water_normal.dds create mode 100644 src/Layers/xrRender/Denoising/DenoiseBackends.cpp create mode 100644 src/Layers/xrRender/Denoising/IDenoiseBackend.h create mode 100644 src/Layers/xrRender/Denoising/NRDDenoiseBackend.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/CameraModelPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/CameraModelPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/GlowPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/GlowPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/IBLPrefilterPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/IBLPrefilterPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/LodPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/LodPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/SSSSSPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/SSSSSPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/ShadowHZBPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/ShadowHZBPassSetup.h create mode 100644 src/Layers/xrRender/FrameGraphPasses/ShadowMaskPassSetup.cpp create mode 100644 src/Layers/xrRender/FrameGraphPasses/ShadowMaskPassSetup.h create mode 100644 src/Layers/xrRender/MaxRectsAllocator.h create mode 100644 src/Layers/xrRender/Upscaling/DlssFgPassSetup.cpp create mode 100644 src/Layers/xrRender/Upscaling/DlssFgPassSetup.h create mode 100644 src/Layers/xrRender/Upscaling/IUpscaleBackend.h create mode 100644 src/Layers/xrRender/Upscaling/NgxDLSS.cpp create mode 100644 src/Layers/xrRender/Upscaling/StreamlineDLSS.h create mode 100644 src/Layers/xrRender/Upscaling/UpscaleBackends.cpp create mode 100644 src/Layers/xrRender/Upscaling/UpscalePassSetup.cpp create mode 100644 src/Layers/xrRender/Upscaling/UpscalePassSetup.h create mode 100644 src/Layers/xrRender/Upscaling/UpscaleState.cpp create mode 100644 src/Layers/xrRender/Upscaling/UpscaleState.h create mode 100644 src/Layers/xrRender/Volumetrics/LightShaftSource.cpp create mode 100644 src/Layers/xrRender/Volumetrics/LightShaftSource.h create mode 100644 src/Layers/xrRender/Volumetrics/ParticleEmitterSource.cpp create mode 100644 src/Layers/xrRender/Volumetrics/ParticleEmitterSource.h create mode 100644 src/Layers/xrRender/fg_rendertarget_pp.cpp diff --git a/.gitmodules b/.gitmodules index ba3b1738d68..af3fc99734e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -30,7 +30,7 @@ url = https://github.com/pattonkan/sse2rvv.git [submodule "Externals/nvrhi"] path = Externals/nvrhi - url = git@github.com:yohjimane/NVRHI.git + url = https://github.com/yohjimane/NVRHI.git branch = main [submodule "Externals/slang"] path = Externals/slang @@ -55,5 +55,5 @@ url = https://github.com/skypjack/entt.git [submodule "Externals/MoltenVK"] path = Externals/MoltenVK - url = git@github.com:yohjimane/MoltenVK.git + url = https://github.com/yohjimane/MoltenVK.git branch = main diff --git a/CMakeLists.txt b/CMakeLists.txt index d064ef55730..8dac936b45a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,9 @@ option(XRAY_USE_LUAJIT "Use LuaJIT" ON) option(XRAY_USE_ASAN "Use AddressSanitizer" OFF) option(XRAY_ENABLE_TRACY "Enable tracy profiler" OFF) option(XRAY_USE_AI_PBR "Enable AI PBR texture converter (requires ONNX Runtime)" OFF) +option(XRAY_USE_FSR "Enable AMD FSR / Ray Regeneration / FidelityFX Denoiser (requires SDK)" OFF) +option(XRAY_USE_DLSS "Enable NVIDIA DLSS / NGX (requires SDK)" OFF) +option(XRAY_USE_NRD "Enable NVIDIA NRD denoise via NRDIntegration+NRI (Linux Vulkan: -DXRAY_USE_NRD=ON)" OFF) include(XRay.Build) include(XRay.Windows.Externals) diff --git a/Externals/CMakeLists.txt b/Externals/CMakeLists.txt index 75133bb5cb6..63475e643d8 100644 --- a/Externals/CMakeLists.txt +++ b/Externals/CMakeLists.txt @@ -4,6 +4,11 @@ else() find_package(Lua51 REQUIRED) endif() +# Static third-party archives linked into shared OpenXRay libs need PIC on Linux. +if (BUILD_SHARED_LIBS AND NOT WIN32) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) +endif() + add_library(sse2neon INTERFACE) target_include_directories(sse2neon INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") @@ -14,6 +19,10 @@ set(SDL_INSTALL OFF CACHE BOOL "Disable SDL3 install rules") set(SDL_DISABLE_INSTALL ON CACHE BOOL "Disable SDL3 install rules") set(SDL_DISABLE_INSTALL_DOCS ON CACHE BOOL "Disable SDL3 docs install") add_subdirectory(SDL) +# Static SDL must be PIC when OpenXRay builds shared libs (xrCore.so etc.). +if (TARGET SDL3-static AND BUILD_SHARED_LIBS) + set_property(TARGET SDL3-static PROPERTY POSITION_INDEPENDENT_CODE ON) +endif() add_subdirectory(luabind) add_subdirectory(xrLuaFix) @@ -134,3 +143,65 @@ if (NOT TARGET xrLuabind) "Read the build instructions: https://github.com/OpenXRay/xray-16/wiki" ) endif() + +if (XRAY_USE_NRD) + if (NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/NRD/CMakeLists.txt") + message(FATAL_ERROR "XRAY_USE_NRD=ON but Externals/NRD is missing") + endif() + set(NRD_STATIC_LIBRARY ON CACHE BOOL "Build NRD as static library" FORCE) + set(NRD_NRI ON CACHE BOOL "Pull NRI for NRDIntegration" FORCE) + set(NRD_EMBEDS_SPIRV_SHADERS ON CACHE BOOL "NRD embeds SPIRV shaders" FORCE) + set(NRD_EMBEDS_DXIL_SHADERS OFF CACHE BOOL "NRD: no DXIL on Linux Vulkan" FORCE) + set(NRD_EMBEDS_DXBC_SHADERS OFF CACHE BOOL "NRD: no DXBC on Linux Vulkan" FORCE) + set(NRD_NORMAL_ENCODING "3" CACHE STRING "NRD normal encoding (RGBA16_UNORM)" FORCE) + set(NRD_ROUGHNESS_ENCODING "1" CACHE STRING "NRD roughness encoding (LINEAR)" FORCE) + set(NRI_ENABLE_NVAPI OFF CACHE BOOL "" FORCE) + set(NRI_ENABLE_AMDAGS OFF CACHE BOOL "" FORCE) + set(NRI_ENABLE_NVTX_SUPPORT OFF CACHE BOOL "" FORCE) + set(NRI_ENABLE_NONE_SUPPORT OFF CACHE BOOL "" FORCE) + set(NRI_ENABLE_D3D11_SUPPORT OFF CACHE BOOL "" FORCE) + set(NRI_ENABLE_D3D12_SUPPORT OFF CACHE BOOL "" FORCE) + set(NRI_ENABLE_VK_SUPPORT ON CACHE BOOL "" FORCE) + add_subdirectory(NRD) + + set(_xray_nri_vk_core "${CMAKE_BINARY_DIR}/_deps/vulkan_headers-src/include/vulkan/vulkan_core.h") + set(_xray_nri_vk_ok FALSE) + if (EXISTS "${_xray_nri_vk_core}") + file(STRINGS "${_xray_nri_vk_core}" _xray_nri_vk_ver REGEX "^#define VK_HEADER_VERSION ") + if (_xray_nri_vk_ver MATCHES "VK_HEADER_VERSION ([0-9]+)" AND CMAKE_MATCH_1 GREATER_EQUAL 354) + set(_xray_nri_vk_ok TRUE) + endif() + endif() + if (NOT _xray_nri_vk_ok) + set(_xray_nri_vk_zip "${CMAKE_BINARY_DIR}/_deps/vulkan_headers-v1.4.354.zip") + set(_xray_nri_vk_extract "${CMAKE_BINARY_DIR}/_deps/vulkan_headers-v1.4.354-extract") + message(STATUS "NRD: upgrading Vulkan-Headers for NRI to v1.4.354") + file(DOWNLOAD + "https://github.com/KhronosGroup/Vulkan-Headers/archive/refs/tags/v1.4.354.zip" + "${_xray_nri_vk_zip}" + SHOW_PROGRESS + STATUS _xray_nri_vk_dl + ) + list(GET _xray_nri_vk_dl 0 _xray_nri_vk_dl_code) + if (NOT _xray_nri_vk_dl_code EQUAL 0) + message(FATAL_ERROR "NRD: failed to download Vulkan-Headers v1.4.354") + endif() + file(REMOVE_RECURSE "${_xray_nri_vk_extract}") + file(ARCHIVE_EXTRACT INPUT "${_xray_nri_vk_zip}" DESTINATION "${_xray_nri_vk_extract}") + file(REMOVE_RECURSE "${CMAKE_BINARY_DIR}/_deps/vulkan_headers-src") + file(RENAME "${_xray_nri_vk_extract}/Vulkan-Headers-1.4.354" "${CMAKE_BINARY_DIR}/_deps/vulkan_headers-src") + file(REMOVE_RECURSE "${_xray_nri_vk_extract}") + endif() + + if (SHADERMAKE_PATH AND NOT EXISTS "${SHADERMAKE_PATH}") + get_filename_component(_sm_dir "${SHADERMAKE_PATH}" DIRECTORY) + get_filename_component(_sm_alt "${_sm_dir}/../ShaderMake" ABSOLUTE) + if (EXISTS "${_sm_alt}") + file(MAKE_DIRECTORY "${_sm_dir}") + file(CREATE_LINK "${_sm_alt}" "${SHADERMAKE_PATH}" SYMBOLIC) + message(STATUS "NRD: linked ShaderMake -> ${SHADERMAKE_PATH}") + endif() + endif() + + message(STATUS "NRD: enabled (NRI + SPIRV, normalEnc=3 roughnessEnc=1)") +endif() diff --git a/framegraph_docs/FORWARD_PLUS_MIGRATION_PLAN.md b/framegraph_docs/FORWARD_PLUS_MIGRATION_PLAN.md index 47adbca7467..f537602b37f 100644 --- a/framegraph_docs/FORWARD_PLUS_MIGRATION_PLAN.md +++ b/framegraph_docs/FORWARD_PLUS_MIGRATION_PLAN.md @@ -806,6 +806,18 @@ MaterialPSO* MaterialCache::GetOrCreateDepthPSO(dxRender_Visual* visual) { **Goal:** Convert legacy diffuse/specular/gloss materials to PBR workflow +### Implementation status (shipped) + +- CLI: `src/Tools/MaterialConverter/` → target `MaterialConverter` + - `scan|convert|fix|report --root ` + - `--mode heuristic` (default), `--mode ai` when `-DXRAY_USE_AI_PBR=ON` +- Output: packed `_pbr.dds` (R=M, G=R, B=AO, A=Parallax) + `.thm` `pbr_name` +- Runtime fallback: `PBRTextureConverter::ConvertTexturesToPBR` uses heuristic when AI unavailable +- Shader PBR: `ResolveMaterialPBR` / `SamplePBRFull` in `bindless_common.h`; LOD/scope/particle_lit/decal consume PBR +- Smoke: + 1. `MaterialConverter convert --root res/gamedata/textures --kind OpaqueDielectric` + 2. Load level with `r4_use_pbr 1`, confirm `MAT_FLAG_HAS_PBR` on forward/skinned/terrain/LOD + This is the **perfect time** to modernize to PBR because: - Shaders are being rewritten anyway (no legacy baggage) - Texture conversion can happen offline (one-time cost) diff --git a/res/gamedata/configs/text/eng/openxray.xml b/res/gamedata/configs/text/eng/openxray.xml index 81eaf6d4632..3c869fb113c 100644 --- a/res/gamedata/configs/text/eng/openxray.xml +++ b/res/gamedata/configs/text/eng/openxray.xml @@ -126,4 +126,40 @@ Grass shadows + + Ray-traced GI (ReSTIR) + + + RT GI intensity + + + RT denoise + + + Upscaling (0=Off 1=FSR 2=DLSS) + + + Upscale quality + + + Render scale + + + FSR frame generation + + + DLSS frame generation + + + DLSS ray reconstruction + + + AMD ray regeneration + + + TAA + + + CAS sharpen + diff --git a/res/gamedata/configs/text/rus/openxray.xml b/res/gamedata/configs/text/rus/openxray.xml index 24389b53094..642286dc72a 100644 --- a/res/gamedata/configs/text/rus/openxray.xml +++ b/res/gamedata/configs/text/rus/openxray.xml @@ -1,138 +1,174 @@ - . + ��������� ��. ��� �������� - + ��������� �� ������ - + ������ � ������ - + �������� - + �������� ��������� - : + ������: ���������� � - : + ������: �������� - : + ������: ������������� - + �������� ����� ���� - Alt+Tab + �� ������� �� ����� ��� Alt+Tab - . + ��������� ����. ����� - + ����� ���������� - + ��� ������� - . + ���. ������ ��������� - . + ����. ������ ��������� - + ������ �������� - . (OpenGL) + ��. ������ ��������� (OpenGL) - + ����� ���� - + ������� - + ������� ��� ����� - + ������������� - ( Alt+Tab) + ������������� (������� Alt+Tab) FXAA - + �������� ������� ���� - + ����������� ��������� - + ��������� ������ - + ��������� ����� - + �������� ��������� - + ���� ������ - HUD + ���� ������ HUD - + ���� - + ��������� �������� - FPS + ����������� FPS � ���� - FPS + ����������� FPS � ���� - (Czech) + ������� (Czech) - (English) + ���������� (English) - (French) + ����������� (French) - (German) + �������� (German) - (Italian) + ����������� (Italian) - (Japanese) + �������� (Japanese) - (Polish) + �������� (Polish) - (Russian) + ������� (Russian) - (Spanish) + ��������� (Spanish) - (Ukrainian) + ���������� (Ukrainian) - + ���� �� ����� + + + ����������� GI (ReSTIR) + + + ������������� RT GI + + + ������ RT + + + ������� (0=���� 1=FSR 2=DLSS) + + + �������� �������� + + + ������� ������� + + + FSR ��������� ������ + + + DLSS ��������� ������ + + + DLSS ray reconstruction + + + AMD ray regeneration + + + TAA + + + CAS �������� diff --git a/res/gamedata/configs/ui/ui_mm_opt.xml b/res/gamedata/configs/ui/ui_mm_opt.xml index 57a499e072c..1c0c02beedd 100644 --- a/res/gamedata/configs/ui/ui_mm_opt.xml +++ b/res/gamedata/configs/ui/ui_mm_opt.xml @@ -514,6 +514,98 @@ + + ui_mm_rt_gi + + + ui_inGame2_checkbox + + + + + ui_mm_rt_gi_intensity + + + + + + + ui_mm_denoise + + + ui_inGame2_checkbox + + + + + ui_mm_upscale + + + + + + + ui_mm_upscale_quality + + + + + + + ui_mm_render_scale + + + + + + + ui_mm_fsr_fg + + + ui_inGame2_checkbox + + + + + ui_mm_dlss_fg + + + ui_inGame2_checkbox + + + + + ui_mm_dlss_rr + + + ui_inGame2_checkbox + + + + + ui_mm_amd_rr + + + ui_inGame2_checkbox + + + + + ui_mm_taa + + + ui_inGame2_checkbox + + + + + ui_mm_cas + + + ui_inGame2_checkbox + + + ui_mm_simply ui_inGame2_button diff --git a/res/gamedata/configs/ui/ui_mm_opt_16.xml b/res/gamedata/configs/ui/ui_mm_opt_16.xml index 7f26722d053..90fad67ba20 100644 --- a/res/gamedata/configs/ui/ui_mm_opt_16.xml +++ b/res/gamedata/configs/ui/ui_mm_opt_16.xml @@ -511,6 +511,98 @@ + + ui_mm_rt_gi + + + ui_inGame2_checkbox + + + + + ui_mm_rt_gi_intensity + + + + + + + ui_mm_denoise + + + ui_inGame2_checkbox + + + + + ui_mm_upscale + + + + + + + ui_mm_upscale_quality + + + + + + + ui_mm_render_scale + + + + + + + ui_mm_fsr_fg + + + ui_inGame2_checkbox + + + + + ui_mm_dlss_fg + + + ui_inGame2_checkbox + + + + + ui_mm_dlss_rr + + + ui_inGame2_checkbox + + + + + ui_mm_amd_rr + + + ui_inGame2_checkbox + + + + + ui_mm_taa + + + ui_inGame2_checkbox + + + + + ui_mm_cas + + + ui_inGame2_checkbox + + + ui_mm_simply ui_inGame2_button diff --git a/res/gamedata/scripts/ui_mm_opt_video_adv.script b/res/gamedata/scripts/ui_mm_opt_video_adv.script index 2e60eba81a5..6a389aeb48d 100644 --- a/res/gamedata/scripts/ui_mm_opt_video_adv.script +++ b/res/gamedata/scripts/ui_mm_opt_video_adv.script @@ -248,6 +248,66 @@ function opt_video_adv:InitControls(x, y, xml, handler) xml:InitStatic("video_adv:cap_r4_tessellation", _st) ctl = xml:InitCheck("video_adv:check_r4_tessellation", _st) table.insert(handler.m_preconditions, {func=mode_4, control=_st}) + + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_rt_gi", _st) + ctl = xml:InitCheck("video_adv:check_rt_gi", _st) + table.insert(handler.m_preconditions, {func=mode_ge_4, control=_st}) + + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_rt_gi_intensity", _st) + ctl = xml:InitTrackBar("video_adv:track_rt_gi_intensity", _st) + table.insert(handler.m_preconditions, {func=mode_ge_4, control=_st}) + + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_denoise", _st) + ctl = xml:InitCheck("video_adv:check_denoise", _st) + table.insert(handler.m_preconditions, {func=mode_ge_4, control=_st}) + + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_upscale", _st) + ctl = xml:InitTrackBar("video_adv:track_upscale", _st) + table.insert(handler.m_preconditions, {func=mode_ge_4, control=_st}) + + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_upscale_quality", _st) + ctl = xml:InitTrackBar("video_adv:track_upscale_quality", _st) + table.insert(handler.m_preconditions, {func=mode_ge_4, control=_st}) + + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_render_scale", _st) + ctl = xml:InitTrackBar("video_adv:track_render_scale", _st) + table.insert(handler.m_preconditions, {func=mode_ge_4, control=_st}) + + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_fsr_fg", _st) + ctl = xml:InitCheck("video_adv:check_fsr_fg", _st) + table.insert(handler.m_preconditions, {func=mode_ge_4, control=_st}) + + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_dlss_fg", _st) + ctl = xml:InitCheck("video_adv:check_dlss_fg", _st) + table.insert(handler.m_preconditions, {func=mode_ge_4, control=_st}) + + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_dlss_rr", _st) + ctl = xml:InitCheck("video_adv:check_dlss_rr", _st) + table.insert(handler.m_preconditions, {func=mode_ge_4, control=_st}) + + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_amd_rr", _st) + ctl = xml:InitCheck("video_adv:check_amd_rr", _st) + table.insert(handler.m_preconditions, {func=mode_ge_4, control=_st}) + + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_taa", _st) + ctl = xml:InitCheck("video_adv:check_taa", _st) + table.insert(handler.m_preconditions, {func=mode_ge_4, control=_st}) + + _st = xml:InitStatic("video_adv:templ_item", nil) + xml:InitStatic("video_adv:cap_cas", _st) + ctl = xml:InitCheck("video_adv:check_cas", _st) + table.insert(handler.m_preconditions, {func=mode_ge_4, control=_st}) --------- _st = xml:InitStatic("video_adv:templ_item", nil) xml:InitStatic("video_adv:cap_vsync", _st) diff --git a/res/gamedata/shaders/r5/batch_compact.cs b/res/gamedata/shaders/r5/batch_compact.cs index de501801cdb..0cea9c5c372 100644 --- a/res/gamedata/shaders/r5/batch_compact.cs +++ b/res/gamedata/shaders/r5/batch_compact.cs @@ -4,7 +4,6 @@ // Input: Static draw args + visibility + prefix buffers // Output: Visible batches in compact arrays + their original indices + material IDs #define SM_5_0 -#include "common.h" #define COMPACT_GROUP_SIZE 256 diff --git a/res/gamedata/shaders/r5/batch_compact_count.cs b/res/gamedata/shaders/r5/batch_compact_count.cs index 16141b52a9e..d37390661ec 100644 --- a/res/gamedata/shaders/r5/batch_compact_count.cs +++ b/res/gamedata/shaders/r5/batch_compact_count.cs @@ -2,7 +2,6 @@ // Per-group prefix scan for visible batches. // Outputs local prefix per batch and group-visible counts. #define SM_5_0 -#include "common.h" #define COMPACT_GROUP_SIZE 256 diff --git a/res/gamedata/shaders/r5/batch_compact_scan.cs b/res/gamedata/shaders/r5/batch_compact_scan.cs index c4620b4bd0f..ca9f989f47f 100644 --- a/res/gamedata/shaders/r5/batch_compact_scan.cs +++ b/res/gamedata/shaders/r5/batch_compact_scan.cs @@ -1,7 +1,6 @@ // batch_compact_scan.cs // Prefix scan over per-group counts to produce group offsets and total visible count. #define SM_5_0 -#include "common.h" #define COMPACT_GROUP_SIZE 256 diff --git a/res/gamedata/shaders/r5/bindless_common.h b/res/gamedata/shaders/r5/bindless_common.h index cc02e2bc7a7..3a109e24180 100644 --- a/res/gamedata/shaders/r5/bindless_common.h +++ b/res/gamedata/shaders/r5/bindless_common.h @@ -40,6 +40,10 @@ struct MaterialData uint detailBumpIndex; // s_detailBump uint detailBumpXIndex; // s_detailBumpX uint tessMethod; // 0=off, 1=PN, 2=HM, 3=PN+HM + uint sssMapIndex; // SSS map (R thick, G strength, B profile) + float emissiveIntensity; + uint _pad1; + uint _pad2; }; // Material flags @@ -57,7 +61,14 @@ struct MaterialData #define MAT_FLAG_HAS_DETAIL_BUMP (1 << 11) #define MAT_FLAG_HAS_LMAP (1 << 12) #define MAT_FLAG_FOLIAGE (1 << 13) // trees / bushes / leaves — enable SSS -#define MAT_FLAG_HAS_SSS_MAP (1 << 14) // detailBumpXIndex holds SSS thickness map (R) +#define MAT_FLAG_HAS_SSS_MAP (1 << 14) // sssMapIndex valid (R thick, G strength, B profile) +#define MAT_FLAG_MULTIPLY (1 << 15) // DestColor*SrcColor (burns / wall stains) +#define MAT_FLAG_PARTICLE_HARD (1 << 16) // disable soft-particle depth fade +#define MAT_FLAG_GLASS (1 << 17) // thin-glass transmission + +#define SSS_PROFILE_SKIN 0.0 +#define SSS_PROFILE_GRASS 1.0 +#define SSS_PROFILE_GLASS 2.0 #define TESS_METHOD_OFF 0 #define TESS_METHOD_PN 1 @@ -182,6 +193,7 @@ struct BumpSample }; // Classic sload.h dual-bump: Nu.wzy + (NuE.xyz - 1), gloss = Nu.x^2 +// IX-Ray PBS: Nu.wy = nxy, Nu.r = height; NuE = M/R/SSS/AO (not error@0.5) BumpSample SampleNormal(MaterialData mat, float2 uv) { BumpSample result; @@ -197,23 +209,35 @@ BumpSample SampleNormal(MaterialData mat, float2 uv) if ((mat.flags & MAT_FLAG_HAS_NORMAL_X) && mat.normalXIndex != INVALID_TEXTURE_INDEX) NuE = GetBindlessTexture(mat.normalXIndex).Sample(smp_linear, uv); - result.normal = Nu.wzy + (NuE.xyz - 1.0); - result.gloss = Nu.x * Nu.x; - result.height = NuE.w; + float errDev = abs(NuE.x - 0.5) + abs(NuE.y - 0.5) + abs(NuE.z - 0.5); + bool ixrayPbs = ((mat.flags & MAT_FLAG_HAS_PBR) != 0) && errDev > 0.25; - if ((mat.flags & MAT_FLAG_HAS_DETAIL_BUMP) && - mat.detailBumpIndex != INVALID_TEXTURE_INDEX && - mat.detailBumpXIndex != INVALID_TEXTURE_INDEX) + if (ixrayPbs) { - float2 duv = uv * mat.detailScale; - float4 NDetail = GetBindlessTexture(mat.detailBumpIndex).Sample(smp_linear, duv); - float4 NDetailX = GetBindlessTexture(mat.detailBumpXIndex).Sample(smp_linear, duv); - result.gloss = result.gloss * NDetail.x * 2.0; - result.normal += NDetail.wzy + NDetailX.xyz - 1.0; + float2 nxy = Nu.wy * 2.0 - 1.0; + result.normal = float3(nxy, sqrt(max(0.0, 1.0 - saturate(dot(nxy, nxy))))); + result.gloss = saturate(1.0 - NuE.y); + result.height = Nu.r; + } + else + { + result.normal = Nu.wzy + (NuE.xyz - 1.0); + result.gloss = Nu.x * Nu.x; + result.height = NuE.w; + + if ((mat.flags & MAT_FLAG_HAS_DETAIL_BUMP) && + mat.detailBumpIndex != INVALID_TEXTURE_INDEX && + mat.detailBumpXIndex != INVALID_TEXTURE_INDEX) + { + float2 duv = uv * mat.detailScale; + float4 NDetail = GetBindlessTexture(mat.detailBumpIndex).Sample(smp_linear, duv); + float4 NDetailX = GetBindlessTexture(mat.detailBumpXIndex).Sample(smp_linear, duv); + result.gloss = result.gloss * NDetail.x * 2.0; + result.normal += NDetail.wzy + NDetailX.xyz - 1.0; + } + + result.normal.z *= 0.5; } - - // Match sload() contrast tweak - result.normal.z *= 0.5; return result; } @@ -234,13 +258,35 @@ float4 SampleDetail(MaterialData mat, float2 uv) // PBR SAMPLING // ───────────────────────────────────────────────────── -float3 SamplePBR(MaterialData mat, float2 uv) +float4 SamplePBRFull(MaterialData mat, float2 uv) { if (mat.pbrIndex == INVALID_TEXTURE_INDEX) - return float3(0.0, 0.5, 1.0); // Default: non-metallic, medium rough, full AO + return float4(0.0, 0.5, 1.0, 0.5); Texture2D tex = GetBindlessTexture(mat.pbrIndex); - return tex.Sample(smp_linear, uv).rgb; + return tex.Sample(smp_linear, uv); +} + +float3 SamplePBR(MaterialData mat, float2 uv) +{ + return SamplePBRFull(mat, uv).rgb; +} + +void ResolveMaterialPBR(MaterialData mat, float2 uv, float gloss, + out float metallic, out float roughness, out float ao, out float parallax) +{ + metallic = 0.0; + roughness = saturate(1.0 - gloss); + ao = 1.0; + parallax = 0.5; + if (mat.flags & MAT_FLAG_HAS_PBR) + { + float4 pbrSample = SamplePBRFull(mat, uv); + metallic = pbrSample.r; + roughness = pbrSample.g; + ao = pbrSample.b; + parallax = pbrSample.a; + } } #endif // BINDLESS_COMMON_H diff --git a/res/gamedata/shaders/r5/bindless_depth.ps b/res/gamedata/shaders/r5/bindless_depth.ps index 1a7f6dbadcd412ab57dffc0a6c25cdc4327d7992..35fd2de5f918cf3a17af01dd1e90dafb36b47ed7 100644 GIT binary patch delta 246 zcmaFJvYTy!mQr$lUU6w|YOz96W?o88YH@LVT7FS^Vo{1-S+PQXX-PrpM2i|RV}OVY(+N43`v2JLQZaI7@7j}jla_v0bA5d ADgXcg delta 40 wcmdnZ_K;OV diff --git a/res/gamedata/shaders/r5/bindless_depth_opaque.ps b/res/gamedata/shaders/r5/bindless_depth_opaque.ps index ee139cc2c52af8fe5a763ecbbe3063763390bdf3..265b28cda21c0c03a7d58db2e5a4aaac3b19b007 100644 GIT binary patch literal 367 zcmZ9|Jr9B~5C-7Azv4#S6#PhJ6bBa}eh@cO1@)5BBjr#NbFSG1eg_<|A?=3I7idDwxhYA>&p&EALLu!HO0^7~$g#5# pE_s3P8r;Z<13Xa0F8Kq~Ks*B>u7sJ5K;8Rw3aHDP`?lJrcmZ9E zIrk*@ro%+atBk_=CleW`TH{m}g%rW&9Cd!dCWkdCN=}t_Aks!vse$Hsp$9^$ zg76l6^9MFWtf??9y!QwHWd9lch9M*qhN@C>Qoo|IAr!=>7~yf$DtX%~2^(6l7Irs4 z`vF2%}Fi}>j(lizs-NJAP@$@Ym$B|X8lGnw^ zjx`#s8nKhPrKqWtXfEL5dfGsExgqw}&(u5|eo;cVmP!Bl_`$ONL!K^5%$cA>T{hha dj^p+NaxzFcE4`-vmFrDVpBFUu8_pi>(GOz6*x~>H literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/bindless_forward.ps b/res/gamedata/shaders/r5/bindless_forward.ps index 1c9da013404b5b98a25f3115384dd465341c2c4a..207a047c4d1a1bfa0e578eaad6f58d74f5335b97 100644 GIT binary patch literal 5101 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 diff --git a/res/gamedata/shaders/r5/bindless_lod.ps b/res/gamedata/shaders/r5/bindless_lod.ps new file mode 100644 index 0000000000000000000000000000000000000000..6dfcb6c0cc31cdedcae56c047dfcf570c19b0a90 GIT binary patch literal 2091 zcma)7T~FIE6n*cnxEe2M2_b39v<>kvkxi*o(Fh9KTjVxw8!3q^J6#JC|9#gVNt}Y% zc&M;_?#DUj+BwUsRk6<~d=gv2X?D3{WyvaU`)P329_hVQkaAa0F67~0L67yG({ThM z-O@6LZ@}p$7d7Qh9SpAr15xw+s)pnto!%vni@~1(;NQAnq@KWb$AnyB*uztrFp<|e ztFAjp2nWWC3{McU`0?v@KEIEA3owRy<*O7HAUFX8r+~2qm;hPpbIfo%pUv;>{2^3~ zm!wcVHVuT=gV!Fl`QzdZw-qZGSBVw51>qZ7+o>mAYRz9m?B*x1B!b?mOjeeD`>jHD z7Skfiu6u)$%?Oj=Z10@N zcOeR7V`S|e4423|!01Nrz4YldaWTflr*W|rcf`4ox~z&EtKh)}IA1?wyO%J&y7K&? zyfdwEA>3hRJ%RZPbz;fGRu8Umduf*Qv~gAS7s68yN1thmZEnyMMr;#>;F)?|Q}w18s>=Cun5Cp#=CrEi zw#g#H&+T~4+K!g3kMt-%_ce`wQ|VHt*%O>=P8KDovh6$6Wo(Z+Yzihs!VUy|Gx%hO*h!k5K4fsxN9(%=IKwyjd^4p yjBzbhm}fpkEdYnDQ&87oUiZalaYnY2S!3-`qfGiLJKdn$l{v-Nr2_2pb?_JZVpY#7(iU2%wcyS$otd=7X;NkqT!`isggk;z zAYHlgDHI>avFRnYDmcvIKb&*^^UY!2R|jj4#~V9c2Q!~g*cKhcDai_-+Xz#uhxmR0_6~9`baXp|*e&)Xb)JUNuWr-E1@~C)G2nY&K5S@GXh7iYWW! zQkvDIks>g4=v7_6Rn--U9nuvTaq^@rCI;6_nM9Dyf=7j@QLp2Y1IQV|Lk^K88OR$# zYcc9bS&$MDDT-f0Do{#Q=p*iRF{75y(tus&a*(v0XkyofA^X%F3F= HEPs3gvV6yH delta 227 zcmdlibd7IPSuec;JucTOWvJJE1ygOjcs~q>1FpB4d4+ZoPtHuuBwDQVU8lz|Ppr&T7dxIi2GS0KZ#`5dE%Su}y%0vSn6RiXw?qxRzrZkYmA8+@e82tjU!`l;kQrEG0qyd*AFq zE+q$Pg&>iZbH15*JIwP)EaObD`AzUk(C<8tvoNV5!JdUOO=ae*?)#*7paUPNDK( zvw3j+e)eI}`PyOlXcX9bgUspkip}o>zz~GCB5lQoN$Tsm#Bq*9WUZGuCa0*?O^y) zlqMC`sPA%_&L}>wiPDWvdn7YMX6(>l!BA^RJ~lr7Zd8yd2Jv>eEJO(nN8jJ=OP$LK zJ<}HYY_c$jm$F3rAziJzL^~^(Sga25&l~Mg*?P_5<6{lX*BTy$hl)z0Vd$Zr*^h3- z$1FC<5V9_>XgBcc(2G`uSx3hNu8b0SooYoN_#Qc-L`}{>`M(&g< zi|eO9&^4`5YmO`uH1yW3nk)EJ4xe(T0$qIGh+3uC$Py!f?t>mZ@oiBYSEZa%p{){S zdQy+reo7IjasCOikt{u<*<>%Vjf9P3t%@5h#`f$P>}Vm$Y?|N2iQr z4uY(qFxGpfSDrS$wU0@NT`{s8THR2?wdv}>$s zONL&QG0P=Nzc58%WRpQ)?^^Lh86rtd@K>b#MJuE&d6Ml1x zn7LAYF=kP`Tvmmc^K_F?yXr3!edy% z^+57uvnKXytMNT0ZZZs$c;ji66S@z5_5DsP5saz~HDAb6k@KLgv%h7k3)zbo>?g{^ zUnYE2uovv+Y!SSho?QmBv%AIh`Sc>VI=h>6_estmRH(garciC|Q2tc%$itwFJooPID^kFnV5 z!8!n?MULc7*F)LAhuU9M6!ffh2zii@_$&fTwmoLdJJ~gcaK);7-nFH1u^0XSGFNP{ z|Een7LM4!A9Z_-wpXi7|LUFwl@&0u|A>|!cJNFS2q=T}b@5Vx3q6U$9?NC5 zT4#8x=rNAPX)WkJ(RS}dA(KaOW5{Cm``uI1jjr|kJ@x~OCJQTt>{F1I{Yf?kFO7_) zNf4%NuOaWD*4#+CNAz!_s*`iJC@AoU!b_{fOM`o}297a%#80-pdVm^}=YMgnj95`+ z?JMwP8sLoRBG?+yP-b}Dc#tw-dAW)|WJyewMW+E?z5}74b#Sj~Kvj6OACQbqlTCxQ zNMpRzVPP;d=rzWEBWQ-$rix8&}bSaZWX$%d5mx--vQ8BX)Yq22C{% zEZ_ywyj*z$OauOad9)N2sQ%7z+$1TILMWy@bT<6HGd%>gAE4PdYX3NLp8Q9@BklKV zO_^roLcry?sV!q&AX22yJ}8HAhA44}@_;oP5DcyA^~zTju6=3a=8<`KuFHn0qW{t$ j*G8gwd#;7r%Z@6AZ84){Nv!FI_H?+hnjKPW(h~VERX4OQ literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/bindless_particle_sixway.ps b/res/gamedata/shaders/r5/bindless_particle_sixway.ps new file mode 100644 index 0000000000000000000000000000000000000000..e3141cdafc093ef5d37d378da2bd38a93dc654c4 GIT binary patch literal 4202 zcma)9ZExC05dO}u*lVTAB;X@J??aRHL_?Y)kp#u`<*qte8@z_q`K|UkkXHTgH?y<0 z*N{eMRgJM{o|$>(ZT&J3p-cqxuKhp!LFc7RR#6!U_F|RBahkZB7uK_TnFNt23je8> z#fIl1_`N9ekgtRv^ZehUt!^a1K%oT4`jK33luXtr)>(?3DswTMo`q4$)%Y!2``$_< z;&Ylu!6Gf#8nS!9^^vO$L$U^@e$$e4s%pnI@-=$dDoBN9kbU_j(q8gX=tAt5a0@JRA#Hnn^jpLTu% z5lytWAD0K=lB+t_CC}GFwIeZpSnusX#_XOK;zCDjiP)ix?O1_OG6J!nYnt*A^LU&^ z!jrqt{L#5Kp%t7)3O^YI|2gy)!*(+DU^DYq~nSE=A`Q zaR&XD2%h&Kl&+hv@5#MqEl)3(lbL0?4&oP<1EIK#{)c6onRy1(#eCj(9d|z1i*f8T?Q-L%2tL^bIlO`A}EqiZ}8L(=t4^jnKOQ&{R=N05NybGSxukQ00Ybq`b8A(Oplo!(lQI20(rB1;uC4%94!xO~in`^erxo4yX4N^O#YZj&cR z=gb4u15fJwp(tK80c0s0&Zy(r zk>N@&DDXw6RwwqTijWl?h7b%E2NMZ1W8O8;1Zx^tBkOqt8=rO{Ogou4)d7IXBr8=l zKN_L5RPfw1&i-D4RImN&VewT%xUfOY9yXP$t9A?(7lvgaXcd4a-Aw5zu8bb};8sTq z6Nf2!LkSeDje{!$Xd=s+re>*VMW8a(OJTp zT=7`s+^>to-BK~KSFhM_PcP8L}G$9B8SL zgZ~e^6fU09pcr6ZRVl|(Mh|DjhcfWsP<#ysV#naRTVoid@Pd9^@jPfI1FF`-6w|Nz z%OvGNitSY9;PL?}&@Bxznxk}R{7;~UM-?pD${>v^@hJRiob{N2w|b=kf*qz8sf|Uv zRGSp0-~WdzGWdYdI~4q~ob0oic>J%l@h{ah%)bl=;lo2b+q`i0g zb03~z=lRv5Fpd=x4l0Cc0SsMwwB<_vquMdU${J>zZPJ#5QZhnhv;|EsZp~rPO z9Ju&4*GA)}G2R b%yF6#M@Fftkp_658>}5tG0ToLaoha|i>4Dj literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/bindless_skinned.ps b/res/gamedata/shaders/r5/bindless_skinned.ps index 61da7f6e6c37b20c1f07ad3137ee51540fafbdf9..3a26eb469f10b5d46180f540aa149b83d68eb1ca 100644 GIT binary patch literal 3419 zcmai0ZI9wM5dO}uaPOoltg`7Y(0=HussiL#bi70)%eJa2%Mb(UI&qYQUG`4*-#d=& zVBQvVFY(Od@ys*h@qCWxDo!a1#`rgU*Z8cKZZDk9@Us`k}~-EHy(CJwH>v_f$#P3yd#{5(AK>A{e+nDl8wh?Y#!1;h886~8<|+Ce1#|4H)x#drMsGZ45oiDsdVlq% z(^>z{0JKm^cT3Lm$N=0MfHq2bo)BiJTL>DP3eQ5l6Fv*xcg(qw*IpaBO0xCk1uW4xp+S$iZ>&-YjXh90vJ2qj) z+A+zjjX<=*HAs;32?B^yOXmy4X|^q!Ds6iZ-2;r! zL{NnURtt-NK%&U|SJ|#ArH#TAkTR3;H)>U0IVv&DgM4+*A?qX_Vkc|LlQV|pK_<3^ zwyyx3R^{)j>VKMB;IJ(@a&#f;wXR$noVQUI{#M6<)qQ<;P!z9L+kyrp$ryzb& zt;4A>)j|fV82d1|z8AW~li*bUi)cw=cAWc4cqYI)CF6qpJH|XFbBF{Nsp$Z8l;5=jT(ndMot~V(>H8vvLM*78& zduFuY?3Iqyb@#h79j`4xLuBE$_Q8V5vVWNG5ZPDqHqULd*-HDCn zb*c=?Y|W7T&@33%E7v*#7JzpHT6Wb)64G6{&1qGvpdFKJm_~HhbmaZ8iX_7f#xRoG z(2Na>uE;w$3p+Fvj8_imOkQxS5{qP1ABucs*=&bsLz5W1T@tp$36ChlLSpHLE-uUp zuXz4OU=UfoMSztrq!!|Aky~|B)^!Oi%2L!>_@L}1Q0LFCT!O-L?(Ev^^*nqOu&hpS z+Xe5>w+#8rbVuH#-$Z{NJq-j#m%3dz>E}$US$i4T*bD+%VLG|1yU|# z!%+JDj3XiS`T(Wb6n3gXvpQHyo!$F48@1~;nNujcROJhXp+-Gi`#rc_^Xb4F;rTs` z*0thrM0fk%b$#o~{`M8U812uW-y69>FofyI5s!}!r}c^yM4C_LzDwdbzO-PA{1M`AS3WNlZr5v zl7~d0pd^G=6Do`=*n83G8XkRv%uYO0?Ey+23lEzpJwYSI#XgegX`8byqsDVMBea;0 zxI7sH#1md%f|@>A1=c7;vhhwH;UDD z3(PUfJyb9k_(TzXueh_G^hAbHh~U#8h$X@_g6=HU^ski*rw!vy7!8O4q|%Mjrv9?@ zLH{Y1wOgWRD2rm{7>&od@b%}?ynVZMYCqgB&?52+0{Hte=BaRotQv- zj_aSLC+AFvCMrV1dAvrctZy#g`ai(?<@)1^m5PIq6SK0Bg(3Bz0j}fLbBN$^f51%5 z2D>k+Z1@GqRQj;4H^~AKs#?~J$z{SKe*&1023(`@LBOaa@o-UOP@$sps@t%>yy{)HJk diff --git a/res/gamedata/shaders/r5/bindless_skinned_depth.ps b/res/gamedata/shaders/r5/bindless_skinned_depth.ps new file mode 100644 index 0000000000000000000000000000000000000000..ae3f5cce25e2855d73e5d8bc2b448269a20a6e5d GIT binary patch literal 762 zcmZva-)q}25Xay3SKJyHHf$zqx;-r9!JEBKG;vL=mcST7wdGVymW(BDfs+4y@-O1D z=|TAJKHt0ZofZWzh2oIy=qI{Fi^;shNfJjiyTd6>L!1zrrQ48XXdzTC+k(T@lTd}^ z#?V|>l~%#?)huX!38i@P-`K!Y!>U^H#&kktY}4iz(v0FHeIV!sfjgzt%-%q)jj%#1 z_zc-2O-E~eL00fbH?nXMhmiR2_5OYzEoZ84`R6}@D*eg#;j zj$GL}uzCeNi4nPExETPYg|fVJx6pu{ey?yQ6Wd~u{#^|@UkO}2%XL|RcWND)VPnZuBVDScj z_omZ%|E9vwLK!`-IZylw!)=A3jUt|egjL9`8aX4;mPT2H-mcMC!8u1qaYQ_bGMdDk z5$Qz^TNDyF3J4qaD2xcc#-3dVHdouRSjj3;mjpzuhtrCHU{PU%NMulm_RE0WUQ zsfentohq1x5b#!*CL_{Di^%7Yn;)l3$Nx7^vk;)MG>ym;>Obm`G^)XDqkTXRB2afI z%$XJ9rLDtkonExjZS$tlXdb_QQ$XsgaDcAw&@T7!hVQFUIJQv`?dLoL=~+Ue6f%cu zyN+$4?DNeRA)0&Qb=1HDD`E5#sL<|M#!bp>gwA_ZX$ zBFkhk7!&82@xNr^c?RczW|8AyteG$A(V8nvKvDf5Ks z!nR*7=Zt_}S%eUEZ^C->GigSjNY#S^h7Krv#j1dR)Cwh0&glDoW5(FcJnS=)4DkMki zGSP+%`@N;*Z(smP4g{vk&A|uR>R~sBI9?@I5m8eKfcjbBUZ^zn%$jLKRB#x`!N@Ko z1JI@bRK(6g_Yy`b$6=UqMfoW3VYG91#ODY7%A2~?Zr2n1^vM7ynzHy+E4)JtW?xI7 zrtC&3fgFLZV@aje7O%4&DvE(E)(Deu6~JgICA~!>870+Obf2vq9U9OP70#-DMVqUe z15+o2wsr>dr_gr|fo;Dp5=UI6ETt0VN?n3;#RLs+_#K*&^;6f()Q(k_7jz;m?c9vF za(o5Eg;w2cen?d+g%p4wT~*qt!D22}9%^>gBLqL#r#3ZuVX|f^_BAdO=2Gv4Of6qF zJtUiH7K+V$^3HnD)|S)z!3ZyJ;bXHP)IVCC{@~~I)=%!Ye?k`@yO;I%Mvm(a;d}Ub p6APB-t#Cc3J9^O%7aDx4|B@QPfoIPB08o40nE9XwgJwsu{0GQf5>5aB delta 622 zcmZ9I&r2IY6vq)=Bhf|?#DX9X#Igy=x+;PY1gla6HIXdpF-&%5f5^tj_82{e0)W?|c8?@65N^uDwv)b`ILk?&qE6Nq4K& z{k4BnANo10Z;i!L7{!UQ6#43MF0lS%eAf>nQ~JP|)>ot9d;B!Iuu_EFi@0EjhUdiW zH$st5B2}*DUHXGD_wkJajaRNe6>i>-xhvFL7sVHgM>$f!h(eTxE^L5p+4UR|Jn@5w zNSUH#%Z3{GluMbnAf7ROZ-BOoBg2PVE&Zb~bGZs}M>vSZl%$TVNSVoys>0jDl(UAt z&2w2^zguF!^N9uqe_(jYlak47qSeXJh5kR*oX_?R2n*9lE~0LmaZG5trI9}27 z6MKz*o~0#l2=!_*K$xF_CpZI4aOfgMFzg=f07EgDUz*1?gijOMQ)P@?lIqG0h)hRF zRo1^J)=lpWTP~+uSiWdkS?v1watYt`Oms}LmE3CeZ7S*lFp(DM>W`Wqn4O4rYx C&%fdT diff --git a/res/gamedata/shaders/r5/bindless_skinned_mdi.ps b/res/gamedata/shaders/r5/bindless_skinned_mdi.ps index f6f7954a8e89919f833a15477162f10e8ca767bc..defd38994ff7ec715295eab32eb6251e30b6bef9 100644 GIT binary patch literal 3542 zcmai1X>a2;5dH37!R`Wu94KmHyZfQRqL6KEVyrJ%iqoPf2wI|Tb*TfAoH)DK|K8yx z)3=dB;ye!DycrJZj}cwQDMkJWf5LZ-kLuO!1(O+m@d7+@XJ65MZRic3{lJ@feLNgI zJ_UpEBmOd(y|}af4g$XXC$~R&IWi3=J$HD{=zski54ywJk9x=c)a&7Shm(lYkZmH0 zK7?74WT~_MV2mx|G-9+Y@hM~ZElyJ!;UtPr+t;L^(Qnz}KN?mzCk0`QZ1)!^C5F4g zWm$4#qpXTeSfQzp2jl5;(D>0n@L95qR2`IOWn9HsivB?U98cBT!w%9$Z&|@2umB|R z{_0I8v;Lg{XrYR3!z?Qz18{Eu+9=J6gfPS0+A~)qUD32M?Cm}KB0ffRBqGh?w4z0x zF(RVaz$;1!48?>E`i9HjFHDFE@-|2#x;5~h9vVL%Oz|eq!I64l#FnaJ?WQgrv>+wz9l9{J>6qfzPCy+_4H7CjLBMfp>HMNN%{Nt3rFjR+HWiG+Jf8hS zoj%K1&Q3Zh*GNg(Cl%qkqR(iB^r@^a@2wTu7148?Ai`By9-$rAAh`5X-pcTu?5)5~a zs^7E3pIjf0C$o_|JS5)TB)L*$0gtsbW3l%QWq6eQBP(SKwbJFJ^s7~?uj1rb4%k&w zvSAiq!;wRfuc{9Dd_E0`gw(r`kFq<+mf}-1oG|ma*t4xAm-x zh3fl0Tzsa7+fcEc!y>y4L*FT`5Bgix|8&1o0q`R7UJ2vr0pGl5CZ(ly+||k0M)Nx9 zhGe!@NIqy44DW(khr@he_eiU*x(x+%TWty|${wsEl8eK*>ByaCl}U~n+yF^oLt8gI zx;*(n7G`KDI2QzTCeP3k!#o+{L5Z(Cn~fE%X%Yk5kgyOZSwtD;BbGiIxN1wh%!)Sx zg}i)=04on#J&5x~Vb$$j>j^5#Qq)-N@_`Ryxq{;lHl`n{p+`vdsmah_rgInTlmT*&JsCqDF* M=JnwI(~*$>0Xl?7UH||9 delta 527 zcmca6eMV@4_(X%0iR%J3+cOq3P3~q<70k>lC@s+|Nv%lE&o4^RwC0+e$0|4ZGE3cL zAr_^{iL9A?s46C(VO37$Qa}QE`9-;jIhj?d8c>yqdFiQnC7L>TRV8JXU{&V_*ERVE z>t=Mz*Rokp_GcHhSFlwu&@-?Gt4+(vPb^U=$}dgN$V)9Q21*&~87Sx~r03)p7b7c4 z%!kN<{iwto;1o1jmQ%dGAgL%gF}ENm6(|c~0A)0C6H9ayP`wTe9GJIrQ%e$aax#;F zTHxCCiV*V1u0beBM<@Wh1uVl0vRW?*B2_#2B>ViywOkUDXL4yxKEvg~s5yBBw>E!D zW?EWlaVo@ldWn;-b0^rBr|6aE7v-b`rdlG5dWUPVs%KBb7y=3lKW7KDu?6%k(XjqT2<9*K+iTft2%PSJ@9fLo zJfN09nVsLv&U^M`P2(h|D461JxYs$+q<RSd;m+KY5R-Wu|^`e;^p#|Ap^{lg>$!M`^vL=u^b9 zjOE_bC+lpL^n8RW{9K%)Ur|XnNyTaD@WHq4WhWq6 zkJ3fKRO7$IAw(QQHG!DH7+FZ za+mSOSccd1IUzh@`DN!PsIPcgM;t8zd^cM>guA*5RwV)o7Y2Xeg{Ca67wCnR=^8iy z3CDk4&*#fg&jOqyPPY+b<=O(AS%3?avoa&8#XjF*bCPdp&Mp4M4u6##BKiuEvm}Rr z7c7+yqZ&w*LA=X^q<14r zY+w6!FdHS{t;mziF4Rz!yirFHNnX_4Re47-i8w0_#p1Z4+*C$Mp%ioENC9+;dfoti zK^lX;YFRiEj8n~kYdak%3l#|nTar7bN|4lK)VCm3b1(_474^6u?JKmwfoha#lLM6~ zLyrh2B0)%C&0+O7oamJOW2L^2I2JJluRpx+=2yR(Kg8cB{tfmgi(4Ou;~-Qb0xZ5LO$tZZ z2x?D-V;)J1C$glmF-YKKHZM?^gD0vPM2=;MJzd)X7fA7G^zazE>4ii5Y(rUgbyleSp%wfD&5D}IvG7hZ zg`oLVK(-LT;S8h=>4dq9hVVDDoD(tv)P^~$Q%BtEsUuft>F!AwmnBXTt>@4rJBFZ) za+0P=HoO17=+l2$Bzrl!yr!{sq|T@`Zo|22cztuZ)exHRcJo2{a%M}c%-RYk0|!JfaSVI#)~w zmWPlTbh|K9_1Vs>eSp;7rkWnmDvTh-$-r*5X@k| z?t};OutO7*jN@G(iWLTp*e_cDLFKDTZD*gVH${u2sds_SqbXmlrRfDQRVB=M_eRqN REmYlP&g}uQ*BL)M{{gCszSjT% literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/bindless_skinned_tess.ds b/res/gamedata/shaders/r5/bindless_skinned_tess.ds index 4d479ab76e5..23086a04f4b 100644 --- a/res/gamedata/shaders/r5/bindless_skinned_tess.ds +++ b/res/gamedata/shaders/r5/bindless_skinned_tess.ds @@ -1,7 +1,7 @@ -// bindless_skinned_tess.ds — Domain for skinned PatchList (PN + optional HM) #define SM_6_0 #include "common.h" #include "bindless_common.h" +#include "shared/tess_pn.h" struct HS_CONTROL_POINT { @@ -32,82 +32,71 @@ struct DS_OUTPUT [domain("tri")] [partitioning("integer")] -DS_OUTPUT main(HS_CONSTANT_DATA input, float3 bary : SV_DomainLocation, - const OutputPatch patch) +DS_OUTPUT main(HS_CONSTANT_DATA input, float3 uvw : SV_DomainLocation, + const OutputPatch bp) { DS_OUTPUT o; - float u = bary.x; - float v = bary.y; - float w = bary.z; - - MaterialData mat = g_Materials[patch[0].materialID]; - bool usePN = (mat.tessMethod == TESS_METHOD_PN || mat.tessMethod == TESS_METHOD_PN_HM); - - float3 pos; - float3 N; - if (usePN) + float minc = min(uvw.x, min(uvw.y, uvw.z)); + [flatten] if (minc != 0.0) { - float3 b300 = patch[0].worldPos; - float3 b030 = patch[1].worldPos; - float3 b003 = patch[2].worldPos; - float3 n200 = normalize(patch[0].normal); - float3 n020 = normalize(patch[1].normal); - float3 n002 = normalize(patch[2].normal); - - float3 b210 = (2.0 * b300 + b030 - dot(b030 - b300, n200) * n200) / 3.0; - float3 b120 = (2.0 * b030 + b300 - dot(b300 - b030, n020) * n020) / 3.0; - float3 b021 = (2.0 * b030 + b003 - dot(b003 - b030, n020) * n020) / 3.0; - float3 b012 = (2.0 * b003 + b030 - dot(b030 - b003, n002) * n002) / 3.0; - float3 b102 = (2.0 * b003 + b300 - dot(b300 - b003, n002) * n002) / 3.0; - float3 b201 = (2.0 * b300 + b003 - dot(b003 - b300, n200) * n200) / 3.0; + [flatten] if (((1.0 / 3.0) - minc) > 0.01) + { + float fK = (1.0 / 3.0) / ((1.0 / 3.0) - minc); + fK = minc < 0.1 ? fK : 1.0; + uvw = lerp(float3(1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0), uvw, fK); + } + } - float3 E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0; - float3 V = (b300 + b030 + b003) / 3.0; - float3 b111 = E + (E - V) * 0.5; + float u = uvw.x; + float v = uvw.y; + float w = uvw.z; - float v12 = 2.0 * dot(b030 - b300, n200 + n020) / max(dot(b030 - b300, b030 - b300), 1e-8); - float v23 = 2.0 * dot(b003 - b030, n020 + n002) / max(dot(b003 - b030, b003 - b030), 1e-8); - float v31 = 2.0 * dot(b300 - b003, n002 + n200) / max(dot(b300 - b003, b300 - b003), 1e-8); - float3 n110 = normalize(n200 + n020 - v12 * (b030 - b300)); - float3 n011 = normalize(n020 + n002 - v23 * (b003 - b030)); - float3 n101 = normalize(n002 + n200 - v31 * (b300 - b003)); + float3 triPos = bp[0].worldPos * w + bp[1].worldPos * v + bp[2].worldPos * u; + float2 tc = bp[0].texcoord * w + bp[1].texcoord * v + bp[2].texcoord * u; + float3 T = bp[0].tangent * w + bp[1].tangent * v + bp[2].tangent * u; + float3 B = bp[0].bitangent * w + bp[1].bitangent * v + bp[2].bitangent * u; + float3 Nlin = bp[0].normal * w + bp[1].normal * v + bp[2].normal * u; - float uu = u * u, vv = v * v, ww = w * w; - pos = b300 * (uu * u) + b030 * (vv * v) + b003 * (ww * w) - + b210 * (3.0 * uu * v) + b120 * (3.0 * u * vv) - + b201 * (3.0 * uu * w) + b102 * (3.0 * u * ww) - + b021 * (3.0 * vv * w) + b012 * (3.0 * v * ww) - + b111 * (6.0 * u * v * w); + MaterialData mat = g_Materials[bp[0].materialID]; + bool usePN = (mat.tessMethod == TESS_METHOD_PN || mat.tessMethod == TESS_METHOD_PN_HM); - N = n200 * uu + n020 * vv + n002 * ww - + n110 * (u * v) + n011 * (v * w) + n101 * (w * u); - N = normalize(N); - } - else + float3 pos = triPos; + float3 N = normalize(Nlin); + if (usePN) { - pos = patch[0].worldPos * u + patch[1].worldPos * v + patch[2].worldPos * w; - N = normalize(patch[0].normal * u + patch[1].normal * v + patch[2].normal * w); + TessPNEvaluate( + bp[0].worldPos, bp[1].worldPos, bp[2].worldPos, + bp[0].normal, bp[1].normal, bp[2].normal, + uvw, pos, N); } - float2 tc = patch[0].texcoord * u + patch[1].texcoord * v + patch[2].texcoord * w; - float3 T = normalize(patch[0].tangent * u + patch[1].tangent * v + patch[2].tangent * w); - float3 B = normalize(patch[0].bitangent * u + patch[1].bitangent * v + patch[2].bitangent * w); - bool useHM = (mat.tessMethod == TESS_METHOD_HM || mat.tessMethod == TESS_METHOD_PN_HM); - if (useHM && ((mat.flags & MAT_FLAG_HAS_NORMAL_X) != 0) && mat.normalXIndex != INVALID_TEXTURE_INDEX) + if (useHM && + ((mat.flags & MAT_FLAG_HAS_DETAIL) != 0) && + ((mat.flags & MAT_FLAG_HAS_NORMAL_X) != 0) && + mat.normalXIndex != INVALID_TEXTURE_INDEX) { float height = GetBindlessTexture(mat.normalXIndex).SampleLevel(smp_linear, tc, 0).a; - float hmScale = (parallax.x > 0.0) ? (parallax.x * 2.0) : 0.04; - pos += N * ((height - 0.5) * hmScale); + if ((mat.flags & MAT_FLAG_HAS_DETAIL_BUMP) != 0 && + mat.detailBumpXIndex != INVALID_TEXTURE_INDEX) + { + float2 tcd = tc * mat.detailScale; + float hDetail = GetBindlessTexture(mat.detailBumpXIndex).SampleLevel(smp_linear, tcd, 0).a; + height += (hDetail - 0.5) * 0.2; + } + pos += N * TessHMDisplace(height); } + [flatten] if (minc == 0.0) + pos = triPos; + o.worldPos = pos; o.position = mul(m_VP, float4(pos, 1.0)); o.texcoord = tc; o.normal = N; - o.tangent = T; - o.bitangent = B; - o.materialID = patch[0].materialID; + o.tangent = normalize(T); + o.bitangent = normalize(B); + o.materialID = bp[0].materialID; return o; } diff --git a/res/gamedata/shaders/r5/bindless_skinned_tess.hs b/res/gamedata/shaders/r5/bindless_skinned_tess.hs index 82c9e911531..aa74dca3268 100644 --- a/res/gamedata/shaders/r5/bindless_skinned_tess.hs +++ b/res/gamedata/shaders/r5/bindless_skinned_tess.hs @@ -56,17 +56,38 @@ HS_CONSTANT_DATA PatchConstant(InputPatch patch) f0 = ComputeEdgeFactor(patch[1].worldPos, patch[2].worldPos); f1 = ComputeEdgeFactor(patch[2].worldPos, patch[0].worldPos); f2 = ComputeEdgeFactor(patch[0].worldPos, patch[1].worldPos); + + if (mat.tessMethod == TESS_METHOD_PN) + { + float3 N0 = normalize(patch[0].normal); + float3 N1 = normalize(patch[1].normal); + float3 N2 = normalize(patch[2].normal); + float3 Nv0 = mul((float3x3)m_V, N0); + float3 Nv1 = mul((float3x3)m_V, N1); + float3 Nv2 = mul((float3x3)m_V, N2); + float3 Pv0 = mul(m_V, float4(patch[0].worldPos, 1.0)).xyz; + float3 Pv1 = mul(m_V, float4(patch[1].worldPos, 1.0)).xyz; + float3 Pv2 = mul(m_V, float4(patch[2].worldPos, 1.0)).xyz; + bool doDiscard = (Nv0.z > 0.1) && (Nv1.z > 0.1) && (Nv2.z > 0.1) && + (Pv0.z > 5.0) && (Pv1.z > 5.0) && (Pv2.z > 5.0); + if (doDiscard) + { + f0 = -1.0; + f1 = -1.0; + f2 = -1.0; + } + } } o.Edges[0] = f0; o.Edges[1] = f1; o.Edges[2] = f2; - o.Inside = max(1.0, round((f0 + f1 + f2) / 3.0)); + o.Inside = (f0 < 0.0) ? -1.0 : max(1.0, round((f0 + f1 + f2) / 3.0)); return o; } [domain("tri")] [partitioning("integer")] -[outputtopology("triangle_cw")] +[outputtopology("triangle_ccw")] [outputcontrolpoints(3)] [patchconstantfunc("PatchConstant")] [maxtessfactor(4.0)] diff --git a/res/gamedata/shaders/r5/bindless_terrain.ps b/res/gamedata/shaders/r5/bindless_terrain.ps index 9051597841ebc643dba95789e983028fd283bf6b..7cec47ed006eaa94f153dad148d3ba36dcb5910a 100644 GIT binary patch delta 1662 zcmZuxOK;;;6jn-NEAD6>NvAI*>5Zlx>^AWvX-Z+Lg4$6P!VDow0@D!^kz3!k7LHx~ zAdgNN3B(2gBXNEPivfAWEMdt4q+!Lj-5~*gU>3l=c4|8jURd|>o$oyEch31+pWpxP zwaBWB4-^ff=G_PF>h`^Mv%Yhu{azUrxf{cq)=s0bRjsw}@3h))Z#C|sqO%_PGCFlf zl}uCF=WRu^(4O5n-mS{;Yiu^=uHm0V3jWP75ND>K!(`YC#AD{2CDfEU$0}}N!;lp1 zO{^RqS=F8)^#d}GD@v_KsD5Cs&Q-9rt!T1}O|yl2mTh33;)uy(Cd-J1u)&?eS#%oy zW|o(8XiwKwq)__$o+)rA93r1`fcW5yo}9qzu^Gs-aVW7f*Sv5#k|k=o(UH^!{KQ`W z-rBaCX3@2TkWbQ<6aKg9}6T{>irbv*<0+erchD3=?`etrGG^jTa&xcCThV#tA zHT3Wy3Y}Km(@DWMx(!bPcp38Vz-Z$QAsSI zJSvI>4^MMFP<6>d+JKg`JuC_a`O;sqTW3T8aCO0OzU2smevug<;ReVEGY}SbChq@7yp+ zGrh9p`!b0l{5-P5_{i9yC4o=VU%l%JZ50u*1RX~@&C`Q2AhS%xWID*LrhslEe-pfX zjTi>#j;->R=`3fQSf#WUlA|Z+F18w@j;OF*foNoYl7}vcudLCDuLl+iPZjFV0#6Dot2w4hy75yi?(9|(%KP1+8mNxU~> z8r_KCM(OHET?x7s7vf#J6ITTv3pZ|jfSfOJ{mpCnC<| zZuBg(W6O|6yAA7uolpZ71=MA*?e@2}Bk22J5@>9Agt7jH4!<5g1LCm&BAS3nVKbo6 z?Jlkz_3x07;mr{Z-{%UrH?oMYMrTD`ejuZCYzkjXCA=ziga&@8mGOlH_(AHtlMe*^ zKGw!^UY|=b4%f9pKL!u=<0A072b;!>vLT>dG3%bR$i zeiGjlWO05TEdHrr5A*WGuE6(;pNEu9FCftj4DK0_NrWn55N$doGIS00H*ig-DjX_1&v7g*?#2pX9SJ(p6FdbS*HQR28d+I1~O z8OP_<1^&AV0xz6e8^WZf;)JRRm+)?_f`4nJG&9obINnhYwT3TMDa#MklBnCVjH0%O zU$vsN?zz6>I=wFcqw(DE*^Z7s^m@@Ut@8$i1II~in+D4~$D7k patch) +DS_OUTPUT main(HS_CONSTANT_DATA input, float3 uvw : SV_DomainLocation, + const OutputPatch bp) { DS_OUTPUT o; - float u = bary.x; - float v = bary.y; - float w = bary.z; - - uint matId = patch[0].materialID; - MaterialData mat = g_Materials[matId]; - bool usePN = (mat.tessMethod == TESS_METHOD_PN || mat.tessMethod == TESS_METHOD_PN_HM); - - float3 pos; - float3 N; - if (usePN) + float minc = min(uvw.x, min(uvw.y, uvw.z)); + [flatten] if (minc != 0.0) { - float3 b300 = patch[0].worldPos; - float3 b030 = patch[1].worldPos; - float3 b003 = patch[2].worldPos; - float3 n200 = normalize(patch[0].normal); - float3 n020 = normalize(patch[1].normal); - float3 n002 = normalize(patch[2].normal); - - float3 b210 = (2.0 * b300 + b030 - dot(b030 - b300, n200) * n200) / 3.0; - float3 b120 = (2.0 * b030 + b300 - dot(b300 - b030, n020) * n020) / 3.0; - float3 b021 = (2.0 * b030 + b003 - dot(b003 - b030, n020) * n020) / 3.0; - float3 b012 = (2.0 * b003 + b030 - dot(b030 - b003, n002) * n002) / 3.0; - float3 b102 = (2.0 * b003 + b300 - dot(b300 - b003, n002) * n002) / 3.0; - float3 b201 = (2.0 * b300 + b003 - dot(b003 - b300, n200) * n200) / 3.0; + [flatten] if (((1.0 / 3.0) - minc) > 0.01) + { + float fK = (1.0 / 3.0) / ((1.0 / 3.0) - minc); + fK = minc < 0.1 ? fK : 1.0; + uvw = lerp(float3(1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0), uvw, fK); + } + } - float3 E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0; - float3 V = (b300 + b030 + b003) / 3.0; - float3 b111 = E + (E - V) * 0.5; + float u = uvw.x; + float v = uvw.y; + float w = uvw.z; - float v12 = 2.0 * dot(b030 - b300, n200 + n020) / max(dot(b030 - b300, b030 - b300), 1e-8); - float v23 = 2.0 * dot(b003 - b030, n020 + n002) / max(dot(b003 - b030, b003 - b030), 1e-8); - float v31 = 2.0 * dot(b300 - b003, n002 + n200) / max(dot(b300 - b003, b300 - b003), 1e-8); - float3 n110 = normalize(n200 + n020 - v12 * (b030 - b300)); - float3 n011 = normalize(n020 + n002 - v23 * (b003 - b030)); - float3 n101 = normalize(n002 + n200 - v31 * (b300 - b003)); + float3 triPos = bp[0].worldPos * w + bp[1].worldPos * v + bp[2].worldPos * u; + float2 tc = bp[0].texcoord * w + bp[1].texcoord * v + bp[2].texcoord * u; + float3 T = bp[0].tangent * w + bp[1].tangent * v + bp[2].tangent * u; + float3 B = bp[0].bitangent * w + bp[1].bitangent * v + bp[2].bitangent * u; + float3 Nlin = bp[0].normal * w + bp[1].normal * v + bp[2].normal * u; + float hemi = bp[0].hemi * w + bp[1].hemi * v + bp[2].hemi * u; - float uu = u * u, vv = v * v, ww = w * w; - pos = b300 * (uu * u) + b030 * (vv * v) + b003 * (ww * w) - + b210 * (3.0 * uu * v) + b120 * (3.0 * u * vv) - + b201 * (3.0 * uu * w) + b102 * (3.0 * u * ww) - + b021 * (3.0 * vv * w) + b012 * (3.0 * v * ww) - + b111 * (6.0 * u * v * w); + uint matId = bp[0].materialID; + MaterialData mat = g_Materials[matId]; + bool usePN = (mat.tessMethod == TESS_METHOD_PN || mat.tessMethod == TESS_METHOD_PN_HM); - N = n200 * uu + n020 * vv + n002 * ww - + n110 * (u * v) + n011 * (v * w) + n101 * (w * u); - N = normalize(N); - } - else + float3 pos = triPos; + float3 N = normalize(Nlin); + if (usePN) { - pos = patch[0].worldPos * u + patch[1].worldPos * v + patch[2].worldPos * w; - N = normalize(patch[0].normal * u + patch[1].normal * v + patch[2].normal * w); + TessPNEvaluate( + bp[0].worldPos, bp[1].worldPos, bp[2].worldPos, + bp[0].normal, bp[1].normal, bp[2].normal, + uvw, pos, N); } - float2 tc = patch[0].texcoord * u + patch[1].texcoord * v + patch[2].texcoord * w; - float3 T = normalize(patch[0].tangent * u + patch[1].tangent * v + patch[2].tangent * w); - float3 B = normalize(patch[0].bitangent * u + patch[1].bitangent * v + patch[2].bitangent * w); - float hemi = patch[0].hemi * u + patch[1].hemi * v + patch[2].hemi * w; - bool useHM = (mat.tessMethod == TESS_METHOD_HM || mat.tessMethod == TESS_METHOD_PN_HM); - if (useHM && ((mat.flags & MAT_FLAG_HAS_NORMAL_X) != 0) && mat.normalXIndex != INVALID_TEXTURE_INDEX) + if (useHM && + ((mat.flags & MAT_FLAG_HAS_DETAIL) != 0) && + ((mat.flags & MAT_FLAG_HAS_NORMAL_X) != 0) && + mat.normalXIndex != INVALID_TEXTURE_INDEX) { float height = GetBindlessTexture(mat.normalXIndex).SampleLevel(smp_linear, tc, 0).a; - // Scale from r2_parallax_h (parallax.x); fallback 0.04m so HM is visible - // even when steep-parallax is off. Classic DX11 used a similar ~cm range. - float hmScale = (parallax.x > 0.0) ? (parallax.x * 2.0) : 0.04; - float displace = (height - 0.5) * hmScale; - pos += N * displace; + if ((mat.flags & MAT_FLAG_HAS_DETAIL_BUMP) != 0 && + mat.detailBumpXIndex != INVALID_TEXTURE_INDEX) + { + float2 tcd = tc * mat.detailScale; + float hDetail = GetBindlessTexture(mat.detailBumpXIndex).SampleLevel(smp_linear, tcd, 0).a; + height += (hDetail - 0.5) * 0.2; + } + pos += N * TessHMDisplace(height); } + [flatten] if (minc == 0.0) + pos = triPos; + o.worldPos = pos; o.position = mul(m_VP, float4(pos, 1.0)); o.texcoord = tc; o.normal = N; - o.tangent = T; - o.bitangent = B; + o.tangent = normalize(T); + o.bitangent = normalize(B); o.materialID = matId; o.hemi = hemi; return o; diff --git a/res/gamedata/shaders/r5/bindless_tess.hs b/res/gamedata/shaders/r5/bindless_tess.hs index 74da43dbab7..e2f50dc90c7 100644 --- a/res/gamedata/shaders/r5/bindless_tess.hs +++ b/res/gamedata/shaders/r5/bindless_tess.hs @@ -59,7 +59,6 @@ HS_CONSTANT_DATA PatchConstant(InputPatch patch) uint matId = patch[0].materialID; MaterialData mat = g_Materials[matId]; - // Classic: only model mats with TessMethod; never terrain (BmmD TessMethod=0) bool doTess = (mat.tessMethod != TESS_METHOD_OFF) && ((mat.flags & MAT_FLAG_WATER) == 0) && ((mat.flags & MAT_FLAG_TERRAIN) == 0); @@ -70,18 +69,39 @@ HS_CONSTANT_DATA PatchConstant(InputPatch patch) f0 = ComputeEdgeFactor(patch[1].worldPos, patch[2].worldPos); f1 = ComputeEdgeFactor(patch[2].worldPos, patch[0].worldPos); f2 = ComputeEdgeFactor(patch[0].worldPos, patch[1].worldPos); + + if (mat.tessMethod == TESS_METHOD_PN) + { + float3 N0 = normalize(patch[0].normal); + float3 N1 = normalize(patch[1].normal); + float3 N2 = normalize(patch[2].normal); + float3 Nv0 = mul((float3x3)m_V, N0); + float3 Nv1 = mul((float3x3)m_V, N1); + float3 Nv2 = mul((float3x3)m_V, N2); + float3 Pv0 = mul(m_V, float4(patch[0].worldPos, 1.0)).xyz; + float3 Pv1 = mul(m_V, float4(patch[1].worldPos, 1.0)).xyz; + float3 Pv2 = mul(m_V, float4(patch[2].worldPos, 1.0)).xyz; + bool doDiscard = (Nv0.z > 0.1) && (Nv1.z > 0.1) && (Nv2.z > 0.1) && + (Pv0.z > 5.0) && (Pv1.z > 5.0) && (Pv2.z > 5.0); + if (doDiscard) + { + f0 = -1.0; + f1 = -1.0; + f2 = -1.0; + } + } } o.Edges[0] = f0; o.Edges[1] = f1; o.Edges[2] = f2; - o.Inside = max(1.0, round((f0 + f1 + f2) / 3.0)); + o.Inside = (f0 < 0.0) ? -1.0 : max(1.0, round((f0 + f1 + f2) / 3.0)); return o; } [domain("tri")] [partitioning("integer")] -[outputtopology("triangle_cw")] +[outputtopology("triangle_ccw")] [outputcontrolpoints(3)] [patchconstantfunc("PatchConstant")] [maxtessfactor(4.0)] diff --git a/res/gamedata/shaders/r5/bloom/composite.ps b/res/gamedata/shaders/r5/bloom/composite.ps index 788b994c20fff3af133d67f5fd461b79db0f736c..3c6dea127539845b82ff7028ec8564af83d60467 100644 GIT binary patch delta 430 zcmZ8du};G<5EZCm$PD`pC~{B~IbowD2o#A0Him9hl(e(j3A%K>{%9Y1r5tfAz4GD-+#QE zfmpuj*3<%gdOYY4pOY$ClimKv-MTP#)&Z_8Rj)QiXuDuW(^p7}VYtj)N9BMzjL@Q!{`?;pigV ZD7Vn&QRl|m`N!v{h-%hc)kCz6^#dR}dBy+$ delta 221 zcmXYry{f`M5QR}L7W)*ZxS+vc6w6>|VXckWne1p5lbPUd5DUQvuu$|ZZgHPT1Yg3| z^=B9c<{Rdm^WV7rOrr=%E>c9P6s1_>s8d?p_m>YwD(u!KshkmpaK1&$ro;qe{jD^l zL%59C4^Mp0wI&^?WA8NMTK{zYJ0EIc5-a%fW)oRBte6N^kX1{U2mH(6n*DcTxl;RO&Ni-n^tvuXeTYwx<*S{?F~6#C>mcfPL#Ilf8VhUfuz#% z0L1tB+;gwb^@l{Hc_nCd7hOiaII$YCzLvl(G`+C;uTNDp!7srUbT55pa8~bnD6;hNOeJI{_DaZp-X32A*=lhyxr*S z0qs7YM^%Ls*9B0W`8=!Sr`U;tjlDLdmZJt(kRdIKb3K`DEKy$_h$bfMP#XQpnQeU&PO-^*e%YgmVd#b7afb~T1473& zP;=q^0$AVfIW*=K>%B$`*Sc%XWhT_vdd6JncwhO#L+zvH1(d)p*EIM{pTl8n03GZT z;Pn6NE<_7aW$KCb=ITtFdjd@{ z$4h_8u_|-?ot5jt(}_l2)YY9?CLH$w%ih6UFB`&}cVUc%(c^ls%+M3U)dB3Gzjla> zNTa#GS-PHUr()c&+L7cbu`Dt-P2U_t{%W4Xj%M zrheF8Y)cLbGy8SXT_GWc5N^cdvwjAF+sQQd0;IySl_|ptoRHpNhyU^dfs?XzlAZ!q W)hyj$nKPcV10H?A1lnxbsKp)czs2#@Lb`@5ta6YVU-XrYe;44M==&bsk{770L7ZB_p2%@VpmR zhDSJ7)&tC7`zqGf>!58Jck$d7)*-$~SLudzpb3PWq0bDmr?*9#ZkE?5@1Z; zI)&P)$r8NC3tR6!Lkp)4+6U#Lz=rWZ9Nyz_)2E{ zqqu@jpwTC6me6*?Bf$SsMP9Hmj);$8C8XJDRR;QTc_<2~JM3UI2S%sU*32nfX9 g_Lights : register(t0); @@ -17,22 +17,71 @@ RWStructuredBuffer g_LightIndexList : register(u1); RWByteAddressBuffer g_LightIndexCounter : register(u2); -[numthreads(1, 1, 1)] -void main(uint3 dtid : SV_DispatchThreadID) +groupshared uint gs_hits[256]; +groupshared uint gs_hitCount; + +bool LightHitsCluster( + GPULightData ld, + float sliceNear, + float sliceFar, + float2 tileMin, + float2 tileMax, + float tileSize, + float cotFovY, + float zNear) { - uint tileX = dtid.x; - uint tileY = dtid.y; - uint slice = dtid.z; + float3 lightPos = ld.positionAndInvRangeSq.xyz; + float range = ld.colorAndRange.w; + float4 clipPos = mul(m_VP, float4(lightPos, 1.0)); + float lightDepth = clipPos.w; + + float depthNear = lightDepth - range; + float depthFar = lightDepth + range; + + if (depthFar < sliceNear || depthNear > sliceFar) + return false; + + if (lightDepth + range <= 0) + return false; + + if (clipPos.w > 1e-4 && lightDepth > range) + { + float3 ndc = clipPos.xyz / clipPos.w; + float2 screenPos; + screenPos.x = (ndc.x * 0.5 + 0.5) * cb_screenSize.x; + screenPos.y = (0.5 - ndc.y * 0.5) * cb_screenSize.y; + + float zNearOverlap = max(sliceNear, max(depthNear, zNear)); + float screenRadius = (range / zNearOverlap) * cotFovY * cb_screenSize.y * 0.5; + screenRadius += tileSize; + + float2 closest = clamp(screenPos, tileMin, tileMax); + float2 diff = screenPos - closest; + if (dot(diff, diff) > screenRadius * screenRadius) + return false; + } + + return true; +} + +[numthreads(64, 1, 1)] +void main(uint3 groupId : SV_GroupID, uint gtid : SV_GroupIndex) +{ uint tilesX = (uint)cb_gridDims.x; uint tilesY = (uint)cb_gridDims.y; uint numSlices = (uint)cb_gridDims.z; float tileSize = cb_depthParams.w; - if (tileX >= tilesX || tileY >= tilesY || slice >= numSlices) + uint clusterIdx = groupId.x; + uint clustersTotal = tilesX * tilesY * numSlices; + if (clusterIdx >= clustersTotal) return; - uint clusterIdx = tileX + tileY * tilesX + slice * tilesX * tilesY; + uint slice = clusterIdx / (tilesX * tilesY); + uint rem = clusterIdx - slice * tilesX * tilesY; + uint tileY = rem / tilesX; + uint tileX = rem - tileY * tilesX; float zNear = cb_depthParams.x; float zFar = cb_depthParams.y; @@ -47,71 +96,68 @@ void main(uint3 dtid : SV_DispatchThreadID) min((tileY + 1) * tileSize, cb_screenSize.y)); uint numVisible = min(g_VisibleLightCount.Load(0), 1024u); + float cotFovY = max(cb_pad.x, 0.5); - uint lightCount = 0; - uint lightIndices[256]; + if (gtid == 0) + gs_hitCount = 0; + GroupMemoryBarrierWithGroupSync(); - for (uint iter = 0; iter < numVisible; iter++) + for (uint iter = gtid; iter < numVisible; iter += 64) { uint i = g_VisibleLightIndices[iter]; GPULightData ld = g_Lights[i]; - float3 lightPos = ld.positionAndInvRangeSq.xyz; - float range = ld.colorAndRange.w; - - float4 clipPos = mul(m_VP, float4(lightPos, 1.0)); - float lightDepth = clipPos.w; - - float depthNear = lightDepth - range; - float depthFar = lightDepth + range; - - if (depthFar < sliceNear || depthNear > sliceFar) + if (!LightHitsCluster(ld, sliceNear, sliceFar, tileMin, tileMax, tileSize, cotFovY, zNear)) continue; - if (lightDepth + range <= 0) - continue; - - if (clipPos.w > 0 && lightDepth > range) - { - float3 ndc = clipPos.xyz / clipPos.w; - float2 screenPos; - screenPos.x = (ndc.x * 0.5 + 0.5) * cb_screenSize.x; - screenPos.y = (0.5 - ndc.y * 0.5) * cb_screenSize.y; - - float zNearOverlap = max(sliceNear, max(depthNear, zNear)); - float screenRadius = (range / zNearOverlap) * cb_screenSize.y * 0.5; - - float2 closest = clamp(screenPos, tileMin, tileMax); - float2 diff = screenPos - closest; - if (dot(diff, diff) > screenRadius * screenRadius) - continue; - } - - if (lightCount < 256) - lightIndices[lightCount] = i; - lightCount++; + uint slot; + InterlockedAdd(gs_hitCount, 1, slot); + if (slot < 256) + gs_hits[slot] = i; } - lightCount = min(lightCount, 256); + GroupMemoryBarrierWithGroupSync(); - if (lightCount > 0) - { - uint globalOffset; - g_LightIndexCounter.InterlockedAdd(0, lightCount, globalOffset); + uint lightCount = min(gs_hitCount, 256u); - if (globalOffset + lightCount <= 1048576) + if (gtid == 0) + { + if (lightCount > 1) { - for (uint j = 0; j < lightCount; j++) - g_LightIndexList[globalOffset + j] = lightIndices[j]; + for (uint a = 1; a < lightCount; a++) + { + uint key = gs_hits[a]; + uint b = a; + while (b > 0 && gs_hits[b - 1] > key) + { + gs_hits[b] = gs_hits[b - 1]; + b--; + } + gs_hits[b] = key; + } + } - g_ClusterGrid[clusterIdx] = uint2(globalOffset, lightCount); + if (lightCount > 0) + { + uint globalOffset; + g_LightIndexCounter.InterlockedAdd(0, lightCount, globalOffset); + + if (globalOffset + lightCount <= 1048576) + { + for (uint j = 0; j < lightCount; j++) + g_LightIndexList[globalOffset + j] = gs_hits[j]; + + g_ClusterGrid[clusterIdx] = uint2(globalOffset, lightCount); + } + else + { + g_ClusterGrid[clusterIdx] = uint2(0, 0); + } } else { g_ClusterGrid[clusterIdx] = uint2(0, 0); } } - else - { - g_ClusterGrid[clusterIdx] = uint2(0, 0); - } } + +#define LOCAL_SHADOW_RECT_F4_V1 1 diff --git a/res/gamedata/shaders/r5/common_functions.h b/res/gamedata/shaders/r5/common_functions.h index c742157cf40..cf424d722c7 100644 --- a/res/gamedata/shaders/r5/common_functions.h +++ b/res/gamedata/shaders/r5/common_functions.h @@ -114,11 +114,10 @@ float3 v_hemi(float3 n) return L_hemi_color.rgb*(.5f + .5f*n.y); } -// Scalar hemi occlusion for skinned/dynamic meshes without baked vertex hemi. -// Same wrap as classic v_hemi; fed into output_forward_pbr ambient term. float calc_model_hemi(float3 norm_w) { - return saturate(0.5f + 0.5f * normalize(norm_w).y); + float ny = normalize(norm_w).y; + return saturate(0.52f + 0.48f * ny); } float3 v_sun(float3 n) @@ -229,6 +228,8 @@ float gbuf_unpack_mtl( float mtl_hemi ) #include "shared/sky_ibl.h" #endif #include "shared/foliage_sss.h" +#include "shared/glass_sss.h" +#include "shared/basecolor_pack.h" #include "shared/shadow_sampling.h" #include "shared/clustered_lighting.h" @@ -259,24 +260,83 @@ f_forward output_forward_pbr( float sunOcclusion = 1.0, float sssStrength = 0.0, float3 sssTint = float3(1, 1, 1), - float sssThickness = 1.0) + float sssThickness = 1.0, + float sssMask = 0.0, + float sssProfile = -1.0) { f_forward res; + if (parallax.w < -0.5) + { + float3 N = normalize(worldNormal); + float3 V = normalize(eye_position - worldPos); + float3 L = normalize(-L_sun_dir_w); + float hemiTerm = saturate(hemi); + float indoor = saturate(1.0 - hemiTerm * 1.35); + float hemiFill = max(hemiTerm, lerp(0.08, 0.28, indoor)); + float hemiDir = 0.4 + 0.6 * saturate(N.y * 0.5 + 0.5); + float3 ambientColor = L_ambient.rgb + L_hemi_color.rgb * max(L_hemi_color.w, 0.35) * hemiDir; + ambientColor = max(ambientColor, L_ambient.rgb + L_hemi_color.rgb * lerp(0.12, 0.4, indoor)); + float3 ambient = PBRAmbient(albedo, N, V, metallic, roughness, ao, ambientColor * hemiFill); + float3 finalColor = ambient; + float sssAmt = max(sssStrength, sssMask * max(parallax.z, 0.0)); + if (sssAmt > 1e-4) + { + finalColor += EvaluateFoliageSSS( + albedo, N, V, L, L_sun_color, 1.0, + sssTint, max(sssThickness, 0.65), sssAmt); + } + { + float dist = length(worldPos - eye_position); + float fog = saturate(dist * fog_params.w + fog_params.x); + finalColor = lerp(finalColor, fog_color.rgb, fog); + } + res.color = float4(finalColor, 1.0); + res.normal = float4(N, roughness); + res.baseColor = float4(albedo, PackBaseColorA(metallic, max(sssMask, saturate(sssAmt)))); + res.worldPos = float4(worldPos, 1.0); + return res; + } + float3 N = normalize(worldNormal); float3 V = normalize(eye_position - worldPos); float3 L = normalize(-L_sun_dir_w); + float albedoLum = dot(albedo, float3(0.2126, 0.7152, 0.0722)); + metallic *= saturate(albedoLum * 3.2 + 0.35); + albedo = SanitizeMetalAlbedo(albedo, metallic); float hemiTerm = saturate(hemi); float sunTerm = saturate(sunOcclusion); + float ndl = dot(N, L); + bool needSunShadow = (ndl > 0.0) || (sssStrength > 1e-4); + float csm = 1.0; + float contact = 1.0; + float2 maskUV = float2(0, 0); + if (svPosition.w != 0) + maskUV = float2(svPosition.x * screen_res.z, svPosition.y * screen_res.w); +#if defined(SHADOW_MASK_FORWARD) + bool useMask = ShadowMaskEnabled() && (svPosition.w != 0); + if (useMask) + { + float3 mask = SampleShadowMask(maskUV); + if (needSunShadow) + { + csm = lerp(0.20, 1.0, saturate(mask.r)); + contact = saturate(mask.g); + } + } + else +#endif + if (needSunShadow) + { #if defined(CSM_CHEAP_FORWARD) - float csm = SampleCSM_Fast(worldPos, N); + csm = SampleCSM_Fast(worldPos, N); #else - float csm = SampleCSM(worldPos, N); + csm = SampleCSM(worldPos, N); #endif - // Soft ambient fill in umbra — full-black CSM reads too heavy indoors / under trees. - csm = lerp(0.20, 1.0, saturate(csm)); - float contact = ContactShadow(worldPos, L); + csm = lerp(0.20, 1.0, saturate(csm)); + contact = ContactShadow(worldPos, L); + } float sunShadow = csm * contact * sunTerm; float3 sunLight = PBRDirectLighting( @@ -291,7 +351,7 @@ f_forward output_forward_pbr( if (iblIntensity > 1e-4) { ambient = PBRAmbientIBL( - albedo, N, V, + albedo, N, V, worldPos, metallic, roughness, ao, hemiTerm, iblIntensity); } @@ -310,26 +370,40 @@ f_forward output_forward_pbr( if (sssStrength > 1e-4) { - finalColor += EvaluateFoliageSSS( - albedo, N, V, L, L_sun_color, sunShadow, - sssTint, sssThickness, sssStrength); -#if defined(SKY_IBL) - if (parallax.w > 1e-4) + if (sssProfile > 3.5) { - finalColor += EvaluateFoliageSkySSS( - albedo, N, sssTint, sssThickness, sssStrength * parallax.w); + finalColor += EvaluateGlassTransmission( + albedo, N, V, L, L_sun_color, sunShadow, + sssThickness, sssStrength); } + else + { + finalColor += EvaluateFoliageSSS( + albedo, N, V, L, L_sun_color, sunShadow, + sssTint, sssThickness, sssStrength); +#if defined(SKY_IBL) + if (parallax.w > 1e-4) + { + finalColor += EvaluateFoliageSkySSS( + albedo, N, sssTint, sssThickness, sssStrength * parallax.w); + } #endif + } } #ifdef CLUSTERED_LIGHTING_FORWARD if (svPosition.w != 0) { - float linearDepth = mul(m_V, float4(worldPos, 1.0)).z; + float linearDepth = abs(mul(m_V, float4(worldPos, 1.0)).z); float3 clusterLights = EvaluateClusteredLights( worldPos, N, V, albedo, metallic, roughness, - svPosition.xy, linearDepth, (uint)pbr_diffuse_mode); + svPosition.xy, linearDepth, (uint)pbr_diffuse_mode, 1.0); finalColor += clusterLights; + + // r_cluster_debug (dev_param_2.w): 1=tileXY, 2=slice + int clusterDebug = (int)(dev_param_2.w + 0.5); + if (clusterDebug > 0) + finalColor = ClusterDebugColor(svPosition.xy, linearDepth, clusterDebug); } #endif @@ -346,7 +420,7 @@ f_forward output_forward_pbr( res.color = float4(finalColor, 1.0); res.normal = float4(N, roughness); - res.baseColor = float4(albedo, metallic); + res.baseColor = float4(albedo, PackBaseColorA(metallic, sssMask)); res.worldPos = float4(worldPos, 1.0); return res; } diff --git a/res/gamedata/shaders/r5/common_iostructs.h b/res/gamedata/shaders/r5/common_iostructs.h index 6433e6e0429..ea325e5dc20 100644 --- a/res/gamedata/shaders/r5/common_iostructs.h +++ b/res/gamedata/shaders/r5/common_iostructs.h @@ -385,6 +385,7 @@ struct v2p_flat float3 rotatedNormal2 : TEXCOORD8; nointerpolation uint objectId : TEXCOORD9; float bladeHash : TEXCOORD10; + float sunOcclusion : TEXCOORD11; // DetailSlot.c_dir (classic c0.x) float4 hpos : SV_Position; }; @@ -409,6 +410,7 @@ struct p_flat float3 rotatedNormal2 : TEXCOORD8; nointerpolation uint objectId : TEXCOORD9; float bladeHash : TEXCOORD10; + float sunOcclusion : TEXCOORD11; float4 hpos : SV_Position; }; @@ -423,6 +425,7 @@ struct v2p_decal #endif float4 position : TEXCOORD1; float3 N : TEXCOORD2; + float sunOcclusion : TEXCOORD3; float4 hpos : SV_Position; }; @@ -435,6 +438,7 @@ struct p_decal #endif float4 position : TEXCOORD1; float3 N : TEXCOORD2; + float sunOcclusion : TEXCOORD3; float4 hpos : SV_Position; }; @@ -451,6 +455,7 @@ struct v2p_billboard float3 N : TEXCOORD2; float heightParam : TEXCOORD3; float bladeHash : TEXCOORD4; + float sunOcclusion : TEXCOORD5; float4 hpos : SV_Position; }; @@ -465,6 +470,7 @@ struct p_billboard float3 N : TEXCOORD2; float heightParam : TEXCOORD3; float bladeHash : TEXCOORD4; + float sunOcclusion : TEXCOORD5; float4 hpos : SV_Position; }; diff --git a/res/gamedata/shaders/r5/cull_utils.h b/res/gamedata/shaders/r5/cull_utils.h index 782b545b484..2bab437eb55 100644 --- a/res/gamedata/shaders/r5/cull_utils.h +++ b/res/gamedata/shaders/r5/cull_utils.h @@ -137,7 +137,7 @@ bool HiZTestSphereTemporal( // Calculate screen-space bounding box (in previous frame's space) float projScale = max(abs(prevViewProj[0][0]), abs(prevViewProj[1][1])); - float2 ndcSize = float2(radius, radius) * projScale / prevClipPos.w; + float2 ndcSize = float2(radius, radius) * projScale / prevClipPos.w * 1.5; float2 minNDC = prevNdc.xy - ndcSize; float2 maxNDC = prevNdc.xy + ndcSize; @@ -168,9 +168,8 @@ bool HiZTestSphereTemporal( if (screenArea > fullArea * 0.18) return true; - // Select mip where box is roughly 2x2 pixels, then bias one level coarser - // so MAX Hi-Z is more conservative (fewer false occlusions). - float mipLevel = floor(log2(max(1.0, max(boxWidth, boxHeight) * 0.5))) - 1.0; + // Coarser mip = larger MAX footprint → fewer false occlusions. + float mipLevel = ceil(log2(max(1.0, max(boxWidth, boxHeight)))); mipLevel = clamp(mipLevel, 0.0, float(hiZMipLevels - 1)); // Sample Hi-Z at 4 corners (previous frame's depth) diff --git a/res/gamedata/shaders/r5/decal_box.ps b/res/gamedata/shaders/r5/decal_box.ps index 75b8f04e581a75b8d956053629dcfa2769338569..9b4039d82f2362e3fd3f4343bf37b0459eee1280 100644 GIT binary patch delta 912 zcmah`O^ee&7^WZg5D^70y$J7iMJH>B?Y4qg19sc)T5P+eo4R-^(`J$erSA za6ZNSwLU2*%C<#p*CWtq)@$wIe!F%sJThx$s}2Cga3lPp*>0MLT_|mYJA<~_>a}0N zZm1}Jz{7EHaqxLC9)-3|I5fx@(+0=$`W`$1P8`<{2uGues+=i86GGPuz!@H6FX)6c zaA)*Qbq$nZXsrl(*!uG0eTs}2qcB~rf&nPr&~1txAMRZZwz8(emLb9=w5|eds|$sSdH_Z#d9683{uAcO89JfXH?W5N#Ds(wd8zj hQ{n(iCz6B{;lWGJRY8JF)UdXC9_{At=H&Xh@*6OU6jA^H delta 120 zcmdlX+{8PCAbLhec!2)QQ52~Dy#2nS zJ7f&YRravG!rUWIt}O|&yy!}RUlH)NQ%2ASu*S>K8E^;rk0M*=%k6T#nq44MVC_m{ zT*Z*>;_=IRy_rvD(d+D5h2$c(51T~}aqvmk{#>Ic?$+RzYwOKuiw9!sP3J&`P)}GH zR?6XCvZGy0lMyTRPU=#%6~awvn#SnDi^eJ*J4tJC=C!3EF!3`5e1hdbjF9lm)?gplvkjnGGcj#-QDv1B{Pl#ji@c9!= wl8>ik!>_96>*ves`IyzeT6J56_sV$KLA+gFoFqaGq9xM^lBH1U7`;Y+0kLStng9R* diff --git a/res/gamedata/shaders/r5/depth/depth_prepass.vs b/res/gamedata/shaders/r5/depth/depth_prepass.vs deleted file mode 100644 index 3f4693da435..00000000000 --- a/res/gamedata/shaders/r5/depth/depth_prepass.vs +++ /dev/null @@ -1,41 +0,0 @@ -// xrRender/Shaders/depth/depth_prepass.vs.hlsl -// Phase 2.2: Depth Prepass Vertex Shader (stub for Phase 2) -// -// This shader will be implemented in Phase 2 for early-Z optimization -// For now, it's a placeholder to establish the directory structure - -#include "../shared/common.h" - -// ══════════════════════════════════════════════════════════ -// INPUT -// ══════════════════════════════════════════════════════════ - -struct VS_INPUT { - float3 position : POSITION; - float2 texcoord : TEXCOORD0; // For alpha testing in pixel shader -}; - -// ══════════════════════════════════════════════════════════ -// OUTPUT -// ══════════════════════════════════════════════════════════ - -struct VS_OUTPUT { - float4 position : SV_POSITION; - float2 texcoord : TEXCOORD0; -}; - -// ══════════════════════════════════════════════════════════ -// VERTEX SHADER (Minimal - depth only) -// ══════════════════════════════════════════════════════════ - -VS_OUTPUT main(VS_INPUT input) { - VS_OUTPUT output; - - // Transform to clip space - output.position = mul(float4(input.position, 1.0), m_WVP); - - // Pass through UVs for alpha testing - output.texcoord = input.texcoord; - - return output; -} diff --git a/res/gamedata/shaders/r5/detail_billboard.ps b/res/gamedata/shaders/r5/detail_billboard.ps index 798ed7f8afd46bf5e29097f6c0953687784cdcf6..b694eb36df78115aa194d424328dae2d5605f509 100644 GIT binary patch delta 135 zcmbQlcaU$wREd<-w9LFzg<$WTXj*e|rsd=( zmM9b#7yBj_XDiq$6epIH7A2OXY5;{Cb4wJo6b$vuCReegNx)@1^@>aL{F9S&N{ch| W^E4+vX44YZff=C#R1Y$^mJ0yKO)Iki delta 88 zcmX@eH;He;)Q#&cSxxly6&!O4G7=RMixo;TGLy6OQj3eN6-qJ-!2G)bRZ` visible_indices : register(t33); -StructuredBuffer detail_models : register(t35); -StructuredBuffer pulled_vertices : register(t36); +StructuredBuffer visible_indices : register(t39); +StructuredBuffer detail_models : register(t60); +StructuredBuffer pulled_vertices : register(t61); StructuredBuffer all_instances : register(t37); StructuredBuffer slot_data : register(t38); @@ -86,6 +90,7 @@ v2p_billboard main(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceI InstanceData raw = all_instances[src_idx]; uint object_id = raw.packed & 0x3F; + uint vis_id = (raw.packed >> 6) & 0x3; float rotation = float((raw.packed >> 8) & 0x3FF) / 1023.0 * TWO_PI; float scale = float((raw.packed >> 18) & 0x3FF) / 1023.0 * PACK_MAX_SCALE; @@ -113,10 +118,17 @@ v2p_billboard main(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceI float4 world_pos = float4(rotated + raw.pos, 1.0); float wind_speed = max(g_wind_direction.y, 0.1); - float time = wave.w; + float wind_phase = (vis_id == 2) ? 1.73 : 0.0; + float wind_mul = (vis_id == 2) ? 1.15 : 1.0; + float time = wave.w + wind_phase; float wind_angle_rad = g_wind_direction.x * (M_PI / 180.0); float2 global_wind_dir = float2(sin(wind_angle_rad), cos(wind_angle_rad)); + if (vis_id == 2) + { + float2 alt = normalize(dir2D_2.xy + float2(1e-4, 0)); + global_wind_dir = normalize(lerp(global_wind_dir, alt, 0.35)); + } float2 dir_uv = world_pos.zx * (0.005 / wind_speed) + time * (0.005 * wind_speed); float wind_dir_noise = g_Perlin4D.SampleLevel(smp_linear, float3(dir_uv, 0), 0).r; @@ -126,7 +138,7 @@ v2p_billboard main(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceI float fbm_wind_strength = lerp(0.25, 1.0, wind_str_noise); fbm_wind_strength *= fbm_wind_strength; - fbm_wind_strength *= wind_speed; + fbm_wind_strength *= wind_speed * wind_mul; float fbm_turbulence = (wind_dir_noise * 2.0 - 1.0) * 0.3; float2 perpendicular_dir = float2(-global_wind_dir.y, global_wind_dir.x); @@ -150,7 +162,8 @@ v2p_billboard main(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceI float slot_hemi = slot_data[slot_idx].hemi; float hemi = abs(slot_hemi); - float sun = sign(slot_hemi) * 0.25 + 0.25; + // Classic gl/deffer_detail: c0.x = sun (c_dir), c0.w = hemi + float sun = saturate(slot_data[slot_idx].sun); uint triBase = (vertex_id / 3) * 3; PulledVertex v0 = pulled_vertices[mdl.pulledVertexBase + triBase]; @@ -179,6 +192,7 @@ v2p_billboard main(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceI uint bh = asuint(bc.x * 73856093 + bc.y * 19349663); bh ^= bh >> 16; O.bladeHash = float(bh & 0xFFFFu) / 65535.0; + O.sunOcclusion = sun; O.hpos = mul(g_detail_VP, world_pos); return O; } diff --git a/res/gamedata/shaders/r5/detail_cull.cs b/res/gamedata/shaders/r5/detail_cull.cs index 96da14e32b0..82a4c68c49d 100644 --- a/res/gamedata/shaders/r5/detail_cull.cs +++ b/res/gamedata/shaders/r5/detail_cull.cs @@ -147,12 +147,13 @@ void main(uint3 group_id : SV_GroupID, uint3 thread_id : SV_GroupThreadID) if (!FrustumTestSphere(bounds_center, bounds_radius, g_frustum_planes)) continue; - if (!HiZTestSphereTemporal(bounds_center, bounds_radius, g_camera_pos, g_view_proj, g_prev_view_proj, + uint flags = asuint(mdl.flags); + bool is_static = (flags & DO_NO_WAVING) != 0; + if (!is_static && + !HiZTestSphereTemporal(bounds_center, bounds_radius, g_camera_pos, g_view_proj, g_prev_view_proj, g_hiz_pyramid, smp_nofilter, g_hiz_width, g_hiz_height, g_hiz_mip_levels)) continue; - uint flags = asuint(mdl.flags); - bool is_static = (flags & DO_NO_WAVING) != 0; if (is_static) AppendDecal(inst_idx); else if (g_grass_mode == 0) diff --git a/res/gamedata/shaders/r5/detail_decal.ps b/res/gamedata/shaders/r5/detail_decal.ps index d300161c86b1c24a86554b6ea7d56f75e21606c6..c31602c4f4b02857ecbe56703414a01deac7ee55 100644 GIT binary patch delta 334 zcmZvWu}Z{16h)C`i$uiM@|Fo^Cy82!OA1{qTnj;4ahW7S&9-!qekv7zlZ( zxCQrk6foQl+8ssz)sbSw*HKQB;{-X&{-QoMqt~5%u@Fi>b6rD|L9q?T4+p=1IQgRz|j>)RNMoyvavd visible_indices : register(t33); -StructuredBuffer detail_models : register(t35); -StructuredBuffer decal_vertices : register(t36); +StructuredBuffer visible_indices : register(t39); +StructuredBuffer detail_models : register(t60); +StructuredBuffer decal_vertices : register(t61); StructuredBuffer all_instances : register(t37); StructuredBuffer slot_data : register(t38); @@ -106,7 +110,9 @@ v2p_decal main(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID) rotated.y = local_pos.y; rotated.z = local_pos.x * s + local_pos.z * c; - float4 world_pos = float4(rotated + raw.pos, 1.0); + float3 world_pos = rotated + raw.pos; + world_pos.y += 0.02; + float3 N = float3(0, 1, 0); float2 uv = float2(v.u, v.v); @@ -122,7 +128,7 @@ v2p_decal main(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID) float slot_hemi = slot_data[slot_idx].hemi; float hemi = abs(slot_hemi); - float sun = sign(slot_hemi) * 0.25 + 0.25; + float sun = saturate(slot_data[slot_idx].sun); #if defined(USE_R2_STATIC_SUN) && !defined(USE_LM_HEMI) O.tcdh = float4(uv, hemi, sun); @@ -130,9 +136,10 @@ v2p_decal main(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID) O.tcdh = uv; #endif - O.position = float4(world_pos.xyz, hemi); - O.N = float3(0, 1, 0); - O.hpos = mul(g_detail_VP, world_pos); + O.position = float4(world_pos, hemi); + O.N = N; + O.sunOcclusion = sun; + O.hpos = mul(g_detail_VP, float4(world_pos, 1.f)); return O; } diff --git a/res/gamedata/shaders/r5/detail_gpu.ps b/res/gamedata/shaders/r5/detail_gpu.ps index 4a1367092ca0a0f150498896e8b09f29f56dd3ed..ef4a80f9bca68b10ac77690c0f63a8b17066affe 100644 GIT binary patch delta 195 zcmZ2#{lR*|REd<-w9LFzg<$W3jj9)K*In4 delta 122 zcmexhz0`Wb)Q#(ZO55q{D})6r6eK2R7b_I!NSWacRpPF^gdGFe%+NX}5tK!-~K3et-bi;LrnfjBuo TC%;IqeDYpd1y)UKu39bt&KfD& diff --git a/res/gamedata/shaders/r5/detail_gpu.vs b/res/gamedata/shaders/r5/detail_gpu.vs index d7dae88fbb2..11a98695ac8 100644 --- a/res/gamedata/shaders/r5/detail_gpu.vs +++ b/res/gamedata/shaders/r5/detail_gpu.vs @@ -57,7 +57,7 @@ static const float HEIGHT_NOISE_SCALE = 0.05; // World-space frequency of hei // Phase 6: Virtual texturing indirection table // NOTE: common_samplers.h uses t0-t31, so we use t32+ to avoid conflicts -Buffer slot_indirection : register(t32); // Packed: physical_page (16) | mip (8) | flags (8) +Buffer slot_indirection : register(t40); // Phase 6: New vertex structure for SDF blade geometry // NOTE: Semantic names must match C++ VertexAttributeDesc in FGDetailManager.cpp @@ -86,6 +86,10 @@ struct GPUSlotData uint packed_palette_01; uint packed_palette_23; float hemi; + float sun; + float _pad0; + float _pad1; + float _pad2; }; static const float PACK_MAX_SCALE = 4.0; @@ -95,7 +99,7 @@ static const float TWO_PI = 6.28318530718; // Perlin4D 3D volume — bound directly at t12 (not bindless, since bindless is Texture2D only) Texture3D g_Perlin4D : register(t12); -StructuredBuffer visible_indices : register(t33); +StructuredBuffer visible_indices : register(t39); StructuredBuffer all_instances : register(t37); StructuredBuffer slot_data : register(t38); @@ -216,7 +220,9 @@ v2p_flat main(v_blade_sdf I, uint instance_id : SV_InstanceID) // Sample Perlin noise for direction, strength, and turbulence separately // Key: Scale inversely with wind_speed, time scroll proportionally float wind_speed = max(g_wind_direction.y, 0.1); // Avoid division by zero - float time = wave.w; // Global time + float wind_phase = (det.vis_id == 2) ? 1.73 : 0.0; + float wind_mul = (det.vis_id == 2) ? 1.15 : 1.0; + float time = wave.w + wind_phase; // Wind DIRECTION sample - larger scale, slower movement // Reference: pos.zx * 0.005/wind_speed + TIME * 0.005 * wind_speed @@ -238,7 +244,7 @@ v2p_flat main(v_blade_sdf I, uint instance_id : SV_InstanceID) // Process strength: remap to [0.25, 1.0], square for contrast, scale by wind_speed float fbm_wind_strength = lerp(0.25, 1.0, wind_str_noise); fbm_wind_strength *= fbm_wind_strength; // Square for more contrast - fbm_wind_strength *= wind_speed; + fbm_wind_strength *= wind_speed * wind_mul; // Process turbulence: same processing as strength float wind_turbulence = lerp(0.25, 1.0, wind_turb_noise); @@ -251,6 +257,11 @@ v2p_flat main(v_blade_sdf I, uint instance_id : SV_InstanceID) // Calculate global wind direction from g_wind_direction float wind_angle_rad = g_wind_direction.x * (M_PI / 180.0); float2 global_wind_dir = float2(sin(wind_angle_rad), cos(wind_angle_rad)); + if (det.vis_id == 2) + { + float2 alt = normalize(dir2D_2.xy + float2(1e-4, 0)); + global_wind_dir = normalize(lerp(global_wind_dir, alt, 0.35)); + } // Add turbulence perpendicular to wind direction float2 perpendicular_dir = float2(-global_wind_dir.y, global_wind_dir.x); @@ -421,7 +432,7 @@ v2p_flat main(v_blade_sdf I, uint instance_id : SV_InstanceID) // ===== Calculate blade normal ===== float slot_hemi = slot_data[slot_idx].hemi; float hemi = abs(slot_hemi); - float sun = sign(slot_hemi) * 0.25f + 0.25f; + float sun = saturate(slot_data[slot_idx].sun); // NORMAL = perpendicular to blade surface = cross(tangent, right) // Since right is derived from rotation matrix, this is stable @@ -459,6 +470,7 @@ v2p_flat main(v_blade_sdf I, uint instance_id : SV_InstanceID) uint bh = asuint(bc.x * 73856093 + bc.y * 19349663); bh ^= bh >> 16; O.bladeHash = float(bh & 0xFFFFu) / 65535.0; + O.sunOcclusion = sun; O.hpos = mul(g_detail_VP, pos); return O; } diff --git a/res/gamedata/shaders/r5/detail_instance_gen.cs b/res/gamedata/shaders/r5/detail_instance_gen.cs index 05db658e433..5ffef20ee00 100644 --- a/res/gamedata/shaders/r5/detail_instance_gen.cs +++ b/res/gamedata/shaders/r5/detail_instance_gen.cs @@ -10,6 +10,10 @@ struct GPUSlotData uint packed_palette_01; uint packed_palette_23; float hemi; + float sun; + float _pad0; + float _pad1; + float _pad2; }; struct InstanceData @@ -287,8 +291,9 @@ void main(uint3 group_id : SV_GroupID, uint3 thread_id : SV_GroupThreadID) else { DetailModelGPU mdl = g_detail_models[object_id]; - float scale = mdl.maxScale * g_detail_height_multiplier; - float rotation = 0.0f; + float scale = lerp(mdl.minScale, mdl.maxScale, pcg_randF(r_scale, 0.0, 1.0)) + * g_detail_height_multiplier; + float rotation = pcg_randF(r_yaw, 0.0, TWO_PI); uint flags = asuint(mdl.flags); const uint DO_NO_WAVING = 0x0001; diff --git a/res/gamedata/shaders/r5/distortion_apply.ps b/res/gamedata/shaders/r5/distortion_apply.ps index e0dc00ce59ee274084768eff10a42bcd6d7b5ca6..a3c761d7a024f3c2bc31ab18f807f7750c34805f 100644 GIT binary patch literal 1671 zcma)6!ET#C5WVvi)8qn;LotndsGS~~RH~&Wwu;+Ssw%WKi;cRlXm@dN8~=M}m&HJv zMCJezym|BH&09_rnq~!s=q7#>2hM3$tXP#$I9c&L=Y_vH>AjO1B53kTRw5-U8s|j( zk{aPGX{{uMzD?C90KzMHJ6&7hjgn9|!Pi`asELW)($WQhEQ!s4ZA6&%(ET8SB2n= z{b;yRshbrFc5n&j;RK)4OTg#Z+0HYFMy8fJC^XvJ;@TG1hvK>^)~Jra)M#HrD6yj2 z!kJa*)+5+?@KO&+Su?gq>&uJ-ITm!g@FS9!jDDh9%3PV3alzA!;kHL038sN4>XzOJ z0?)r6vSlf_#;OQ|bb}#sbFb6dbOa0!k`#a&qh0yl_|e=gASFy9V(&P1k2%Q-w_P!0 zMOi5i7lu1s60xQ#7_MEmdDX+U;kCn=xfq@gf>RkLzl{LK>EO%-Bu$oj!uwpE zM^Cebz|&Mxg){?NzuxtFg5rv>JDROG|0B_GqfR^B$4w#dgCH2g!viE-xlN;yZRo)i z#`Js~bjz?Fw;owq8wcFnv56Q?+YiMfVL)C_D||7NcbFp4#YBHON}N4!C{L~gr9~{L z^ncqN9$K2C`mZz-|6{x0f=Y(}!bEp!Q$J+H1J>BwPYfhfDjoe}4Lme@-n*9easLiq apYc$KdT3Cu^>Nfgc5WV|XjM|G`f^ppz0ZDsl5UNxf3x@`Au~zmZS;_WVyK9mZ<-arQZ>xYR zPVCL=H*em&dG=|7Yn~&7pQE>tXPxpqPRj)0Bo1JCt5@7$^08iYjJUq0C0i?sG)L5Whv_{eeh z{h8n=cr%pQipsSQ1>a+OTP@%kY))5BWwG0?ZanQJ#HEy&>s3&B+vdW(7c4R57k1A$ z0ncd+H3oE*?hbCqfGY>;u$cL)WnaJ#(m&^txMP-~WKqH;#wJ3f4C^qzx2)^iZ)BLLVfEiLWr*4{>e~jZ`2y-kldN}F d<7k=ct{=r>oQB@mV-?>fPA|%1$4X`&tzR~000RI3 diff --git a/res/gamedata/shaders/r5/dlss_rr_guides.cs b/res/gamedata/shaders/r5/dlss_rr_guides.cs new file mode 100644 index 00000000000..c848d37bacd --- /dev/null +++ b/res/gamedata/shaders/r5/dlss_rr_guides.cs @@ -0,0 +1,71 @@ +#include "common.h" +#include "shared/pbr_brdf.h" +#include "shared/basecolor_pack.h" + +cbuffer DlssRrGuideParams : register(b5) { + float4 g_CameraPos; + float2 g_ScreenSize; + float2 g_Pad; +}; + +Texture2D g_BaseColor : register(t0); +Texture2D g_Normal : register(t1); +Texture2D g_WorldPos : register(t2); +Texture2D g_NoisySpecular : register(t3); +Texture2D g_Depth : register(t4); + +RWTexture2D u_DiffuseAlbedo : register(u0); +RWTexture2D u_SpecularAlbedo : register(u1); +RWTexture2D u_SpecularHitDist : register(u2); + +float3 EnvBRDFApprox2(float3 specularColor, float alpha, float NoV) +{ + NoV = abs(NoV); + float4 X = float4(1.0, NoV, NoV * NoV, NoV * NoV * NoV); + float4 Y = float4(1.0, alpha, alpha * alpha, alpha * alpha * alpha); + float2x2 M1 = float2x2(0.99044, -1.28514, 1.29678, -0.755907); + float3x3 M2 = float3x3(1.0, 2.92338, 59.4188, 20.3225, -27.0302, 222.592, 121.563, 626.13, 316.627); + float2x2 M3 = float2x2(0.0365463, 3.32707, 9.0632, -9.04756); + float3x3 M4 = float3x3(1.0, 3.59685, -1.36772, 9.04401, -16.3174, 9.22949, 5.56589, 19.7886, -20.2123); + float bias = dot(mul(M1, X.xy), Y.xy) * rcp(dot(mul(M2, X.xyw), Y.xyw)); + float scale = dot(mul(M3, X.xy), Y.xy) * rcp(dot(mul(M4, X.xzw), Y.xyw)); + bias *= saturate(specularColor.g * 50.0); + return mad(specularColor, max(0.0, scale), max(0.0, bias)); +} + +[numthreads(8, 8, 1)] +void main(uint3 dtid : SV_DispatchThreadID) +{ + uint2 pixel = dtid.xy; + if (pixel.x >= (uint)g_ScreenSize.x || pixel.y >= (uint)g_ScreenSize.y) + return; + + float depth = g_Depth.Load(int3(pixel, 0)); + if (depth >= 1.0) + { + u_DiffuseAlbedo[pixel] = float4(1.0, 1.0, 1.0, 1.0); + u_SpecularAlbedo[pixel] = 0; + u_SpecularHitDist[pixel] = 0; + return; + } + + float4 base = g_BaseColor.Load(int3(pixel, 0)); + float4 nrm = g_Normal.Load(int3(pixel, 0)); + float3 worldPos = g_WorldPos.Load(int3(pixel, 0)).xyz; + float3 N = normalize(nrm.xyz); + float roughness = saturate(nrm.w); + float metallic = UnpackMetallicFromBaseA(base.a); + float3 albedo = max(base.rgb, 0.0); + float3 F0 = CalculateF0(albedo, metallic); + float3 V = normalize(g_CameraPos.xyz - worldPos); + float NoV = saturate(dot(N, V)); + float alpha = roughness * roughness; + + float3 diffAlb = albedo * (1.0 - metallic); + float3 specAlb = EnvBRDFApprox2(F0, alpha, NoV); + float specHit = max(g_NoisySpecular.Load(int3(pixel, 0)).a, 0.0); + + u_DiffuseAlbedo[pixel] = float4(diffAlb, 1.0); + u_SpecularAlbedo[pixel] = float4(specAlb, 1.0); + u_SpecularHitDist[pixel] = specHit; +} diff --git a/res/gamedata/shaders/r5/effects_world_soft.ps b/res/gamedata/shaders/r5/effects_world_soft.ps index 171d1dd337085ba38bce11327305b81b9eb2cdbe..a006cf480be380680b8f008570081249ce5ae842 100644 GIT binary patch delta 105 zcmcc4`G;ddw32dWUUE)pN~(fVaYkZMYKnewX;E5Ya%y~TVo`RnUWO9a#;CW9le?J$ zwLObHN>hS?(!M|`jpF3gywvajy>d-WE(It^$t+GzEK0HFntYjQ^W;m+Hj@olmIDCc CeI(ET delta 184 zcmY+8Ar8Vo5Jf|4;0U;pV5rBCKsW&j1~qJ_?S}14+1-XKO0D>YA zxSzb2m;dhGd(G;4cy=a90t?AJh*G9@T6heFP6;xLc}eUYtf|nPf>gMz37jG($Vp1_ zc0URfEyF81&)UxPU#xA0ir{fP!A2VcgPryTxDwk g_histogram : register(t0); RWTexture2D g_exposure : register(u0); -// Recover approximate linear average luminance from 64 log2 bins. -// Classic CoP used a linear downsample of the bloom/high buffer; this is the -// FG stand-in (same MiddleGray formula on top). -float ComputeAverageLuminance() +float LuminanceFromBin(uint bin) { - uint totalPixels = 0; - for (uint i = 0; i < 64; i++) - totalPixels += g_histogram[i]; - - if (totalPixels == 0) - return 1.0; - - // Match histogram binning in luminance_histogram.cs: log2 in [-10, +4] const float minLog = -10.0; const float logRange = 14.0; + float t = (float(bin) + 0.5) / 64.0; + return exp2(minLog + t * logRange); +} - float weightedSum = 0.0; +float PercentileLuminance(uint totalPixels, float percentile) +{ + float target = saturate(percentile) * float(totalPixels); + uint accum = 0; for (uint bin = 0; bin < 64; bin++) { - uint c = g_histogram[bin]; - if (c == 0) - continue; - float t = (float(bin) + 0.5) / 64.0; - float lum = exp2(minLog + t * logRange); - weightedSum += lum * float(c); + accum += g_histogram[bin]; + if (float(accum) >= target) + return LuminanceFromBin(bin); } - return weightedSum / float(totalPixels); + return LuminanceFromBin(63); } [numthreads(1, 1, 1)] void main(uint3 dispatch_id : SV_DispatchThreadID) { - float Lw = ComputeAverageLuminance(); + uint totalPixels = 0; + for (uint i = 0; i < 64; i++) + totalPixels += g_histogram[i]; + + float Lw = 1.0; + if (totalPixels > 0) + { + float L50 = PercentileLuminance(totalPixels, g_low_percentile); + float L90 = PercentileLuminance(totalPixels, g_high_percentile); + Lw = max(lerp(L50, L90, 0.35), 1e-6); + } + float scale = g_middle_gray_x / max(Lw * g_middle_gray_y + g_middle_gray_z, 1e-6); + scale *= exp2(g_ev_bias); + scale = clamp(scale, 1.0 / 128.0, 20.0); float scalePrev = g_exposure[uint2(0, 0)]; - // First frame / seed: treat 0 as "no history" if (scalePrev <= 1e-6) scalePrev = 1.0; - float rvalue = lerp(scalePrev, scale, saturate(g_middle_gray_w)); + float rate = (scale > scalePrev) ? g_adapt_up : g_adapt_down; + float alpha = saturate(1.0 - exp(-max(rate, 0.01) * max(g_delta_time, 1e-4))); + float rvalue = lerp(scalePrev, scale, alpha); rvalue = clamp(rvalue, 1.0 / 128.0, 20.0); g_exposure[uint2(0, 0)] = rvalue; diff --git a/res/gamedata/shaders/r5/gbuffer.ps b/res/gamedata/shaders/r5/gbuffer.ps deleted file mode 100644 index c8d052cea2f33d007804bb695e77ae1eb31ec2f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5188 zcmdT|O>g5i5WV|X47L|Lb>xqvml#_>j)klMTQVf2SRg12My6!}5@}E}>fK`RJr)J} z2ltnBM2a$%E!w>@Fye@M9BMRg-pph&fs2SJ+fy-#j|9J}N%nd^9H?+km2)hrV(>%G z(&D_Q@UA)%LHCoCmnCP;@ttu?t?epOo(eS5+#F0M@b&LME&4Q141TbEPAq)(K~;-$ zTEjNpNf+Sv0pQP};H18X6R+|*=L{Yo{$;lfV?Xi3U~z@q!Eam?`z@~!l6W5KNn2N{N`E!dBGF&gNBJvreRm@J zea&w>v=&d3C;I6#LMk&C?r7bTd$=R^$V{&#n*P07g5I@(rj@>|8YOLrp^}|B! zZnkT$X|$AdTjhjVc9m$yvhRoYor*hZWmuJ`U6C_N#2D)>2F%eNBFc8-wCqps+UjnY zQA+ULJFP9&vJb2X=eyp+DS3ew^NH8Z~NE6NSbohjVH z%xPv5Jnv;Xp8X29Kf}xI4F6KmSC%e!kSqgakG>qrV;ZqiqLhE8L#1VMGHHj|(fGe% zdYt_~oN{0!jiE-GF3RKHIM_T)c`k-M_s5L~BfT5l^;pXBKRD|7;pOWee>Gl9|K*Fo zHxJEJL^3K5Y*R_$+cY#!zk#Xq6S59}_j0e@@GrTS$=tg(WvU(5J({K2w{a6M@-hGX qm3(dZrtK&hW{|Z=YR2CWQ6t-N*=cg5to&K(}9C2LAvb29v%3 diff --git a/res/gamedata/shaders/r5/gbuffer.vs b/res/gamedata/shaders/r5/gbuffer.vs deleted file mode 100644 index 436ac789de6..00000000000 --- a/res/gamedata/shaders/r5/gbuffer.vs +++ /dev/null @@ -1,66 +0,0 @@ -// xrRender/Shaders/gbuffer.vs.hlsl -#include "shared/common.h" - -// ══════════════════════════════════════════════════════════ -// INPUT -// ══════════════════════════════════════════════════════════ - -struct VS_INPUT { - float3 position : POSITION; - float3 normal : NORMAL; - float2 texcoord : TEXCOORD0; - float3 tangent : TANGENT; - float3 binormal : BINORMAL; -}; - -// ══════════════════════════════════════════════════════════ -// OUTPUT -// ══════════════════════════════════════════════════════════ - -struct VS_OUTPUT { - float4 position : SV_POSITION; - float3 worldPos : TEXCOORD0; - float3 worldNormal : TEXCOORD1; - float2 texcoord : TEXCOORD2; - float3 worldTangent : TEXCOORD3; - float3 worldBinormal : TEXCOORD4; -}; - -// ══════════════════════════════════════════════════════════ -// VERTEX SHADER -// ══════════════════════════════════════════════════════════ - -VS_OUTPUT main(VS_INPUT input) { - VS_OUTPUT output; - - // Transform position to clip space using X-Ray's m_WVP - output.position = mul(float4(input.position, 1.0), m_WVP); - - // Transform position to world space using X-Ray's m_W - float4 worldPos4 = float4(input.position, 1.0); - output.worldPos.x = dot(worldPos4, m_W[0]); - output.worldPos.y = dot(worldPos4, m_W[1]); - output.worldPos.z = dot(worldPos4, m_W[2]); - - // Transform normal to world space (3x4 matrix) - output.worldNormal.x = dot(input.normal, m_W[0].xyz); - output.worldNormal.y = dot(input.normal, m_W[1].xyz); - output.worldNormal.z = dot(input.normal, m_W[2].xyz); - output.worldNormal = normalize(output.worldNormal); - - // Transform tangent space to world space - output.worldTangent.x = dot(input.tangent, m_W[0].xyz); - output.worldTangent.y = dot(input.tangent, m_W[1].xyz); - output.worldTangent.z = dot(input.tangent, m_W[2].xyz); - output.worldTangent = normalize(output.worldTangent); - - output.worldBinormal.x = dot(input.binormal, m_W[0].xyz); - output.worldBinormal.y = dot(input.binormal, m_W[1].xyz); - output.worldBinormal.z = dot(input.binormal, m_W[2].xyz); - output.worldBinormal = normalize(output.worldBinormal); - - // Pass through texture coordinates - output.texcoord = input.texcoord; - - return output; -} diff --git a/res/gamedata/shaders/r5/hud/default.ps b/res/gamedata/shaders/r5/hud/default.ps index d05fdb9c3164353010760c348043477bb8406032..ccc8ccecc8bb3acd8399f45c153e39d39224c3c9 100644 GIT binary patch delta 105 zcmcb~xQJ;&A0HQIT26jqiHSl u_BRDFLUT : register(u0); + +cbuffer BRDFLUTParams : register(b5) +{ + uint2 cb_Dims; + uint2 cb_Pad; +}; + +static const float PI = 3.14159265359; + +float RadicalInverse_VdC(uint bits) +{ + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; +} + +float2 Hammersley(uint i, uint N) +{ + return float2(float(i) / float(N), RadicalInverse_VdC(i)); +} + +float3 ImportanceSampleGGX(float2 Xi, float3 N, float roughness) +{ + float a = roughness * roughness; + float phi = 2.0 * PI * Xi.x; + float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y)); + float sinTheta = sqrt(max(1.0 - cosTheta * cosTheta, 0.0)); + float3 H = float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); + float3 up = abs(N.z) < 0.999 ? float3(0, 0, 1) : float3(1, 0, 0); + float3 T = normalize(cross(up, N)); + float3 B = cross(N, T); + return normalize(T * H.x + B * H.y + N * H.z); +} + +float GeometrySchlickGGX(float NdotV, float roughness) +{ + float a = roughness; + float k = (a * a) / 2.0; + return NdotV / max(NdotV * (1.0 - k) + k, 1e-5); +} + +float GeometrySmith(float NdotV, float NdotL, float roughness) +{ + return GeometrySchlickGGX(NdotV, roughness) * GeometrySchlickGGX(NdotL, roughness); +} + +float2 IntegrateBRDF(float NdotV, float roughness) +{ + float3 V = float3(sqrt(max(1.0 - NdotV * NdotV, 0.0)), 0.0, NdotV); + float A = 0.0; + float B = 0.0; + const uint SAMPLE_COUNT = 64u; + float3 N = float3(0, 0, 1); + for (uint i = 0u; i < SAMPLE_COUNT; ++i) + { + float2 Xi = Hammersley(i, SAMPLE_COUNT); + float3 H = ImportanceSampleGGX(Xi, N, roughness); + float3 L = normalize(2.0 * dot(V, H) * H - V); + float NdotL = saturate(L.z); + float NdotH = saturate(H.z); + float VdotH = saturate(dot(V, H)); + if (NdotL > 0.0) + { + float G = GeometrySmith(NdotV, NdotL, roughness); + float G_Vis = (G * VdotH) / max(NdotH * NdotV, 1e-5); + float Fc = pow(1.0 - VdotH, 5.0); + A += (1.0 - Fc) * G_Vis; + B += Fc * G_Vis; + } + } + A /= float(SAMPLE_COUNT); + B /= float(SAMPLE_COUNT); + return float2(A, B); +} + +[numthreads(8, 8, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + if (any(dtID.xy >= cb_Dims)) + return; + float2 uv = (float2(dtID.xy) + 0.5) / float2(cb_Dims); + float NdotV = max(uv.x, 1e-3); + float roughness = max(uv.y, 1e-3); + u_BRDFLUT[dtID.xy] = IntegrateBRDF(NdotV, roughness); +} diff --git a/res/gamedata/shaders/r5/ibl_prefilter_specular.cs b/res/gamedata/shaders/r5/ibl_prefilter_specular.cs new file mode 100644 index 00000000000..de3a94a6f18 --- /dev/null +++ b/res/gamedata/shaders/r5/ibl_prefilter_specular.cs @@ -0,0 +1,111 @@ +#define SM_6_0 + +TextureCube t_Source : register(t0); +SamplerState smp_linear : register(s0); +RWTexture2D u_Dest : register(u0); + +cbuffer PrefilterParams : register(b5) +{ + uint2 cb_Size; + uint cb_Face; + uint cb_SampleCount; + float cb_Roughness; + float cb_MipBias; + float2 cb_Pad; +}; + +static const float PI = 3.14159265359; + +float RadicalInverse_VdC(uint bits) +{ + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; +} + +float2 Hammersley(uint i, uint N) +{ + return float2(float(i) / float(N), RadicalInverse_VdC(i)); +} + +float3 ImportanceSampleGGX(float2 Xi, float3 N, float roughness) +{ + float a = max(roughness * roughness, 1e-4); + float phi = 2.0 * PI * Xi.x; + float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y)); + float sinTheta = sqrt(max(1.0 - cosTheta * cosTheta, 0.0)); + float3 H = float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); + float3 up = abs(N.z) < 0.999 ? float3(0, 0, 1) : float3(1, 0, 0); + float3 T = normalize(cross(up, N)); + float3 B = cross(N, T); + return normalize(T * H.x + B * H.y + N * H.z); +} + +float3 FaceDirection(uint face, float2 uv) +{ + float2 st = uv * 2.0 - 1.0; + if (face == 0u) return normalize(float3( 1.0, -st.y, -st.x)); + if (face == 1u) return normalize(float3(-1.0, -st.y, st.x)); + if (face == 2u) return normalize(float3( st.x, 1.0, st.y)); + if (face == 3u) return normalize(float3( st.x, -1.0, -st.y)); + if (face == 4u) return normalize(float3( st.x, -st.y, 1.0)); + return normalize(float3(-st.x, -st.y, -1.0)); +} + +float DistributionGGX(float NdotH, float roughness) +{ + float a = roughness * roughness; + float a2 = a * a; + float d = NdotH * NdotH * (a2 - 1.0) + 1.0; + return a2 / max(PI * d * d, 1e-6); +} + +[numthreads(8, 8, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + if (dtID.x >= cb_Size.x || dtID.y >= cb_Size.y) + return; + + float2 uv = (float2(dtID.xy) + 0.5) / float2(cb_Size); + float3 N = FaceDirection(cb_Face, uv); + float3 R = N; + float3 V = R; + + float3 prefiltered = 0.0.xxx; + float totalWeight = 0.0; + uint sampleCount = max(cb_SampleCount, 1u); + + if (cb_Roughness < 1e-3) + { + prefiltered = t_Source.SampleLevel(smp_linear, N, cb_MipBias).rgb; + totalWeight = 1.0; + } + else + { + for (uint i = 0u; i < sampleCount; ++i) + { + float2 Xi = Hammersley(i, sampleCount); + float3 H = ImportanceSampleGGX(Xi, N, cb_Roughness); + float3 L = normalize(2.0 * dot(V, H) * H - V); + float NdotL = saturate(dot(N, L)); + if (NdotL > 0.0) + { + float NdotH = saturate(dot(N, H)); + float HdotV = saturate(dot(H, V)); + float D = DistributionGGX(NdotH, cb_Roughness); + float pdf = D * NdotH / max(4.0 * HdotV, 1e-4); + float saTexel = 4.0 * PI / (6.0 * float(cb_Size.x * cb_Size.y)); + float saSample = 1.0 / max(float(sampleCount) * pdf, 1e-4); + float mip = cb_MipBias + 0.5 * log2(max(saSample / saTexel, 1e-4)); + prefiltered += t_Source.SampleLevel(smp_linear, L, mip).rgb * NdotL; + totalWeight += NdotL; + } + } + } + + prefiltered /= max(totalWeight, 1e-4); + u_Dest[dtID.xy] = float4(prefiltered, 1.0); +} diff --git a/res/gamedata/shaders/r5/ibl_probe_capture.cs b/res/gamedata/shaders/r5/ibl_probe_capture.cs new file mode 100644 index 00000000000..0d53de0a43d --- /dev/null +++ b/res/gamedata/shaders/r5/ibl_probe_capture.cs @@ -0,0 +1,91 @@ +#define SM_6_0 + +TextureCube t_Sky0 : register(t0); +TextureCube t_Sky1 : register(t1); +Texture2D g_SceneColor : register(t2); +Texture2D g_SceneDepth : register(t3); +SamplerState smp_linear : register(s0); +SamplerState smp_nofilter : register(s1); +RWTexture2D u_ProbeFaces : register(u0); + +cbuffer ProbeCaptureParams : register(b5) +{ + float4x4 cb_VP; + float4x4 cb_InvVP; + float3 cb_ProbePos; + float cb_SkyLerp; + float3 cb_EyePos; + float cb_MaxDistance; + uint2 cb_Size; + uint cb_Face; + uint cb_Pad; +}; + +float3 FaceDirection(uint face, float2 uv) +{ + float2 st = uv * 2.0 - 1.0; + if (face == 0u) return normalize(float3( 1.0, -st.y, -st.x)); + if (face == 1u) return normalize(float3(-1.0, -st.y, st.x)); + if (face == 2u) return normalize(float3( st.x, 1.0, st.y)); + if (face == 3u) return normalize(float3( st.x, -1.0, -st.y)); + if (face == 4u) return normalize(float3( st.x, -st.y, 1.0)); + return normalize(float3(-st.x, -st.y, -1.0)); +} + +bool ProjectWorld(float3 worldPos, out float2 uv, out float viewZ) +{ + float4 clip = mul(cb_VP, float4(worldPos, 1.0)); + if (clip.w <= 1e-4) + { + uv = 0; + viewZ = 0; + return false; + } + float3 ndc = clip.xyz / clip.w; + uv = float2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5); + viewZ = abs(clip.w); + return all(uv >= 0.0) && all(uv <= 1.0); +} + +[numthreads(8, 8, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + if (dtID.x >= cb_Size.x || dtID.y >= cb_Size.y) + return; + + float2 uv = (float2(dtID.xy) + 0.5) / float2(cb_Size); + float3 dir = FaceDirection(cb_Face, uv); + float3 sky = lerp( + t_Sky0.SampleLevel(smp_linear, dir, 0.0).rgb, + t_Sky1.SampleLevel(smp_linear, dir, 0.0).rgb, + saturate(cb_SkyLerp)); + + float3 color = sky; + const uint STEPS = 16u; + float stepLen = cb_MaxDistance / float(STEPS); + for (uint i = 1u; i <= STEPS; ++i) + { + float3 p = cb_ProbePos + dir * (stepLen * float(i)); + float2 suv; + float vz; + if (!ProjectWorld(p, suv, vz)) + continue; + float rawDepth = g_SceneDepth.SampleLevel(smp_nofilter, suv, 0).x; + if (rawDepth >= 0.9995) + continue; + float4 worldH = mul(cb_InvVP, float4(suv.x * 2.0 - 1.0, 1.0 - suv.y * 2.0, rawDepth, 1.0)); + float3 hitPos = worldH.xyz / max(worldH.w, 1e-5); + float dist = length(hitPos - cb_ProbePos); + if (dist > cb_MaxDistance) + continue; + float align = saturate(dot(normalize(hitPos - cb_ProbePos), dir)); + if (align < 0.85) + continue; + float3 scene = g_SceneColor.SampleLevel(smp_linear, suv, 0).rgb; + float w = saturate(1.0 - dist / cb_MaxDistance) * align; + color = lerp(color, scene, w); + break; + } + + u_ProbeFaces[dtID.xy] = float4(color, 1.0); +} diff --git a/res/gamedata/shaders/r5/ibl_sh_project.cs b/res/gamedata/shaders/r5/ibl_sh_project.cs new file mode 100644 index 00000000000..fd5a7aa35c3 --- /dev/null +++ b/res/gamedata/shaders/r5/ibl_sh_project.cs @@ -0,0 +1,72 @@ +#define SM_6_0 + +TextureCube t_Source : register(t0); +SamplerState smp_linear : register(s0); +RWStructuredBuffer u_SH : register(u0); + +cbuffer SHProjectParams : register(b5) +{ + uint cb_Size; + uint cb_OutOffset; + uint2 cb_Pad; +}; + +static const float PI = 3.14159265359; + +float3 FaceDirection(uint face, float2 uv) +{ + float2 st = uv * 2.0 - 1.0; + if (face == 0u) return normalize(float3( 1.0, -st.y, -st.x)); + if (face == 1u) return normalize(float3(-1.0, -st.y, st.x)); + if (face == 2u) return normalize(float3( st.x, 1.0, st.y)); + if (face == 3u) return normalize(float3( st.x, -1.0, -st.y)); + if (face == 4u) return normalize(float3( st.x, -st.y, 1.0)); + return normalize(float3(-st.x, -st.y, -1.0)); +} + +void SHAdd(inout float3 sh[9], float3 dir, float3 color, float weight) +{ + float x = dir.x; + float y = dir.y; + float z = dir.z; + float3 c = color * weight; + sh[0] += c * 0.282095; + sh[1] += c * (-0.488603 * y); + sh[2] += c * (0.488603 * z); + sh[3] += c * (-0.488603 * x); + sh[4] += c * (1.092548 * x * y); + sh[5] += c * (-1.092548 * y * z); + sh[6] += c * (0.315392 * (3.0 * z * z - 1.0)); + sh[7] += c * (-1.092548 * x * z); + sh[8] += c * (0.546274 * (x * x - y * y)); +} + +[numthreads(1, 1, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + float3 sh[9]; + [unroll] for (uint k = 0; k < 9; ++k) + sh[k] = 0.0.xxx; + + uint size = max(cb_Size, 1u); + for (uint face = 0u; face < 6u; ++face) + { + for (uint y = 0u; y < size; ++y) + { + for (uint x = 0u; x < size; ++x) + { + float2 uv = (float2(x, y) + 0.5) / float(size); + float3 dir = FaceDirection(face, uv); + float2 st = uv * 2.0 - 1.0; + float inv = rcp(max(dot(st, st) + 1.0, 1e-4)); + float solidAngle = 4.0 * inv * sqrt(inv); + solidAngle *= (4.0 * PI) / (6.0 * float(size * size)); + float3 color = t_Source.SampleLevel(smp_linear, dir, 0.0).rgb; + SHAdd(sh, dir, color, solidAngle); + } + } + } + + [unroll] for (uint o = 0; o < 9; ++o) + u_SH[cb_OutOffset + o] = float4(sh[o], 0.0); +} diff --git a/res/gamedata/shaders/r5/light_hiz_cull.cs b/res/gamedata/shaders/r5/light_hiz_cull.cs index 6b1f946a2e3..31d664aad23 100644 --- a/res/gamedata/shaders/r5/light_hiz_cull.cs +++ b/res/gamedata/shaders/r5/light_hiz_cull.cs @@ -29,9 +29,10 @@ void main(uint3 dtid : SV_DispatchThreadID) GPULightData ld = g_Lights[lightIdx]; float3 lightPos = ld.positionAndInvRangeSq.xyz; float range = ld.colorAndRange.w; + float cullRange = range * 1.75; float3 toLight = lightPos - cb_cameraPos.xyz; - if (dot(toLight, toLight) <= range * range) + if (dot(toLight, toLight) <= cullRange * cullRange) { uint idx; g_VisibleLightCount.InterlockedAdd(0, 1, idx); @@ -41,7 +42,7 @@ void main(uint3 dtid : SV_DispatchThreadID) } bool visible = HiZTestSphereTemporal( - lightPos, range, cb_cameraPos.xyz, + lightPos, cullRange, cb_cameraPos.xyz, cb_curViewProj, cb_prevViewProj, g_HiZPyramid, smp_nofilter, cb_hizWidth, cb_hizHeight, cb_hizMipLevels); @@ -54,3 +55,5 @@ void main(uint3 dtid : SV_DispatchThreadID) g_VisibleLightIndices[idx] = lightIdx; } } + +#define LOCAL_SHADOW_RECT_F4_V1 1 diff --git a/res/gamedata/shaders/r5/light_shadow_cull.cs b/res/gamedata/shaders/r5/light_shadow_cull.cs new file mode 100644 index 00000000000..fcf6b317f0b --- /dev/null +++ b/res/gamedata/shaders/r5/light_shadow_cull.cs @@ -0,0 +1,74 @@ +#define SM_5_0 +#include "common.h" + +struct GPUObjectData +{ + float3 position; + float radius; + uint batchIndex; + uint flags; + float2 padding; +}; + +cbuffer LightShadowCullParams : register(b5) +{ + float4 g_FrustumPlanes[6]; + uint g_ObjectCount; + float g_RadiusInflate; + uint g_MaxOut; + uint g_Pad; +}; + +StructuredBuffer g_Objects : register(t0); +ByteAddressBuffer g_InputDrawArgs : register(t1); +StructuredBuffer g_InputMaterialIDs : register(t2); + +RWStructuredBuffer g_OutIndices : register(u0); +RWByteAddressBuffer g_OutDrawArgs : register(u1); +RWStructuredBuffer g_OutMaterialIDs : register(u2); +RWByteAddressBuffer g_OutCount : register(u3); + +bool LightFrustumTestSphere(float3 center, float radius, float4 planes[6]) +{ + for (uint i = 0; i < 5; ++i) + { + float dist = dot(planes[i].xyz, center) + planes[i].w; + if (dist > radius) + return false; + } + return true; +} + +[numthreads(64, 1, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + uint objectIdx = dtID.x; + if (objectIdx >= g_ObjectCount) + return; + + GPUObjectData obj = g_Objects[objectIdx]; + float r = obj.radius * g_RadiusInflate; + if (!LightFrustumTestSphere(obj.position, r, g_FrustumPlanes)) + return; + + uint slot; + g_OutCount.InterlockedAdd(0, 1, slot); + if (slot >= g_MaxOut) + return; + + g_OutIndices[slot] = objectIdx; + + uint srcOff = objectIdx * 20; + uint indexCount = g_InputDrawArgs.Load(srcOff); + uint startIndex = g_InputDrawArgs.Load(srcOff + 8); + int baseVertex = asint(g_InputDrawArgs.Load(srcOff + 12)); + + uint dstOff = slot * 20; + g_OutDrawArgs.Store(dstOff, indexCount); + g_OutDrawArgs.Store(dstOff + 4, 1); + g_OutDrawArgs.Store(dstOff + 8, startIndex); + g_OutDrawArgs.Store(dstOff + 12, asuint(baseVertex)); + g_OutDrawArgs.Store(dstOff + 16, slot); + + g_OutMaterialIDs[slot] = g_InputMaterialIDs[objectIdx]; +} diff --git a/res/gamedata/shaders/r5/lighting.ps b/res/gamedata/shaders/r5/lighting.ps deleted file mode 100644 index a5e222adf98761a58dbe2ce1a0ca03a59eaba1b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7420 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 CZ9VM( diff --git a/res/gamedata/shaders/r5/local_shadow_esm_blur.cs b/res/gamedata/shaders/r5/local_shadow_esm_blur.cs new file mode 100644 index 00000000000..ac5c3eec287 --- /dev/null +++ b/res/gamedata/shaders/r5/local_shadow_esm_blur.cs @@ -0,0 +1,39 @@ +#define SM_6_0 +#include "common.h" + +cbuffer LocalShadowEsmBlurCB : register(b5) +{ + uint g_page; + uint g_x0; + uint g_y0; + uint g_size; + uint g_horizontal; + uint g_pad0; + uint g_pad1; + uint g_pad2; +}; + +Texture2DArray g_Input : register(t0); +RWTexture2DArray g_Output : register(u0); + +static const float kW[5] = { 0.06136, 0.24477, 0.38774, 0.24477, 0.06136 }; + +[numthreads(8, 8, 1)] +void main(uint3 id : SV_DispatchThreadID) +{ + if (id.x >= g_size || id.y >= g_size) + return; + + int2 base = int2(g_x0 + id.x, g_y0 + id.y); + float acc = 0.0; + [unroll] for (int i = -2; i <= 2; ++i) + { + int2 p = base; + if (g_horizontal != 0) + p.x = clamp(base.x + i, int(g_x0), int(g_x0 + g_size - 1)); + else + p.y = clamp(base.y + i, int(g_y0), int(g_y0 + g_size - 1)); + acc += g_Input.Load(int4(p, g_page, 0)) * kW[i + 2]; + } + g_Output[uint3(base, g_page)] = acc; +} diff --git a/res/gamedata/shaders/r5/local_shadow_esm_convert.cs b/res/gamedata/shaders/r5/local_shadow_esm_convert.cs new file mode 100644 index 00000000000..8129f967e32 --- /dev/null +++ b/res/gamedata/shaders/r5/local_shadow_esm_convert.cs @@ -0,0 +1,28 @@ +#define SM_6_0 +#include "common.h" + +cbuffer LocalShadowEsmCB : register(b5) +{ + uint g_page; + uint g_x0; + uint g_y0; + uint g_size; + float g_k; + float g_pad0; + float g_pad1; + float g_pad2; +}; + +Texture2DArray g_DepthAtlas : register(t0); +RWTexture2DArray g_EsmAtlas : register(u0); + +[numthreads(8, 8, 1)] +void main(uint3 id : SV_DispatchThreadID) +{ + if (id.x >= g_size || id.y >= g_size) + return; + + uint2 coord = uint2(g_x0 + id.x, g_y0 + id.y); + float depth = g_DepthAtlas.Load(int4(coord, g_page, 0)); + g_EsmAtlas[uint3(coord, g_page)] = exp(g_k * saturate(depth)); +} diff --git a/res/gamedata/shaders/r5/motion_blur.ps b/res/gamedata/shaders/r5/motion_blur.ps new file mode 100644 index 0000000000000000000000000000000000000000..df0aa65733a3e5c79e89c1a056897d66efd314f7 GIT binary patch literal 1558 zcma)6O>f&U488kT5ZFa-KIXXHFf45kYj+6HuECN81BSs+QY91M$blrM&f5ItXXD5va#U@{!b=N3oVl!0bMIpCwT=7nLowNN-`2-4uDD;Uj3Vk;V$mY!Z(U&sAXDqZCHPtSvM#Hg=O3DZrTERC+@=M^V=_?m`LxbFS&q@+I`yRF2{ zS%?)X892^fufwWP#sc-rxJ5Py|KOvzlFxy!h@yTXUQJWj_R~P=&9dbVPo~_WI}WM0 zc3Qp;?Un=?gM`bF2F;87dtQ7KEP|OkG8yAa_+v t_DirectLighting : register(t0); +Texture2D t_DenoisedDiffuse : register(t1); +Texture2D t_DenoisedSpecular : register(t2); +Texture2D t_Depth : register(t3); +Texture2D t_SceneColorIn : register(t4); +Texture2D t_BaseColor : register(t5); +Texture2D t_Normal : register(t6); +Texture2D t_WorldPos : register(t7); +Texture2D t_ClassifyWorldPos : register(t8); + +RWTexture2D u_SceneColor : register(u0); +RWTexture2D u_OutDiffuse : register(u1); +RWTexture2D u_OutSpecular : register(u2); + +[numthreads(8, 8, 1)] +void main(uint3 dispatchID : SV_DispatchThreadID) +{ + uint2 pixel = dispatchID.xy; + float2 screenSize = g_Params.xy; + uint method = (uint)g_Params.z; + if (pixel.x >= (uint)screenSize.x || pixel.y >= (uint)screenSize.y) + return; + + float depth = t_Depth.Load(int3(pixel, 0)); + float guideMark = t_WorldPos.Load(int3(pixel, 0)).w; + float classifyMark = t_ClassifyWorldPos.Load(int3(pixel, 0)).w; + if (depth >= 1.0 || SkipRtSurfLighting(classifyMark, guideMark)) { + u_SceneColor[pixel] = t_SceneColorIn.Load(int3(pixel, 0)); + u_OutDiffuse[pixel] = 0; + u_OutSpecular[pixel] = 0; + return; + } + + float3 direct = t_DirectLighting.Load(int3(pixel, 0)).rgb; + float4 dIn = t_DenoisedDiffuse.Load(int3(pixel, 0)); + float4 sIn = t_DenoisedSpecular.Load(int3(pixel, 0)); + float4 baseColor = t_BaseColor.Load(int3(pixel, 0)); + float4 nData = t_Normal.Load(int3(pixel, 0)); + float3 worldPos = t_WorldPos.Load(int3(pixel, 0)).xyz; + float3 N = normalize(nData.xyz); + float roughness = max(saturate(nData.w), MIN_ROUGHNESS); + float3 albedo = max(baseColor.rgb, 0.0); + float metallic = UnpackMetallicFromBaseA(baseColor.a); + float3 V = normalize(g_CameraPos.xyz - worldPos); + float3 F0 = CalculateF0(albedo, metallic); + + float3 diffFactor, specFactor; + NRD_MaterialFactors(N, V, albedo, F0, roughness, diffFactor, specFactor); + + float3 diff = (method == 0) ? NRD_YCoCgToLinear(dIn.xyz) : max(dIn.xyz, 0.0); + float3 spec = (method == 0) ? NRD_YCoCgToLinear(sIn.xyz) : max(sIn.xyz, 0.0); + diff = NRD_SanitizeRadiance(diff) * diffFactor; + spec = NRD_SanitizeRadiance(spec) * specFactor; + + float rejitter = 1.0; + if (g_Params.w > 0.5) { + float3 Ne = normalize(t_Normal.Load(int3(int2(pixel) + int2(1, 0), 0)).xyz); + float3 Nw = normalize(t_Normal.Load(int3(int2(pixel) + int2(-1, 0), 0)).xyz); + float3 Nn = normalize(t_Normal.Load(int3(int2(pixel) + int2(0, 1), 0)).xyz); + float3 Ns = normalize(t_Normal.Load(int3(int2(pixel) + int2(0, -1), 0)).xyz); + float edge = 1.0 - 0.25 * (saturate(dot(N, Ne)) + saturate(dot(N, Nw)) + saturate(dot(N, Nn)) + saturate(dot(N, Ns))); + float3 L = normalize(diff + spec + 1e-4); + float lobe = saturate(dot(N, L)); + rejitter = lerp(1.08, 0.92, saturate(edge * 2.0)) * lerp(1.05, 0.97, lobe); + diff *= rejitter; + spec *= rejitter; + } + + u_OutDiffuse[pixel] = float4(diff, 1.0); + u_OutSpecular[pixel] = float4(spec, 1.0); + float3 ambientBase = t_SceneColorIn.Load(int3(pixel, 0)).rgb; + float3 lit = direct + diff + spec; + float dist = length(worldPos - g_CameraPos.xyz); + float fog = saturate(dist * g_FogParams.w + g_FogParams.x); + lit *= (1.0 - fog); + u_SceneColor[pixel] = float4(ambientBase + lit, 1.0); +} diff --git a/res/gamedata/shaders/r5/nrd_pack_inputs.cs b/res/gamedata/shaders/r5/nrd_pack_inputs.cs new file mode 100644 index 00000000000..b8cecb912e5 --- /dev/null +++ b/res/gamedata/shaders/r5/nrd_pack_inputs.cs @@ -0,0 +1,122 @@ +#include "shared/pbr_brdf.h" +#include "shared/basecolor_pack.h" +#include "shared/nrd_helpers.h" + +cbuffer NrdPackParams : register(b5) { + float4x4 g_WorldToView; + float4x4 g_WorldToViewPrev; + float4x4 g_WorldToClip; + float4x4 g_WorldToClipPrev; + float4 g_ScreenNearFar; + float4 g_HitDistMethod; + float4 g_CameraPos_Range; +}; + +Texture2D t_NoisyDiffuse : register(t0); +Texture2D t_NoisySpecular : register(t1); +Texture2D t_HitDist : register(t2); +Texture2D t_Normal : register(t3); +Texture2D t_Depth : register(t4); +Texture2D t_WorldPos : register(t5); +Texture2D t_BaseColor : register(t6); + +RWTexture2D u_DiffRadianceHitDist : register(u0); +RWTexture2D u_SpecRadianceHitDist : register(u1); +RWTexture2D u_NormalRoughness : register(u2); +RWTexture2D u_ViewZ : register(u3); +RWTexture2D u_MotionVectors : register(u4); + +float4 PackNormalRoughness(float3 N, float roughness) +{ + N /= max(abs(N.x), max(abs(N.y), abs(N.z))); + return float4(N * 0.5 + 0.5, saturate(roughness)); +} + +float2 ProjectToUv(float4x4 worldToClip, float3 worldPos) +{ + float4 clip = mul(worldToClip, float4(worldPos, 1.0)); + float2 ndc = clip.xy / max(clip.w, 1e-5); + ndc.y = -ndc.y; + return ndc * 0.5 + 0.5; +} + +[numthreads(8, 8, 1)] +void main(uint3 dispatchID : SV_DispatchThreadID) +{ + uint2 pixel = dispatchID.xy; + float2 screenSize = g_ScreenNearFar.xy; + if (pixel.x >= (uint)screenSize.x || pixel.y >= (uint)screenSize.y) + return; + + float nearZ = g_ScreenNearFar.z; + float denoisingRange = g_CameraPos_Range.w; + float3 hitDistParams = g_HitDistMethod.xyz; + uint method = (uint)g_HitDistMethod.w; + float3 cameraPos = g_CameraPos_Range.xyz; + + float depth = t_Depth.Load(int3(pixel, 0)); + if (depth >= 1.0) { + u_DiffRadianceHitDist[pixel] = 0; + u_SpecRadianceHitDist[pixel] = 0; + u_NormalRoughness[pixel] = float4(0.5, 0.5, 1.0, 1.0); + u_ViewZ[pixel] = denoisingRange * 2.0; + u_MotionVectors[pixel] = 0; + return; + } + + float3 worldPos = t_WorldPos.Load(int3(pixel, 0)).xyz; + float viewZ = mul(g_WorldToView, float4(worldPos, 1.0)).z; + viewZ = max(abs(viewZ), nearZ); + + float viewZPrev = mul(g_WorldToViewPrev, float4(worldPos, 1.0)).z; + viewZPrev = max(abs(viewZPrev), nearZ); + + float2 uv = ProjectToUv(g_WorldToClip, worldPos); + float2 uvPrev = ProjectToUv(g_WorldToClipPrev, worldPos); + float3 motion; + motion.xy = (uvPrev - uv) * screenSize; + motion.z = viewZPrev - viewZ; + u_MotionVectors[pixel] = float4(motion, 0.0); + + float4 nData = t_Normal.Load(int3(pixel, 0)); + float3 N = normalize(nData.xyz); + float roughness = max(saturate(nData.w), MIN_ROUGHNESS); + float4 baseColor = t_BaseColor.Load(int3(pixel, 0)); + float3 albedo = max(baseColor.rgb, 0.0); + float metallic = UnpackMetallicFromBaseA(baseColor.a); + float3 V = normalize(cameraPos - worldPos); + float3 F0 = CalculateF0(albedo, metallic); + + float3 diffFactor, specFactor; + NRD_MaterialFactors(N, V, albedo, F0, roughness, diffFactor, specFactor); + + float diffHit = NRD_TrimHitDistance(max(t_HitDist.Load(int3(pixel, 0)), 0.0), 1e-3); + float4 specData = t_NoisySpecular.Load(int3(pixel, 0)); + float specHit = NRD_TrimHitDistance(max(specData.a, 0.0), 1e-3); + + float3 diffIrradiance = max(t_NoisyDiffuse.Load(int3(pixel, 0)).rgb, 0.0); + float3 specIrradiance = max(specData.rgb, 0.0); + + float3 diff = NRD_SanitizeRadiance(diffIrradiance / diffFactor); + float3 spec = NRD_SanitizeRadiance(specIrradiance / specFactor); + + if (metallic >= 0.999) { + diff = 0; + diffHit = 0; + } + + float4 packedDiff; + float4 packedSpec; + if (method == 0) { + packedDiff = float4(NRD_LinearToYCoCg(diff), REBLUR_GetNormHitDist(diffHit, viewZ, hitDistParams, 1.0)); + packedSpec = float4(NRD_LinearToYCoCg(spec), REBLUR_GetNormHitDist(specHit, viewZ, hitDistParams, roughness)); + } else { + packedDiff = float4(diff, diffHit); + packedSpec = float4(spec, specHit); + } + + u_DiffRadianceHitDist[pixel] = packedDiff; + u_SpecRadianceHitDist[pixel] = packedSpec; + u_NormalRoughness[pixel] = PackNormalRoughness(N, roughness); + u_ViewZ[pixel] = viewZ; +} diff --git a/res/gamedata/shaders/r5/pnv.h b/res/gamedata/shaders/r5/pnv.h index 316816d4c72..9c6133fb28c 100644 --- a/res/gamedata/shaders/r5/pnv.h +++ b/res/gamedata/shaders/r5/pnv.h @@ -20,10 +20,9 @@ #define SCANLINES_INTENSITY 0.015 // ��������� ������������� #define VIGNETTE_RADIUS 1.0 // -// Pixel -// Note: screen_res is now in static_globals cbuffer (shared/common.h) -// Note: m_zoom_deviation remains as loose uniform (engine binds it individually) +#ifndef PNV_ZOOM_DEVIATION_DEFINED uniform float4 m_zoom_deviation; +#endif float4 calc_night_vision_effect(float2 tc0, float4 color, float3 NV_COLOR) { diff --git a/res/gamedata/shaders/r5/postprocess.ps b/res/gamedata/shaders/r5/postprocess.ps new file mode 100644 index 0000000000000000000000000000000000000000..649c14a01ea43592076ccaedb9af9e8a4d5d6885 GIT binary patch literal 3118 zcmb7GZENF35dQ98F)6skmS`fFZ;|hqrFPB0wuReRZC^9LVNUm1o+#eg{a3+meG1A(nxvJi_8AS<@7Q-JwxY3QpqzgubyG! zA}!P~+*XO>aBZcM?Mt2G28VChMJCGOdRDCEbo4eIjjq;mljIT>kI&IR^Z6gq-Tm|I zm*s5!h)=)G9`El)RW0fyZV1Yv*P_UxwcIDTT;$Wy)u{+&zGv!}@=))T4DO(cRsuKN zrM&j+>1c@XdkBAkV19atV1Phc*8M2q1w=o}7!gk*D^sbSaElVEtc+BhA{|0?qg9mg z8Im^ELs8a9mfFQNES{p-PE*(BK5feSh%;+67Io)6pmxD$H!n<5H-tth*#Ljkhp!cl(a$Y1PBL zHH~eLW9pb()ABz5-R(_&hV9+nKeJ4|o@M%;W!|;az26w>o^KE;A?WD>pAIpXH--mj-ODRRK;Wi7BbjO z8m!{as07dRZGQcouLNW|ff_YRC`*Al#jdkRBgVB+b7*;}k`Q&%zuP2Jaz#HQN4C^# z@eH`UYh6l92Pd}^yTiiJohFgM?~wSz?#PE+<8Ha-bgqu3&u8JCXgdzK2HS^ShL)K) zFh$x`hoQg^zS351{7FD}?~3Fvrpd-6H9SGmcXrPA|M3=b73Es{G`_HDEl&9h z2kT_OfF1`?;j}KCRth~GHO0ocbSe;89nsZEGfW~aQZJa8y?9zCRC(<5_KG|)thN6a zt=m9jYy$n&p?>10uCi$MQxE1QBu9b#K~lq~GT+uO*h|_B6NAqzIb9TL7)E}hq6HV* zB!F0^Df&Xkg|aXZ(rZ)~Ouo9F1AJ>ZY6;4z7t91FADudwBuU*J0QS3oam5swXBA~lu$$HB7JITrt;wy1J|65dNtHW-HLJBxc!oa z5ToBlLT9sarC+vZr2zusev8CE)@seiOq09PK1iO;ru0iQ_^f4T9kpqdFZRD z31w~&j9UZ0R|QJjPhn70qxav@O6Gn7PK`SQ~V!WyU!j5X~r`z{ih7t6BPn zrJW2#J3JJ&0Z`&RoE#oxjT`Jm5(vNPD6dH_7HJ&ibf}H?Nri{ZNa9gGuCWo289Eks Yii3hfU*rnMpXStz40FI>13KUQ2e46;Z2$lO literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/rain/rain_billboard.cs b/res/gamedata/shaders/r5/rain/rain_billboard.cs index 29919fb1f82..513dc1d6c2a 100644 --- a/res/gamedata/shaders/r5/rain/rain_billboard.cs +++ b/res/gamedata/shaders/r5/rain/rain_billboard.cs @@ -27,7 +27,7 @@ struct RainParticle { void WriteRainVertex(uint vIdx, float3 pos, uint col, float2 uv) { uint b = vIdx * 24u; - g_Vertices.Store3(b, asuint(float4(pos, 0.0))); + g_Vertices.Store3(b, asuint(pos)); g_Vertices.Store(b + 12u, col); g_Vertices.Store2(b + 16u, asuint(uv)); } diff --git a/res/gamedata/shaders/r5/rain/rain_update.cs b/res/gamedata/shaders/r5/rain/rain_update.cs index ae34c2ae293..0fd402e0aee 100644 --- a/res/gamedata/shaders/r5/rain/rain_update.cs +++ b/res/gamedata/shaders/r5/rain/rain_update.cs @@ -112,9 +112,21 @@ bool UnderRoof(float3 headPos) return false; float depth = saturate(uvz.z); - float shadowDepth = g_RainShadow.SampleLevel(smp_nofilter, uvz.xy, 0).x; const float bias = 0.0015; - float cover = saturate((shadowDepth - (depth - bias)) * 800.0); + float2 smSize; + g_RainShadow.GetDimensions(smSize.x, smSize.y); + float2 texel = 1.0 / max(smSize, float2(1.0, 1.0)); + float cover = 0.0; + [unroll] for (int y = -2; y <= 2; ++y) + { + [unroll] for (int x = -2; x <= 2; ++x) + { + float2 o = float2((float)x, (float)y) * texel * 1.25; + float shadowDepth = g_RainShadow.SampleLevel(smp_nofilter, uvz.xy + o, 0).x; + cover += saturate((shadowDepth - (depth - bias)) * 120.0); + } + } + cover *= (1.0 / 25.0); float edge = saturate(min(uvz.x, 1.0 - uvz.x) * 8.0) * saturate(min(uvz.y, 1.0 - uvz.y) * 8.0); cover = lerp(1.0, cover, edge); return cover < 0.5; diff --git a/res/gamedata/shaders/r5/restir_di_common.h b/res/gamedata/shaders/r5/restir_di_common.h new file mode 100644 index 00000000000..b2c25c4ce8c --- /dev/null +++ b/res/gamedata/shaders/r5/restir_di_common.h @@ -0,0 +1,81 @@ +#ifndef RESTIR_DI_COMMON_H +#define RESTIR_DI_COMMON_H + +#ifndef RESTIR_GI_COMMON_H +#error "restir_gi_common.h must be included before restir_di_common.h" +#endif + +static const float RESTIR_DI_W_MAX = 64.0; + +struct DIReservoir +{ + uint lightIndex; + float targetPdf; + float W; + float w_sum; + uint M; + uint age; +}; + +float ClampDIReservoirW(float W) +{ + if (isnan(W) || isinf(W) || W <= 0) + return 0; + return min(W, RESTIR_DI_W_MAX); +} + +DIReservoir EmptyDIReservoir() +{ + DIReservoir r; + r.lightIndex = RESTIR_INVALID_ID; + r.targetPdf = 0; + r.W = 0; + r.w_sum = 0; + r.M = 0; + r.age = 0; + return r; +} + +bool IsDIReservoirValid(DIReservoir r) +{ + return r.lightIndex != RESTIR_INVALID_ID && r.M > 0 && r.W > 0; +} + +bool DIReservoirUpdate(inout DIReservoir r, float weight, uint lightIndex, float targetPdf, inout uint rng) +{ + r.M += 1; + if (isnan(weight) || isinf(weight) || weight <= 0) + return false; + + r.w_sum += weight; + + float xi = rand_float(rng); + if (xi < weight / max(r.w_sum, 1e-6)) + { + r.lightIndex = lightIndex; + r.targetPdf = targetPdf; + return true; + } + return false; +} + +float4 PackDIReservoir(DIReservoir r) +{ + uint meta = ((min(r.M, 65535u) & 0xFFFFu) << 16) | (min(r.age, 65535u) & 0xFFFFu); + return float4(asfloat(r.lightIndex), r.W, r.targetPdf, asfloat(meta)); +} + +DIReservoir UnpackDIReservoir(float4 packed) +{ + DIReservoir r; + r.lightIndex = asuint(packed.x); + r.W = packed.y; + r.targetPdf = packed.z; + uint meta = asuint(packed.w); + r.M = (meta >> 16) & 0xFFFFu; + r.age = meta & 0xFFFFu; + r.w_sum = 0; + return r; +} + +#endif diff --git a/res/gamedata/shaders/r5/restir_di_eval.h b/res/gamedata/shaders/r5/restir_di_eval.h new file mode 100644 index 00000000000..b1f26cf61bd --- /dev/null +++ b/res/gamedata/shaders/r5/restir_di_eval.h @@ -0,0 +1,89 @@ +#ifndef RESTIR_DI_EVAL_H +#define RESTIR_DI_EVAL_H + +struct GPULightDataDI { + float4 positionAndInvRangeSq; + float4 colorAndRange; + float4 directionAndSpotScale; + float4 spotParamsAndType; + float4x4 spotVP; + float4 localShadowRect; +}; + +float PointAttenDI(float distSq, float invRangeSq) +{ + float factor = saturate(1.0 - distSq * abs(invRangeSq)); + return factor * factor; +} + +float SpotAttenDI(float3 toLight, float3 spotDir, float scale, float offset) +{ + float cosAngle = dot(normalize(-toLight), spotDir); + return saturate(cosAngle * scale + offset); +} + +float EvalLocalLightAttenuationDI(GPULightDataDI light, float3 worldPos, out float3 L, out float dist, out float3 lightColor) +{ + float3 lightPos = light.positionAndInvRangeSq.xyz; + float invRangeSq = abs(light.positionAndInvRangeSq.w); + lightColor = light.colorAndRange.xyz; + float lightType = light.spotParamsAndType.y; + + float3 toLight = lightPos - worldPos; + float distSq = dot(toLight, toLight); + dist = sqrt(max(distSq, 1e-8)); + L = toLight / dist; + float atten = PointAttenDI(distSq, invRangeSq); + + if (lightType > 0.5) { + uint texIdx = asuint(light.spotParamsAndType.z); + if (texIdx != 0xFFFFFFFFu) { + float4 projPos = mul(light.spotVP, float4(worldPos, 1.0)); + if (projPos.w > 0) { + float2 projUV = projPos.xy / projPos.w * 0.5 + 0.5; + projUV.y = 1.0 - projUV.y; + Texture2D spotTex = GetBindlessTexture(texIdx); + atten *= spotTex.SampleLevel(smp_linear, projUV, 0).r; + } else { + atten = 0; + } + } else { + float3 spotDir = light.directionAndSpotScale.xyz; + float spotScale = light.directionAndSpotScale.w; + float spotOffset = light.spotParamsAndType.x; + atten *= SpotAttenDI(toLight, spotDir, spotScale, spotOffset); + } + } + return atten; +} + +float EvalLocalLightTargetPdfDI( + GPULightDataDI light, float3 worldPos, float3 N, float3 V, + float3 albedo, float metallic, float roughness) +{ + float3 L, lightColor; + float dist; + float atten = EvalLocalLightAttenuationDI(light, worldPos, L, dist, lightColor); + if (atten <= 0.001) + return 0; + float3 lit = PBRDirectLighting(albedo, N, V, L, lightColor * atten, metallic, roughness, 1); + return Luminance(lit); +} + +bool IsHudLightDI(GPULightDataDI light) +{ + return light.positionAndInvRangeSq.w < 0.0; +} + +float LightEmitterRadiusDI(GPULightDataDI light) +{ + float range = max(abs(light.colorAndRange.w), 0.5); + return clamp(range * 0.02, 0.03, 0.4); +} + +float SoftShadowAmountDI(float dist) +{ + return saturate((dist - 1.25) / 10.0); +} + +#endif diff --git a/res/gamedata/shaders/r5/restir_di_shade.cs b/res/gamedata/shaders/r5/restir_di_shade.cs new file mode 100644 index 00000000000..0ac19dc615a --- /dev/null +++ b/res/gamedata/shaders/r5/restir_di_shade.cs @@ -0,0 +1,150 @@ +#include "bindless_common.h" +#include "rt_common.h" +#include "rt_visibility.h" +#include "shared/pbr_brdf.h" +#include "shared/basecolor_pack.h" +#include "shared/foliage_sss.h" +#include "shared/skin_sss.h" +#include "restir_gi_common.h" +#include "restir_di_common.h" +#include "restir_di_eval.h" +#include "shared/surface_marks.h" + +cbuffer ReSTIRDIShadeParams : register(b5) { + float4 g_CameraPos; + float2 g_ScreenSize; + uint g_GrassBatchStart; + uint g_DetailAtlasIndex; + float4 g_ClusterParams; + uint g_IdentityStaticCount; + uint g_TerrainBatchCount; + uint g_SkinnedBatchStart; + uint g_Pad0; +}; + +RaytracingAccelerationStructure g_SceneTLAS : register(t1); +StructuredBuffer g_LightData : register(t20); +StructuredBuffer g_BatchInfo : register(t2); +ByteAddressBuffer g_MegaVB : register(t3); +ByteAddressBuffer g_MegaIB : register(t18); +ByteAddressBuffer g_GrassVB : register(t12); +ByteAddressBuffer g_GrassIB : register(t13); +Texture2D t_DIReservoir : register(t0); +Texture2D t_Depth : register(t4); +Texture2D t_BaseColor : register(t6); +Texture2D t_WorldPos : register(t7); +Texture2D t_Normal : register(t11); + +RWTexture2D u_DirectLighting : register(u0); + +float TraceShadowRayDI(float3 origin, float3 dir, float tMax) +{ + return TraceVisibilityAtten( + g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, + origin, dir, tMax, + g_IdentityStaticCount, g_TerrainBatchCount, g_SkinnedBatchStart, g_GrassBatchStart, + g_DetailAtlasIndex, false); +} + +float TraceSoftShadowDI(float3 origin, float3 lightPos, float emitterRadius, bool hudLight, inout uint rng) +{ + float3 toLight = lightPos - origin; + float dist = length(toLight); + if (dist < 1e-4) + return 1.0; + + float3 L = toLight / dist; + float soft = hudLight ? 0.0 : SoftShadowAmountDI(dist); + float radius = emitterRadius * lerp(0.12, 1.0, soft); + uint samples = 1; + if (soft > 0.08) + samples = 2; + if (soft > 0.4) + samples = 4; + + float3 up = (abs(L.y) < 0.99) ? float3(0, 1, 0) : float3(1, 0, 0); + float3 T = normalize(cross(up, L)); + float3 B = cross(L, T); + + float endSkip = hudLight ? 0.05 : max(0.55, emitterRadius * 3.0 + 0.25); + float vis = 0.0; + for (uint i = 0; i < samples; i++) { + float2 u = float2(rand_float(rng), rand_float(rng)); + float r = sqrt(u.x) * radius; + float a = u.y * 6.2831853; + float3 samplePos = lightPos + T * (cos(a) * r) + B * (sin(a) * r); + float3 dir = samplePos - origin; + float d = length(dir); + float tMax = max(d - endSkip, d * 0.88); + if (tMax < 0.02) + { + vis += 1.0; + continue; + } + vis += TraceShadowRayDI(origin, dir / max(d, 1e-4), tMax); + } + return vis / (float)samples; +} + +[numthreads(8, 8, 1)] +void main(uint3 dispatchID : SV_DispatchThreadID) +{ + uint2 pixel = dispatchID.xy; + if (pixel.x >= (uint)g_ScreenSize.x || pixel.y >= (uint)g_ScreenSize.y) + return; + + float depth = t_Depth.Load(int3(pixel, 0)); + if (depth >= 1.0) + return; + + DIReservoir r = UnpackDIReservoir(t_DIReservoir.Load(int3(pixel, 0))); + if (!IsDIReservoirValid(r) || r.lightIndex >= (uint)g_ClusterParams.w) + return; + + float4 worldPosData = t_WorldPos.Load(int3(pixel, 0)); + float3 worldPos = worldPosData.xyz; + const bool hudSurf = IsHudSurfMark(worldPosData.w); + float4 normalData = t_Normal.Load(int3(pixel, 0)); + float3 N = normalize(normalData.xyz); + float roughness = max(normalData.w, MIN_ROUGHNESS); + float4 baseColorData = t_BaseColor.Load(int3(pixel, 0)); + float3 albedo = max(baseColorData.rgb, 0.0); + float metallic = UnpackMetallicFromBaseA(baseColorData.a); + float sssMask = UnpackSSSMaskFromBaseA(baseColorData.a); + float3 V = normalize(g_CameraPos.xyz - worldPos); + + GPULightDataDI light = g_LightData[r.lightIndex]; + const bool hudLight = IsHudLightDI(light); + if (hudSurf && !hudLight) + return; + + float3 L, lightColor; + float dist; + float atten = EvalLocalLightAttenuationDI(light, worldPos, L, dist, lightColor); + if (atten <= 0.001) + return; + + float shadow = 1.0; + if (!hudLight && asuint(light.spotParamsAndType.w) != 0xFFFFFFFFu) { + float3 biased = worldPos + N * 0.05; + uint rng = pcg_hash(pixel.x + pixel.y * 1973u + r.lightIndex * 26699u + asuint(dist * 100.0)); + shadow = TraceSoftShadowDI(biased, light.positionAndInvRangeSq.xyz, LightEmitterRadiusDI(light), false, rng); + if (!hudSurf && shadow <= 0.001 && light.spotParamsAndType.y < 0.5) + shadow = 0.35; + } + if (shadow <= 0.001) + return; + + float3 shaded = PBRDirectLighting(albedo, N, V, L, lightColor * atten * shadow, metallic, roughness, 1); + if (!hudSurf && sssMask > 0.01) { + float3 sssTint = SkinSSSTint(); + float sssThickness = saturate(0.55 + sssMask * 0.35); + shaded += EvaluateFoliageSSS( + albedo, N, V, L, lightColor * atten, max(shadow, 0.35), + sssTint, sssThickness, sssMask); + } + + float3 direct = u_DirectLighting[pixel].rgb; + direct += shaded * r.W; + u_DirectLighting[pixel] = float4(direct, 1.0); +} diff --git a/res/gamedata/shaders/r5/restir_di_spatial.cs b/res/gamedata/shaders/r5/restir_di_spatial.cs new file mode 100644 index 00000000000..1ca8b2c9cb7 --- /dev/null +++ b/res/gamedata/shaders/r5/restir_di_spatial.cs @@ -0,0 +1,170 @@ +#include "bindless_common.h" +#include "rt_common.h" +#include "rt_visibility.h" +#include "shared/pbr_brdf.h" +#include "shared/basecolor_pack.h" +#include "restir_gi_common.h" +#include "restir_di_common.h" +#include "restir_di_eval.h" +#include "shared/surface_marks.h" + +cbuffer ReSTIRDISpatialParams : register(b5) { + float4x4 g_WorldToView; + float4 g_CameraPos; + float2 g_ScreenSize; + float2 g_InvScreenSize; + uint g_FrameIndex; + uint g_SpatialSamples; + float g_SpatialRadius; + uint g_MMax; + float4 g_ClusterParams; + float4 g_ClusterScales; + uint g_GrassBatchStart; + uint g_DetailAtlasIndex; + uint g_IdentityStaticCount; + uint g_TerrainBatchCount; + uint g_SkinnedBatchStart; + uint g_Pad0; + uint g_Pad1; + uint g_Pad2; +}; + +RaytracingAccelerationStructure g_SceneTLAS : register(t1); +StructuredBuffer g_LightData : register(t20); +StructuredBuffer g_BatchInfo : register(t2); +ByteAddressBuffer g_MegaVB : register(t3); +ByteAddressBuffer g_MegaIB : register(t18); +ByteAddressBuffer g_GrassVB : register(t12); +ByteAddressBuffer g_GrassIB : register(t13); +Texture2D t_SrcDI : register(t0); +Texture2D t_Depth : register(t4); +Texture2D t_BaseColor : register(t6); +Texture2D t_WorldPos : register(t7); +Texture2D t_Normal : register(t11); + +RWTexture2D u_DIReservoir : register(u0); + +float TraceShadowDI(float3 origin, float3 dir, float tMax) +{ + return TraceVisibilityAtten( + g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, + origin, dir, tMax, + g_IdentityStaticCount, g_TerrainBatchCount, g_SkinnedBatchStart, g_GrassBatchStart, + g_DetailAtlasIndex, false); +} + +[numthreads(8, 8, 1)] +void main(uint3 dispatchID : SV_DispatchThreadID) +{ + uint2 pixel = dispatchID.xy; + if (pixel.x >= (uint)g_ScreenSize.x || pixel.y >= (uint)g_ScreenSize.y) + return; + + float depth = t_Depth.Load(int3(pixel, 0)); + if (depth >= 1.0 || g_SpatialSamples == 0) { + u_DIReservoir[pixel] = t_SrcDI.Load(int3(pixel, 0)); + return; + } + + float4 worldPosData = t_WorldPos.Load(int3(pixel, 0)); + float3 worldPos = worldPosData.xyz; + const float centerMark = worldPosData.w; + const bool hudSurf = IsHudSurfMark(centerMark); + float4 normalData = t_Normal.Load(int3(pixel, 0)); + float3 N = normalize(normalData.xyz); + float roughness = max(normalData.w, MIN_ROUGHNESS); + float4 baseColorData = t_BaseColor.Load(int3(pixel, 0)); + float3 albedo = max(baseColorData.rgb, 0.0); + float metallic = UnpackMetallicFromBaseA(baseColorData.a); + float3 V = normalize(g_CameraPos.xyz - worldPos); + float linearDepth = abs(mul(g_WorldToView, float4(worldPos, 1.0)).z); + + DIReservoir center = UnpackDIReservoir(t_SrcDI.Load(int3(pixel, 0))); + uint rng = pcg_hash(pixel.x + pixel.y * 1973u + g_FrameIndex * 26699u); + DIReservoir output = EmptyDIReservoir(); + + float targetCenter = 0; + if (IsDIReservoirValid(center) && center.lightIndex < (uint)g_ClusterParams.w) { + if (!(hudSurf && !IsHudLightDI(g_LightData[center.lightIndex]))) { + targetCenter = EvalLocalLightTargetPdfDI( + g_LightData[center.lightIndex], worldPos, N, V, albedo, metallic, roughness); + if (targetCenter > 0) { + output.lightIndex = center.lightIndex; + output.targetPdf = targetCenter; + output.w_sum = targetCenter * center.W; + output.M = 1; + output.age = center.age; + } + } + } + + for (uint i = 0; i < g_SpatialSamples; i++) { + float2 disc = float2(rand_float(rng), rand_float(rng)); + float ang = disc.x * 6.2831853; + float rad = sqrt(disc.y) * g_SpatialRadius; + int2 nPixel = int2(float2(pixel) + float2(cos(ang), sin(ang)) * rad); + if (nPixel.x < 0 || nPixel.y < 0 || + nPixel.x >= (int)g_ScreenSize.x || nPixel.y >= (int)g_ScreenSize.y) + continue; + + float4 nWorldData = t_WorldPos.Load(int3(nPixel, 0)); + if (!SameHudSurfClass(centerMark, nWorldData.w)) + continue; + float3 nWorld = nWorldData.xyz; + float3 nN = normalize(t_Normal.Load(int3(nPixel, 0)).xyz); + float nDepth = abs(mul(g_WorldToView, float4(nWorld, 1.0)).z); + if (abs(nDepth - linearDepth) / max(linearDepth, 1e-3) > 0.1) + continue; + if (dot(N, nN) < 0.906) + continue; + + DIReservoir neighbor = UnpackDIReservoir(t_SrcDI.Load(int3(nPixel, 0))); + if (!IsDIReservoirValid(neighbor) || neighbor.lightIndex >= (uint)g_ClusterParams.w) + continue; + if (hudSurf && !IsHudLightDI(g_LightData[neighbor.lightIndex])) + continue; + + float targetN = EvalLocalLightTargetPdfDI( + g_LightData[neighbor.lightIndex], worldPos, N, V, albedo, metallic, roughness); + if (targetN <= 0) + continue; + + uint clampedM = min(neighbor.M, g_MMax); + float w = targetN * neighbor.W * clampedM; + DIReservoirUpdate(output, w, neighbor.lightIndex, targetN, rng); + output.M += clampedM - 1; + } + + float outPdf = output.targetPdf; + if (outPdf <= 0 && IsDIReservoirValid(output) && output.lightIndex < (uint)g_ClusterParams.w) { + outPdf = EvalLocalLightTargetPdfDI( + g_LightData[output.lightIndex], worldPos, N, V, albedo, metallic, roughness); + output.targetPdf = outPdf; + } + + if (outPdf > 0 && output.M > 0) + output.W = ClampDIReservoirW(output.w_sum / max(outPdf * (float)output.M, 1e-6)); + else + output = EmptyDIReservoir(); + + if (IsDIReservoirValid(output) && output.lightIndex < (uint)g_ClusterParams.w) { + GPULightDataDI light = g_LightData[output.lightIndex]; + float3 L, lightColor; + float dist; + float atten = EvalLocalLightAttenuationDI(light, worldPos, L, dist, lightColor); + if (atten <= 0.001) { + output.W = 0; + } else if (!IsHudLightDI(light) && asuint(light.spotParamsAndType.w) != 0xFFFFFFFFu) { + float3 biased = worldPos + N * 0.02; + float shadowDist = max(dist - 0.35, dist * 0.96); + if (TraceShadowDI(biased, L, shadowDist) <= 0.001) + output.W = 0; + } + } + + if (output.W <= 0) + output = EmptyDIReservoir(); + + output.M = min(output.M, g_MMax); + u_DIReservoir[pixel] = PackDIReservoir(output); +} diff --git a/res/gamedata/shaders/r5/restir_di_temporal.cs b/res/gamedata/shaders/r5/restir_di_temporal.cs new file mode 100644 index 00000000000..956236e3141 --- /dev/null +++ b/res/gamedata/shaders/r5/restir_di_temporal.cs @@ -0,0 +1,126 @@ +#include "bindless_common.h" +#include "rt_common.h" +#include "shared/pbr_brdf.h" +#include "shared/basecolor_pack.h" +#include "restir_gi_common.h" +#include "restir_di_common.h" +#include "restir_di_eval.h" +#include "shared/surface_marks.h" + +cbuffer ReSTIRDITemporalParams : register(b5) { + float4x4 g_WorldToView; + float4 g_CameraPos; + float2 g_ScreenSize; + float2 g_InvScreenSize; + uint g_FrameIndex; + uint g_MMax; + float2 g_Pad; + float4 g_ClusterParams; + float4 g_ClusterScales; +}; + +StructuredBuffer g_LightData : register(t20); +Texture2D t_PrevDI : register(t0); +Texture2D t_MotionVectors : register(t2); +Texture2D t_Depth : register(t3); +Texture2D t_PrevNormal : register(t5); +Texture2D t_BaseColor : register(t6); +Texture2D t_WorldPos : register(t7); +Texture2D t_PrevWorldPos : register(t11); +Texture2D t_Normal : register(t12); + +RWTexture2D u_DIReservoir : register(u0); + +[numthreads(8, 8, 1)] +void main(uint3 dispatchID : SV_DispatchThreadID) +{ + uint2 pixel = dispatchID.xy; + if (pixel.x >= (uint)g_ScreenSize.x || pixel.y >= (uint)g_ScreenSize.y) + return; + + float depth = t_Depth.Load(int3(pixel, 0)); + if (depth >= 1.0) { + u_DIReservoir[pixel] = PackDIReservoir(EmptyDIReservoir()); + return; + } + + DIReservoir curr = UnpackDIReservoir(u_DIReservoir[pixel]); + float4 worldPosData = t_WorldPos.Load(int3(pixel, 0)); + float3 worldPos = worldPosData.xyz; + const float centerMark = worldPosData.w; + const bool hudSurf = IsHudSurfMark(centerMark); + float4 normalData = t_Normal.Load(int3(pixel, 0)); + float3 N = normalize(normalData.xyz); + float roughness = max(normalData.w, MIN_ROUGHNESS); + float4 baseColorData = t_BaseColor.Load(int3(pixel, 0)); + float3 albedo = max(baseColorData.rgb, 0.0); + float metallic = UnpackMetallicFromBaseA(baseColorData.a); + float3 V = normalize(g_CameraPos.xyz - worldPos); + + uint rng = pcg_hash(pixel.x + pixel.y * 7919u + g_FrameIndex * 48611u); + DIReservoir output = EmptyDIReservoir(); + + float targetCurr = 0; + if (IsDIReservoirValid(curr) && curr.lightIndex < (uint)g_ClusterParams.w) { + if (!(hudSurf && !IsHudLightDI(g_LightData[curr.lightIndex]))) { + targetCurr = EvalLocalLightTargetPdfDI( + g_LightData[curr.lightIndex], worldPos, N, V, albedo, metallic, roughness); + if (targetCurr > 0) { + output.lightIndex = curr.lightIndex; + output.targetPdf = targetCurr; + output.w_sum = targetCurr * curr.W; + output.M = 1; + output.age = curr.age; + } + } + } + + float2 motion = t_MotionVectors.Load(int3(pixel, 0)); + float2 currUV = (float2(pixel) + 0.5) * g_InvScreenSize; + float2 prevUV = currUV + motion; + + if (all(prevUV >= 0) && all(prevUV < 1.0)) { + int2 prevPixel = int2(prevUV * g_ScreenSize); + float4 prevWorldPosData = t_PrevWorldPos.Load(int3(prevPixel, 0)); + float3 prevWorldPos = prevWorldPosData.xyz; + float3 prevN = normalize(t_PrevNormal.Load(int3(prevPixel, 0)).xyz); + float viewDist = length(worldPos - g_CameraPos.xyz); + float posDist = length(worldPos - prevWorldPos); + bool valid = SameHudSurfClass(centerMark, prevWorldPosData.w) && + posDist < 0.1 * viewDist && dot(N, prevN) > 0.906; + + if (valid) { + DIReservoir prev = UnpackDIReservoir(t_PrevDI.Load(int3(prevPixel, 0))); + if (IsDIReservoirValid(prev) && prev.lightIndex < (uint)g_ClusterParams.w && + !(hudSurf && !IsHudLightDI(g_LightData[prev.lightIndex]))) { + float targetPrev = EvalLocalLightTargetPdfDI( + g_LightData[prev.lightIndex], worldPos, N, V, albedo, metallic, roughness); + if (targetPrev > 0) { + uint clampedM = min(prev.M, g_MMax); + float wPrev = targetPrev * prev.W * clampedM; + DIReservoirUpdate(output, wPrev, prev.lightIndex, targetPrev, rng); + output.M += clampedM - 1; + output.age = prev.age + 1; + } + } + } + } + + float outPdf = output.targetPdf; + if (outPdf <= 0 && IsDIReservoirValid(output) && output.lightIndex < (uint)g_ClusterParams.w) { + outPdf = EvalLocalLightTargetPdfDI( + g_LightData[output.lightIndex], worldPos, N, V, albedo, metallic, roughness); + output.targetPdf = outPdf; + } + + if (outPdf > 0 && output.M > 0) + output.W = ClampDIReservoirW(output.w_sum / max(outPdf * (float)output.M, 1e-6)); + else + output = EmptyDIReservoir(); + + if (output.W <= 0) + output = EmptyDIReservoir(); + + output.M = min(output.M, g_MMax); + u_DIReservoir[pixel] = PackDIReservoir(output); +} diff --git a/res/gamedata/shaders/r5/restir_gi_blur.cs b/res/gamedata/shaders/r5/restir_gi_blur.cs new file mode 100644 index 00000000000..cee1ea40409 --- /dev/null +++ b/res/gamedata/shaders/r5/restir_gi_blur.cs @@ -0,0 +1,129 @@ +#include "shared/surface_marks.h" + +cbuffer BlurParams : register(b5) { + float2 g_ScreenSize; + float2 g_InvScreenSize; + float g_PhiNormal; + float g_PhiDepth; + uint g_Step; + uint g_Mode; +}; + +Texture2D t_DirectLighting : register(t0); +Texture2D t_NoisyDiffuse : register(t1); +Texture2D t_Depth : register(t2); +Texture2D t_Normal : register(t3); +Texture2D t_SceneColorIn : register(t4); +Texture2D t_NoisySpecular : register(t5); +Texture2D t_ClassifyWorldPos : register(t6); +Texture2D t_WorldPos : register(t7); + +RWTexture2D u_SceneColor : register(u0); +RWTexture2D u_FilteredDiffuse : register(u1); +RWTexture2D u_FilteredSpecular : register(u2); + +static const float kKernel[5] = { 0.0625, 0.25, 0.375, 0.25, 0.0625 }; + +float Luma(float3 c) +{ + return dot(c, float3(0.2126, 0.7152, 0.0722)); +} + +float3 SoftClampFirefly(float3 c, float centerLuma, float maxScale) +{ + float l = Luma(c); + float limit = max(centerLuma * maxScale, 0.05); + if (l > limit) + c *= limit / max(l, 1e-4); + return c; +} + +[numthreads(8, 8, 1)] +void main(uint3 dispatchID : SV_DispatchThreadID) +{ + uint2 pixel = dispatchID.xy; + if (pixel.x >= (uint)g_ScreenSize.x || pixel.y >= (uint)g_ScreenSize.y) + return; + + float depth = t_Depth.Load(int3(pixel, 0)); + float3 direct = t_DirectLighting.Load(int3(pixel, 0)).rgb; + float3 noisyD = t_NoisyDiffuse.Load(int3(pixel, 0)).rgb; + float4 noisyS4 = t_NoisySpecular.Load(int3(pixel, 0)); + float3 noisyS = noisyS4.rgb; + float specHitDist = noisyS4.a; + + float guideMark = t_WorldPos.Load(int3(pixel, 0)).w; + float classifyMark = t_ClassifyWorldPos.Load(int3(pixel, 0)).w; + if (depth >= 1.0 || SkipRtSurfLighting(classifyMark, guideMark)) { + u_SceneColor[pixel] = t_SceneColorIn.Load(int3(pixel, 0)); + u_FilteredDiffuse[pixel] = 0; + u_FilteredSpecular[pixel] = 0; + return; + } + + float3 N = normalize(t_Normal.Load(int3(pixel, 0)).xyz); + float centerLumaD = Luma(noisyD); + float centerLumaS = Luma(noisyS); + float directLuma = max(Luma(direct), 0.02); + int step = (int)max(g_Step, 1u); + + float3 sumD = 0; + float3 sumS = 0; + float wSumD = 0; + float wSumS = 0; + float3 nbMinD = noisyD; + float3 nbMaxD = noisyD; + float3 nbMinS = noisyS; + float3 nbMaxS = noisyS; + + [unroll] for (int iy = -2; iy <= 2; ++iy) { + [unroll] for (int ix = -2; ix <= 2; ++ix) { + int2 np = int2(pixel) + int2(ix, iy) * step; + if (np.x < 0 || np.y < 0 || np.x >= (int)g_ScreenSize.x || np.y >= (int)g_ScreenSize.y) + continue; + + float nd = t_Depth.Load(int3(np, 0)); + if (nd >= 1.0) + continue; + if (!SameHudSurfClass(guideMark, t_WorldPos.Load(int3(np, 0)).w)) + continue; + + float3 nN = normalize(t_Normal.Load(int3(np, 0)).xyz); + float3 nD = t_NoisyDiffuse.Load(int3(np, 0)).rgb; + float3 nS = t_NoisySpecular.Load(int3(np, 0)).rgb; + nD = SoftClampFirefly(nD, directLuma, 5.0); + nS = SoftClampFirefly(nS, directLuma, 6.5); + + nbMinD = min(nbMinD, nD); + nbMaxD = max(nbMaxD, nD); + nbMinS = min(nbMinS, nS); + nbMaxS = max(nbMaxS, nS); + + float w = kKernel[ix + 2] * kKernel[iy + 2]; + float wGeo = exp(-abs(depth - nd) * g_PhiDepth); + wGeo *= pow(saturate(dot(N, nN)), g_PhiNormal); + w *= max(wGeo, 0.05); + + float wD = w * exp(-abs(Luma(nD) - centerLumaD) * 1.25); + float wS = w * exp(-abs(Luma(nS) - centerLumaS) * 0.75); + sumD += nD * wD; + sumS += nS * wS; + wSumD += wD; + wSumS += wS; + } + } + + float3 filteredD = (wSumD > 1e-4) ? (sumD / wSumD) : SoftClampFirefly(noisyD, directLuma, 5.0); + float3 filteredS = (wSumS > 1e-4) ? (sumS / wSumS) : SoftClampFirefly(noisyS, directLuma, 6.5); + filteredD = clamp(filteredD, nbMinD * 0.5, nbMaxD * 1.5 + 0.01); + filteredS = clamp(filteredS, nbMinS * 0.35, nbMaxS * 1.75 + 0.01); + + u_FilteredDiffuse[pixel] = float4(filteredD, 1.0); + u_FilteredSpecular[pixel] = float4(filteredS, specHitDist); + + float3 ambientBase = t_SceneColorIn.Load(int3(pixel, 0)).rgb; + if (g_Mode == 1) + u_SceneColor[pixel] = float4(ambientBase + direct + filteredD + filteredS, 1.0); + else + u_SceneColor[pixel] = float4(ambientBase + direct + filteredD, 1.0); +} diff --git a/res/gamedata/shaders/r5/restir_gi_common.h b/res/gamedata/shaders/r5/restir_gi_common.h index fb09095e8f0..b299838117e 100644 --- a/res/gamedata/shaders/r5/restir_gi_common.h +++ b/res/gamedata/shaders/r5/restir_gi_common.h @@ -6,7 +6,7 @@ #endif static const uint RESTIR_INVALID_ID = 0xFFFFFFFF; -static const float RESTIR_MAX_RADIANCE = 100.0; +static const float RESTIR_MAX_RADIANCE = 48.0; static const uint RESTIR_M_MAX = 20; struct GIReservoir @@ -121,6 +121,31 @@ GIReservoir UnpackReservoir(float4 A, float4 B) return r; } +GIReservoir UnpackReservoir(float4 A, float4 B, float2 C) +{ + GIReservoir r = UnpackReservoir(A, B); + r.w_sum = C.x; + return r; +} + +float2 PackReservoirC(GIReservoir r) +{ + return float2(r.w_sum, asfloat(r.M)); +} + +float AgeConfidence(uint age, uint softMax) +{ + float a = (float)age; + float s = (float)max(softMax, 1u); + return saturate(1.0 - (a / (s * 2.0))); +} + +uint TemporalMClamp(uint M, uint age, uint mMax) +{ + uint ageCut = age > 24u ? (mMax / 2u) : mMax; + return min(M, max(ageCut, 1u)); +} + // Jacobian of reconnection shift: reusing sample from pixel q at pixel r // x1_new = primary surface at destination pixel (r) // x1_old = primary surface at source pixel (q) diff --git a/res/gamedata/shaders/r5/restir_gi_composite.cs b/res/gamedata/shaders/r5/restir_gi_composite.cs index 881889ced16..2c0057affbc 100644 --- a/res/gamedata/shaders/r5/restir_gi_composite.cs +++ b/res/gamedata/shaders/r5/restir_gi_composite.cs @@ -1,6 +1,8 @@ -#include "common.h" #include "rt_common.h" #include "shared/pbr_brdf.h" +#include "shared/basecolor_pack.h" +#include "shared/nrd_helpers.h" +#include "shared/surface_marks.h" #include "restir_gi_common.h" cbuffer CompositeParams : register(b5) { @@ -9,6 +11,8 @@ float2 g_ScreenSize; float g_GIIntensity; uint g_Pad; + float4 g_FogParams; + float4 g_FogColor; }; Texture2D t_DirectLighting : register(t0); @@ -19,8 +23,14 @@ Texture2D t_SceneColorIn : register(t6); Texture2D t_WorldPos : register(t7); Texture2D t_Normal : register(t8); +Texture2D t_NoisySpecular : register(t9); +Texture2D t_ClassifyWorldPos : register(t10); +Texture2D t_SpecReservoirA : register(t11); +Texture2D t_SpecReservoirB : register(t12); RWTexture2D u_SceneColor : register(u0); +RWTexture2D u_NoisyDiffuse : register(u1); +RWTexture2D u_HitDist : register(u2); [numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) @@ -32,38 +42,84 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float depth = t_Depth.Load(int3(pixel, 0)); if (depth >= 1.0) { u_SceneColor[pixel] = t_SceneColorIn.Load(int3(pixel, 0)); + u_NoisyDiffuse[pixel] = 0; + u_HitDist[pixel] = 0; + return; + } + + float guideMark = t_WorldPos.Load(int3(pixel, 0)).w; + float classifyMark = t_ClassifyWorldPos.Load(int3(pixel, 0)).w; + if (IsWaterSurfMark(classifyMark) && !IsHudSurfMark(guideMark)) { + u_SceneColor[pixel] = t_SceneColorIn.Load(int3(pixel, 0)); + u_NoisyDiffuse[pixel] = 0; + u_HitDist[pixel] = 0; return; } float3 direct = t_DirectLighting.Load(int3(pixel, 0)).rgb; + float4 specSample = t_NoisySpecular.Load(int3(pixel, 0)); GIReservoir r = UnpackReservoir( t_ReservoirA.Load(int3(pixel, 0)), t_ReservoirB.Load(int3(pixel, 0)) ); + GIReservoir specR = UnpackReservoir( + t_SpecReservoirA.Load(int3(pixel, 0)), + t_SpecReservoirB.Load(int3(pixel, 0)) + ); - float3 indirect = 0; - if (IsReservoirValid(r) && r.W > 0) { - float3 worldPos = t_WorldPos.Load(int3(pixel, 0)).xyz; - float4 normalData = t_Normal.Load(int3(pixel, 0)); - float4 baseColorData = t_BaseColor.Load(int3(pixel, 0)); - - float3 N = normalize(normalData.xyz); - float roughness = normalData.w; - float3 albedo = baseColorData.rgb; - float metallic = baseColorData.a; + float3 worldPos = t_WorldPos.Load(int3(pixel, 0)).xyz; + float4 normalData = t_Normal.Load(int3(pixel, 0)); + float4 baseColorData = t_BaseColor.Load(int3(pixel, 0)); + float3 N = normalize(normalData.xyz); + float roughness = max(normalData.w, MIN_ROUGHNESS); + float3 albedo = max(baseColorData.rgb, 0.0); + float metallic = UnpackMetallicFromBaseA(baseColorData.a); + float3 F0 = CalculateF0(albedo, metallic); + float3 V = normalize(g_CameraPos.xyz - worldPos); + float3 Fenv = NRD_EnvironmentTerm_Rtg(F0, abs(dot(N, V)), roughness); + float3 diffIrradiance = 0; + float hitDist = 0; + if (IsReservoirValid(r) && r.W > 0 && metallic < 0.999) { float3 wi = normalize(r.samplePos - worldPos); float cosTheta = max(dot(N, wi), 0); - float3 F0 = CalculateF0(albedo, metallic); float3 kD = (1.0 - F_Schlick(cosTheta, F0)) * (1.0 - metallic); float3 brdfCos = kD * albedo / PI * cosTheta; - indirect = r.Lo * brdfCos * r.W; - indirect = min(indirect, RESTIR_MAX_RADIANCE); - indirect *= g_GIIntensity; + diffIrradiance = r.Lo * brdfCos * r.W; + diffIrradiance = min(diffIrradiance, RESTIR_MAX_RADIANCE); + hitDist = length(r.samplePos - worldPos); } + if (hitDist < 1e-3) + hitDist = 0; + diffIrradiance *= g_GIIntensity; + + float3 specIrradiance = max(specSample.rgb, 0.0) * g_GIIntensity; + if (IsReservoirValid(specR) && specR.W > 0) { + float3 fromRes = min(specR.Lo * Fenv * specR.W, RESTIR_MAX_RADIANCE) * g_GIIntensity; + specIrradiance = lerp(specIrradiance, fromRes, 0.85); + if (hitDist < 1e-3) + hitDist = length(specR.samplePos - worldPos); + } + if (specSample.a > 1e-3 && hitDist < 1e-3) + hitDist = specSample.a; + + diffIrradiance = min(diffIrradiance, RESTIR_MAX_RADIANCE); + specIrradiance = min(specIrradiance, RESTIR_MAX_RADIANCE); + + float giAo = 1.0; + if (hitDist > 0.35 && hitDist < 3.0) + giAo = saturate(0.72 + hitDist * 0.09); + + u_NoisyDiffuse[pixel] = float4(diffIrradiance * giAo, 1.0); + u_HitDist[pixel] = hitDist; - float3 finalColor = direct + indirect; - u_SceneColor[pixel] = float4(finalColor, 1.0); + float3 ambientBase = t_SceneColorIn.Load(int3(pixel, 0)).rgb; + float3 lit = direct + (diffIrradiance + specIrradiance) * giAo; + float dist = length(worldPos - g_CameraPos.xyz); + float fog = saturate(dist * g_FogParams.w + g_FogParams.x); + fog = saturate(fog * lerp(1.0, 1.12, saturate(g_GIIntensity))); + lit *= (1.0 - fog); + u_SceneColor[pixel] = float4(ambientBase + lit, 1.0); } diff --git a/res/gamedata/shaders/r5/restir_gi_initial.cs b/res/gamedata/shaders/r5/restir_gi_initial.cs index 6ff2620abc5..ce8f7aee6dd 100644 --- a/res/gamedata/shaders/r5/restir_gi_initial.cs +++ b/res/gamedata/shaders/r5/restir_gi_initial.cs @@ -1,11 +1,22 @@ #include "bindless_common.h" #include "rt_common.h" +#include "rt_visibility.h" +#include "rt_shade_hit.h" +#include "rt_irradiance_cache.h" #include "shared/pbr_brdf.h" +#include "shared/basecolor_pack.h" +#include "shared/nrd_helpers.h" +#include "shared/foliage_sss.h" +#include "shared/skin_sss.h" +#include "shared/surface_marks.h" #include "restir_gi_common.h" +#include "restir_di_common.h" +#include "restir_di_eval.h" cbuffer ReSTIRGIParams : register(b5) { float4x4 g_InvViewProj; float4x4 g_PrevViewProj; + float4x4 g_WorldToView; float4 g_CameraPos; float4 g_SunDir_Intensity; float4 g_SunColor_SkyWeight; @@ -17,13 +28,22 @@ uint g_SkinnedBatchStart; uint g_GrassBatchStart; uint g_DetailAtlasIndex; - uint3 g_Pad; + uint g_LocalLightSamples; + uint g_DICandidates; + uint g_Bounces; + float4 g_DISampleParams; + float4 g_ClusterParams; + float4 g_ClusterScales; + float4 g_HemiColor; + uint g_CacheSize; + float g_CacheCellSize; + uint g_CacheMaxAge; + uint g_Pad1; }; RaytracingAccelerationStructure g_SceneTLAS : register(t1); StructuredBuffer g_BatchInfo : register(t2); ByteAddressBuffer g_MegaVB : register(t3); -ByteAddressBuffer g_MegaIB : register(t4); TextureCube g_Sky0 : register(t5); TextureCube g_Sky1 : register(t6); ByteAddressBuffer g_SkinnedVB : register(t7); @@ -34,22 +54,31 @@ Texture2D t_Normal : register(t15); Texture2D t_BaseColor : register(t16); Texture2D t_WorldPos : register(t17); +ByteAddressBuffer g_MegaIB : register(t18); + +StructuredBuffer g_LightData : register(t20); +StructuredBuffer g_DILightIndices : register(t21); +StructuredBuffer g_DILightCDF : register(t22); RWTexture2D u_DirectLighting : register(u0); RWTexture2D u_ReservoirA : register(u1); RWTexture2D u_ReservoirB : register(u2); - -static const uint MAX_SHADOW_SKIPS = 8; +RWTexture2D u_NoisySpecular : register(u3); +RWTexture2D u_DIReservoir : register(u4); +RWTexture2D u_SpecReservoirA : register(u5); +RWTexture2D u_SpecReservoirB : register(u6); +RWTexture2D u_ReservoirC : register(u7); +RWStructuredBuffer u_IrradianceCache : register(u8); bool IsSkinnedBatch(uint batchIdx) { - return g_SkinnedBatchStart > 0 && batchIdx >= g_SkinnedBatchStart && - !(g_GrassBatchStart > 0 && batchIdx >= g_GrassBatchStart); + return g_SkinnedBatchStart != 0xFFFFFFFFu && batchIdx >= g_SkinnedBatchStart && + (g_GrassBatchStart == 0xFFFFFFFFu || batchIdx < g_GrassBatchStart); } bool IsGrassBatch(uint batchIdx) { - return g_GrassBatchStart > 0 && batchIdx >= g_GrassBatchStart; + return g_GrassBatchStart != 0xFFFFFFFFu && batchIdx >= g_GrassBatchStart; } bool IsTerrainBatch(uint batchIdx) @@ -66,96 +95,164 @@ float3 SampleSky(float3 dir) return lerp(s0, s1, w); } -float TraceShadow(float3 origin, float3 sunDir) +float TraceShadow(float3 origin, float3 dir, float tMax, bool nearSkinnedOccludes) { - float atten = 1.0; - float3 shadowOrigin = origin; + return TraceVisibilityAtten( + g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, + origin, dir, tMax, + g_IdentityStaticCount, g_TerrainBatchCount, g_SkinnedBatchStart, g_GrassBatchStart, + g_DetailAtlasIndex, nearSkinnedOccludes); +} - for (uint si = 0; si < MAX_SHADOW_SKIPS; si++) { - RayDesc ray; - ray.Origin = shadowOrigin; - ray.Direction = sunDir; - ray.TMin = 0.001; - ray.TMax = 10000.0; +float PointLightAttenuationRT(float distSq, float invRangeSq) +{ + float factor = saturate(1.0 - distSq * abs(invRangeSq)); + return factor * factor; +} - RayQuery q; - q.TraceRayInline(g_SceneTLAS, RAY_FLAG_NONE, 0xFF, ray); - while (q.Proceed()) { - if (q.CandidateType() == CANDIDATE_NON_OPAQUE_TRIANGLE) { - uint candBatch = q.CandidateInstanceID() + q.CandidateGeometryIndex(); - if (IsGrassBatch(candBatch) && g_DetailAtlasIndex > 0) { - RTBatchInfo candInfo = g_BatchInfo[candBatch]; - float2 candUV = GetSkinnedHitUV(g_GrassVB, g_GrassIB, candInfo, - q.CandidatePrimitiveIndex(), q.CandidateTriangleBarycentrics()); - float4 texel = GetBindlessTexture(g_DetailAtlasIndex).SampleLevel(smp_linear, candUV, 0); - if (texel.a >= 0.3) - q.CommitNonOpaqueTriangleHit(); - } - } - } +float SpotLightAttenuationRT(float3 toLight, float3 spotDir, float scale, float offset) +{ + float cosAngle = dot(normalize(-toLight), spotDir); + return saturate(cosAngle * scale + offset); +} - if (q.CommittedStatus() != COMMITTED_TRIANGLE_HIT) - break; +float LinearizeDepthRT(float ndcDepth, float zNear, float zFar) +{ + return zNear * zFar / (zFar - ndcDepth * (zFar - zNear)); +} - uint sBatchIdx = q.CommittedInstanceID() + q.CommittedGeometryIndex(); - RTBatchInfo sInfo = g_BatchInfo[sBatchIdx]; +uint SampleDILightIS(float u, out float lightPdf) +{ + uint count = (uint)g_DISampleParams.x; + float powerSum = g_DISampleParams.y; + lightPdf = 0; + if (count == 0 || powerSum <= 1e-8) + return 0; + + float target = u * powerSum; + uint lo = 0; + uint hi = count; + while (lo < hi) { + uint mid = (lo + hi) >> 1; + if (g_DILightCDF[mid] < target) + lo = mid + 1; + else + hi = mid; + } + uint i = min(lo, count - 1); + float prev = (i == 0) ? 0.0 : g_DILightCDF[i - 1]; + float w = max(g_DILightCDF[i] - prev, 1e-8); + lightPdf = w / powerSum; + return g_DILightIndices[i]; +} - if (IsGrassBatch(sBatchIdx)) { - if (g_DetailAtlasIndex > 0) { atten = 0; break; } - atten *= 0.5; - shadowOrigin = shadowOrigin + sunDir * (q.CommittedRayT() + 0.002); - continue; +float3 EvaluateLocalLightsRT( + float3 worldPos, float3 N, float3 V, float3 albedo, float metallic, float roughness, + float2 pixelPos, float linearDepth, uint maxSamples, inout uint rng) +{ + uint diCount = (uint)g_DISampleParams.x; + if (diCount == 0 || maxSamples == 0 || g_DISampleParams.y <= 1e-8) + return 0; + + uint sampleCount = min(diCount, maxSamples); + float3 total = 0; + float3 biasedPos = worldPos + N * 0.02; + + bool exhaustive = (sampleCount == diCount); + for (uint i = 0; i < sampleCount; i++) { + float lightPdf = 1.0; + uint lightIdx; + if (exhaustive) { + lightIdx = g_DILightIndices[i]; + } else { + lightIdx = SampleDILightIS(rand_float(rng), lightPdf); + if (lightPdf <= 1e-8) + continue; } - - MaterialData sMat = g_Materials[sInfo.materialID]; - - if (sMat.flags & MAT_FLAG_WATER) { - atten *= 0.85; - shadowOrigin = shadowOrigin + sunDir * (q.CommittedRayT() + 0.002); + GPULightDataDI light = g_LightData[lightIdx]; + float3 L, lightColor; + float dist; + float atten = EvalLocalLightAttenuationDI(light, worldPos, L, dist, lightColor); + if (atten <= 0.001) continue; - } - if (IsTerrainBatch(sBatchIdx) || IsSkinnedBatch(sBatchIdx)) { atten = 0; break; } + float shadow = 1.0; + if (asuint(light.spotParamsAndType.w) != 0xFFFFFFFFu) { + float endSkip = max(0.55, LightEmitterRadiusDI(light) * 3.0 + 0.25); + float shadowDist = max(dist - endSkip, dist * 0.88); + if (shadowDist > 0.02) + shadow = TraceShadow(biasedPos, L, shadowDist, false); + } + if (shadow <= 0.001) + continue; - float2 sUV; - if (IsSkinnedBatch(sBatchIdx)) - sUV = GetSkinnedHitUV(g_SkinnedVB, g_SkinnedIB, sInfo, q.CommittedPrimitiveIndex(), q.CommittedTriangleBarycentrics()); - else - sUV = GetHitUV(g_MegaVB, g_MegaIB, sInfo, q.CommittedPrimitiveIndex(), q.CommittedTriangleBarycentrics()); + float3 lit = PBRDirectLighting(albedo, N, V, L, lightColor * atten * shadow, metallic, roughness, 1); + total += exhaustive ? lit : (lit / (lightPdf * (float)sampleCount)); + } - float4 sDiffuse = SampleDiffuseLevel(sMat, sUV); + return total; +} - if ((sMat.flags & MAT_FLAG_ALPHA_TEST) && sDiffuse.a < sMat.alphaRef) { - shadowOrigin = shadowOrigin + sunDir * (q.CommittedRayT() + 0.002); - continue; +DIReservoir SampleDIReservoir( + float3 worldPos, float3 N, float3 V, float3 albedo, float metallic, float roughness, + float2 pixelPos, float linearDepth, uint candidates, bool hudSurface, inout uint rng) +{ + DIReservoir r = EmptyDIReservoir(); + uint diCount = (uint)g_DISampleParams.x; + if (diCount == 0 || candidates == 0 || g_DISampleParams.y <= 1e-8) + return r; + + uint sampleCount = min(diCount, candidates); + for (uint i = 0; i < sampleCount; i++) { + float lightPdf = 0; + uint lightIdx; + if (sampleCount == diCount) { + lightIdx = g_DILightIndices[i]; + float prev = (i == 0) ? 0.0 : g_DILightCDF[i - 1]; + lightPdf = max(g_DILightCDF[i] - prev, 1e-8) / g_DISampleParams.y; + } else { + lightIdx = SampleDILightIS(rand_float(rng), lightPdf); } - if ((sMat.flags & MAT_FLAG_ALPHA_BLEND) && sDiffuse.a < 0.5) { - atten *= (1.0 - sDiffuse.a); - shadowOrigin = shadowOrigin + sunDir * (q.CommittedRayT() + 0.002); + if (hudSurface && !IsHudLightDI(g_LightData[lightIdx])) continue; - } - - atten = 0; - break; + float pdf = EvalLocalLightTargetPdfDI( + g_LightData[lightIdx], worldPos, N, V, albedo, metallic, roughness); + float w = (lightPdf > 1e-8) ? (pdf / lightPdf) : 0.0; + DIReservoirUpdate(r, w, lightIdx, pdf, rng); } - return atten; + + if (r.M > 0 && r.targetPdf > 0 && r.lightIndex != RESTIR_INVALID_ID) { + r.W = ClampDIReservoirW(r.w_sum / max(r.targetPdf * (float)r.M, 1e-6)); + if (r.W <= 0) + r = EmptyDIReservoir(); + } else + r = EmptyDIReservoir(); + return r; } +float3 SampleTerrainAlbedo(TerrainMaterialData mat, float2 uv); + struct BounceHit { float3 position; float3 normal; float3 geoNormal; float3 albedo; + float3 baked; + float3 emissive; float metallic; float roughness; + float hemi; float t; bool valid; }; -BounceHit TraceBounce(float3 origin, float3 direction, inout uint rng) +BounceHit TraceBounce(float3 origin, float3 direction, inout uint rng, bool skipNearSkinned) { BounceHit result; result.valid = false; + result.baked = 0; + result.emissive = 0; + result.hemi = 0; float3 rayOrigin = origin; for (uint skip = 0; skip < 4; skip++) { @@ -192,16 +289,24 @@ BounceHit TraceBounce(float3 origin, float3 direction, inout uint rng) float3 hitN, geoN; float2 hitUV; + float2 lmUV = 0; + float hemi = 0.5; if (IsGrassBatch(batchIdx)) { hitUV = GetSkinnedHitUV(g_GrassVB, g_GrassIB, info, primIdx, bary); hitN = GetSkinnedHitNormal(g_GrassVB, g_GrassIB, info, primIdx, bary); geoN = GetSkinnedHitGeoNormal(g_GrassVB, g_GrassIB, info, primIdx); } else if (IsSkinnedBatch(batchIdx)) { + if (skipNearSkinned && q.CommittedRayT() < 0.45) { + rayOrigin = rayOrigin + direction * (q.CommittedRayT() + 0.002); + continue; + } hitUV = GetSkinnedHitUV(g_SkinnedVB, g_SkinnedIB, info, primIdx, bary); hitN = GetSkinnedHitNormal(g_SkinnedVB, g_SkinnedIB, info, primIdx, bary); geoN = GetSkinnedHitGeoNormal(g_SkinnedVB, g_SkinnedIB, info, primIdx); } else { hitUV = GetHitUV(g_MegaVB, g_MegaIB, info, primIdx, bary); + lmUV = GetHitLightmapUV(g_MegaVB, g_MegaIB, info, primIdx, bary); + hemi = GetHitHemi(g_MegaVB, g_MegaIB, info, primIdx, bary); hitN = TransformNormalToWorld(GetHitNormal(g_MegaVB, g_MegaIB, info, primIdx, bary), objectToWorld); geoN = TransformNormalToWorld(GetHitGeometricNormal(g_MegaVB, g_MegaIB, info, primIdx), objectToWorld); } @@ -212,6 +317,8 @@ BounceHit TraceBounce(float3 origin, float3 direction, inout uint rng) float3 albedo = float3(0.5, 0.5, 0.5); float metallic = 0; float roughness = 1.0; + float3 baked = 0; + float3 emissive = 0; if (IsGrassBatch(batchIdx)) { if (g_DetailAtlasIndex > 0) { @@ -224,11 +331,17 @@ BounceHit TraceBounce(float3 origin, float3 direction, inout uint rng) } else { albedo = lerp(float3(0.08, 0.18, 0.03), float3(0.15, 0.35, 0.06), 1.0 - hitUV.y); } + baked = g_HemiColor.rgb * albedo * 0.35; } else if (IsTerrainBatch(batchIdx)) { TerrainMaterialData tmat = g_TerrainMaterials[info.materialID]; albedo = SampleTerrainAlbedo(tmat, hitUV); + baked = ShadeBakedFromTerrainLmap(tmat, lmUV, albedo, g_HemiColor.rgb, hemi); } else { MaterialData mat = g_Materials[info.materialID]; + if (skipNearSkinned && (mat.flags & MAT_FLAG_WATER)) { + rayOrigin = rayOrigin + direction * (q.CommittedRayT() + 0.002); + continue; + } float4 diffuse = SampleDiffuseLevel(mat, hitUV); albedo = diffuse.rgb; @@ -242,14 +355,20 @@ BounceHit TraceBounce(float3 origin, float3 direction, inout uint rng) metallic = pbr.r; roughness = pbr.g; } + baked = ShadeBakedFromHemi(hemi, albedo, g_HemiColor.rgb); + if ((mat.flags & MAT_FLAG_EMISSIVE) && mat.emissiveIntensity > 0.0) + emissive = albedo * mat.emissiveIntensity; } result.position = rayOrigin + direction * q.CommittedRayT(); result.normal = hitN; result.geoNormal = geoN; result.albedo = albedo; + result.baked = baked; + result.emissive = emissive; result.metallic = metallic; result.roughness = roughness; + result.hemi = hemi; result.t = q.CommittedRayT(); result.valid = true; return result; @@ -280,6 +399,65 @@ float3 SampleTerrainAlbedo(TerrainMaterialData mat, float2 uv) return baseSample.rgb * blendedDetail * 2.0; } +float3 ImportanceSampleGGX(float2 Xi, float3 N, float roughness) +{ + float a = max(roughness * roughness, 0.001); + float phi = 2.0 * PI * Xi.x; + float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y)); + float sinTheta = sqrt(max(1.0 - cosTheta * cosTheta, 0.0)); + + float3 H; + H.x = cos(phi) * sinTheta; + H.y = sin(phi) * sinTheta; + H.z = cosTheta; + + float3 up = abs(N.z) < 0.999 ? float3(0, 0, 1) : float3(1, 0, 0); + float3 tangent = normalize(cross(up, N)); + float3 bitangent = cross(N, tangent); + return normalize(tangent * H.x + bitangent * H.y + N * H.z); +} + +float3 EvaluateSecondaryLo( + BounceHit h, + float3 primaryPos, + float3 sunDir, + float3 sunColor, + float2 pixelPos, + inout uint rngState) +{ + float3 hitBiased = h.position + h.geoNormal * 0.005; + float3 hitV = normalize(primaryPos - h.position); + float hitShadow = TraceShadow(hitBiased, sunDir, 10000.0, false); + float3 Lo = ShadeHitDirect(h.albedo, h.normal, hitV, h.metallic, h.roughness, sunDir, sunColor, hitShadow, h.baked); + Lo += h.emissive; + float hitLinDepth = abs(mul(g_WorldToView, float4(h.position, 1.0)).z); + Lo += EvaluateLocalLightsRT(h.position, h.normal, hitV, h.albedo, h.metallic, h.roughness, + pixelPos, hitLinDepth, min(g_LocalLightSamples, 4u), rngState); + if (g_Bounces >= 2u && rand_float(rngState) < 0.75) { + float2 u2 = float2(rand_float(rngState), rand_float(rngState)); + float3 dir2 = cosine_weighted_hemisphere(u2, h.normal); + BounceHit h2 = TraceBounce(hitBiased, dir2, rngState, false); + if (h2.valid) { + float3 hit2Biased = h2.position + h2.geoNormal * 0.005; + float3 hit2V = normalize(h.position - h2.position); + float sh2 = TraceShadow(hit2Biased, sunDir, 10000.0, false); + float3 Lo2 = ShadeHitDirect(h2.albedo, h2.normal, hit2V, h2.metallic, h2.roughness, sunDir, sunColor, sh2, h2.baked); + Lo2 += h2.emissive; + Lo2 += EvaluateLocalLightsRT(h2.position, h2.normal, hit2V, h2.albedo, h2.metallic, h2.roughness, + pixelPos, abs(mul(g_WorldToView, float4(h2.position, 1.0)).z), min(g_LocalLightSamples, 2u), rngState); + Lo += Lo2 * h2.albedo * (1.0 / 0.75) * 0.55; + UpdateIrradianceCache(u_IrradianceCache, h2.position, Lo2, g_CacheCellSize, g_CacheSize, g_FrameIndex); + } else { + Lo += SampleSky(dir2) * (1.0 / 0.75) * 0.45; + } + } + float3 cached = QueryIrradianceCacheRW(u_IrradianceCache, h.position, g_CacheCellSize, g_CacheSize, g_FrameIndex, g_CacheMaxAge); + if (Luminance(Lo) < 0.02 && any(cached > 0)) + Lo = max(Lo, cached * h.albedo); + UpdateIrradianceCache(u_IrradianceCache, h.position, Lo, g_CacheCellSize, g_CacheSize, g_FrameIndex); + return min(Lo, RESTIR_MAX_RADIANCE); +} + [numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) { @@ -288,83 +466,170 @@ void main(uint3 dispatchID : SV_DispatchThreadID) return; float depth = t_Depth.Load(int3(pixel, 0)); + float4 worldPosData = t_WorldPos.Load(int3(pixel, 0)); if (depth >= 1.0) { u_DirectLighting[pixel] = 0; u_ReservoirA[pixel] = 0; u_ReservoirB[pixel] = 0; + u_NoisySpecular[pixel] = 0; + u_DIReservoir[pixel] = PackDIReservoir(EmptyDIReservoir()); + u_SpecReservoirA[pixel] = 0; + u_SpecReservoirB[pixel] = 0; + u_ReservoirC[pixel] = 0; return; } - float3 worldPos = t_WorldPos.Load(int3(pixel, 0)).xyz; + const bool isHud = IsHudSurfMark(worldPosData.w); + float3 worldPos = worldPosData.xyz; float4 normalData = t_Normal.Load(int3(pixel, 0)); float4 baseColorData = t_BaseColor.Load(int3(pixel, 0)); float3 N = normalize(normalData.xyz); - float roughness = normalData.w; + float roughness = max(normalData.w, MIN_ROUGHNESS); float3 albedo = baseColorData.rgb; - float metallic = baseColorData.a; + float metallic = UnpackMetallicFromBaseA(baseColorData.a); float3 V = normalize(g_CameraPos.xyz - worldPos); float3 sunDir = normalize(-g_SunDir_Intensity.xyz); float sunIntensity = g_SunDir_Intensity.w; float3 sunColor = g_SunColor_SkyWeight.xyz * sunIntensity; - float3 biasedPos = worldPos + N * 0.01; + float3 biasedPos = worldPos + N * (isHud ? 0.03 : 0.01); + float linearDepth = abs(mul(g_WorldToView, float4(worldPos, 1.0)).z); + float2 pixelPos = float2(pixel) + 0.5; - // === DIRECT LIGHTING (shadow ray to sun) === - float shadow = TraceShadow(biasedPos, sunDir); + float shadow = TraceShadow(biasedPos, sunDir, 10000.0, isHud); float3 direct = 0; - if (shadow > 0.001) { - float NdotL = max(0.0, dot(N, sunDir)); - direct = albedo * NdotL * sunColor * shadow; + if (shadow > 0.001) + direct = PBRDirectLighting(albedo, N, V, sunDir, sunColor * shadow, metallic, roughness, 1); + + float sssMask = UnpackSSSMaskFromBaseA(baseColorData.a); + if (!isHud && sssMask > 0.01) { + float3 sssTint = SkinSSSTint(); + float sssThickness = saturate(0.55 + sssMask * 0.35); + direct += EvaluateFoliageSSS( + albedo, N, V, sunDir, sunColor, max(shadow, 0.35), + sssTint, sssThickness, sssMask); } - // === INDIRECT LIGHTING (1 cosine bounce + NEE) === uint rng = pcg_hash(pixel.x + pixel.y * 1973u + g_FrameIndex * 26699u); - - float2 u = float2(rand_float(rng), rand_float(rng)); - float3 bounceDir = cosine_weighted_hemisphere(u, N); - float cosPDF = max(dot(bounceDir, N), 0) / PI; - - BounceHit hit = TraceBounce(biasedPos, bounceDir, rng); + DIReservoir diRes = SampleDIReservoir( + worldPos, N, V, albedo, metallic, roughness, + pixelPos, linearDepth, max(g_DICandidates, 1u), isHud, rng); + u_DIReservoir[pixel] = PackDIReservoir(diRes); GIReservoir reservoir = EmptyReservoir(); - - if (!hit.valid) { - direct += albedo * SampleSky(bounceDir); - } - + GIReservoir specReservoir = EmptyReservoir(); u_DirectLighting[pixel] = float4(direct, 1.0); - if (hit.valid) { - float3 hitBiased = hit.position + hit.geoNormal * 0.005; - float3 hitV = normalize(worldPos - hit.position); - - float hitShadow = TraceShadow(hitBiased, sunDir); - float3 secondaryDirect = 0; - if (hitShadow > 0.001) { - float hitNdotL = max(0.0, dot(hit.normal, sunDir)); - secondaryDirect = hit.albedo * hitNdotL * sunColor * hitShadow; + float3 F0primary = CalculateF0(albedo, metallic); + float3 kDprimary = (1.0 - metallic); + bool diffLobeActive = any(kDprimary * albedo > 1e-4); + + if (diffLobeActive) { + float2 u = float2(rand_float(rng), rand_float(rng)); + float3 bounceDir = cosine_weighted_hemisphere(u, N); + float cosPDF = max(dot(bounceDir, N), 0) / PI; + BounceHit hit = TraceBounce(biasedPos, bounceDir, rng, isHud); + + float3 LoSurf = 0; + float3 LoSky = min(SampleSky(bounceDir), RESTIR_MAX_RADIANCE); + float3 samplePos = worldPos + bounceDir * g_ClusterScales.y; + float3 sampleN = -bounceDir; + bool usedSurf = false; + + if (hit.valid && cosPDF > 1e-6) { + LoSurf = EvaluateSecondaryLo(hit, worldPos, sunDir, sunColor, pixelPos, rng); + if (isHud) + LoSurf = min(LoSurf, sunColor * 0.35 + 0.08); + samplePos = hit.position; + sampleN = hit.normal; + usedSurf = true; + } else if (isHud) { + LoSky = min(LoSky, sunColor * 0.25 + 0.06); } - float3 Lo = secondaryDirect; - Lo = min(Lo, RESTIR_MAX_RADIANCE); - float3 wi = normalize(hit.position - worldPos); + float3 Lo = usedSurf ? LoSurf : LoSky; + float3 wi = normalize(samplePos - worldPos); float cosTheta = max(dot(N, wi), 0); - float3 F0 = CalculateF0(albedo, metallic); - float3 kD = (1.0 - F_Schlick(cosTheta, F0)) * (1.0 - metallic); + float3 kD = (1.0 - F_Schlick(cosTheta, F0primary)) * (1.0 - metallic); float3 brdfCos = kD * albedo / PI * cosTheta; - float3 target = Lo * brdfCos; - float targetLum = Luminance(target); - + float targetLum = Luminance(Lo * brdfCos); if (targetLum > 0 && cosPDF > 1e-6) { - float w = targetLum / cosPDF; - ReservoirUpdate(reservoir, w, hit.position, hit.normal, Lo, rng); - reservoir.W = 1.0 / cosPDF; + float mis = 1.0; + if (usedSurf && Luminance(LoSky) > 1e-4) { + float pSurf = cosPDF; + float pSky = 1.0 / (2.0 * PI); + mis = pSurf / max(pSurf + pSky, 1e-6); + } + float w = (targetLum / cosPDF) * mis; + ReservoirUpdate(reservoir, w, samplePos, sampleN, Lo, rng); + float pHat = max(targetLum, 1e-6); + reservoir.W = (reservoir.M > 0) ? reservoir.w_sum / (pHat * (float)reservoir.M) : 0; + reservoir.age = 0; + } + } + + float3 R; + if (roughness < 0.08) { + R = reflect(-V, N); + } else { + float2 uSpec = float2(rand_float(rng), rand_float(rng)); + float3 H = ImportanceSampleGGX(uSpec, N, roughness); + R = normalize(2.0 * max(dot(V, H), 0.0) * H - V); + if (dot(R, N) <= 0.0) + R = reflect(-V, N); + } + + float3 specular = 0; + float specHitDist = 0; + float3 Fenv = NRD_EnvironmentTerm_Rtg(F0primary, abs(dot(N, V)), roughness); + + if (dot(R, N) > 0.0) { + BounceHit specHit = TraceBounce(biasedPos, R, rng, isHud); + float3 Lo = 0; + float3 samplePos = worldPos + R * g_ClusterScales.y; + float3 sampleN = -R; + if (specHit.valid) { + Lo = EvaluateSecondaryLo(specHit, worldPos, sunDir, sunColor, pixelPos, rng); + if (isHud) + Lo = min(Lo, sunColor * 0.3 + 0.05); + samplePos = specHit.position; + sampleN = specHit.normal; + specHitDist = length(specHit.position - worldPos); + if (specHitDist < 1e-3) + specHitDist = 0; + } else { + Lo = min(SampleSky(R), RESTIR_MAX_RADIANCE); + if (isHud) + Lo = min(Lo, sunColor * 0.2 + 0.04); + specHitDist = g_ClusterScales.y; + } + specular = min(Lo, RESTIR_MAX_RADIANCE) * Fenv; + float targetLum = Luminance(specular); + if (targetLum > 0) { + float w = targetLum; + ReservoirUpdate(specReservoir, w, samplePos, sampleN, Lo, rng); + specReservoir.W = (specReservoir.M > 0) ? specReservoir.w_sum / (targetLum * (float)specReservoir.M) : 0; + specReservoir.age = 0; } } + float dLum = max(Luminance(direct), 0.05); + float specClamp = lerp(4.0, 12.0, saturate(roughness * 5.0)); + if (isHud) + specClamp = min(specClamp, 1.5); + if (Luminance(specular) > dLum * specClamp) + specular *= (dLum * specClamp) / max(Luminance(specular), 1e-4); + u_NoisySpecular[pixel] = float4(specular, specHitDist); + float4 resA, resB; PackReservoir(reservoir, resA, resB); u_ReservoirA[pixel] = resA; u_ReservoirB[pixel] = resB; + u_ReservoirC[pixel] = PackReservoirC(reservoir); + + float4 sA, sB; + PackReservoir(specReservoir, sA, sB); + u_SpecReservoirA[pixel] = sA; + u_SpecReservoirB[pixel] = sB; } diff --git a/res/gamedata/shaders/r5/restir_gi_spatial.cs b/res/gamedata/shaders/r5/restir_gi_spatial.cs new file mode 100644 index 00000000000..62687d400e9 --- /dev/null +++ b/res/gamedata/shaders/r5/restir_gi_spatial.cs @@ -0,0 +1,215 @@ +#include "bindless_common.h" +#include "rt_common.h" +#include "rt_visibility.h" +#include "shared/pbr_brdf.h" +#include "shared/basecolor_pack.h" +#include "shared/surface_marks.h" +#include "restir_gi_common.h" + +cbuffer ReSTIRSpatialParams : register(b5) { + float4x4 g_InvViewProj; + float4 g_CameraPos; + float2 g_ScreenSize; + float2 g_InvScreenSize; + uint g_FrameIndex; + uint g_SpatialSamples; + float g_SpatialRadius; + uint g_MMax; + uint g_IdentityStaticCount; + uint g_TerrainBatchCount; + uint g_SkinnedBatchStart; + uint g_GrassBatchStart; + uint g_DetailAtlasIndex; + uint3 g_Pad; +}; + +RaytracingAccelerationStructure g_SceneTLAS : register(t1); +StructuredBuffer g_BatchInfo : register(t2); +ByteAddressBuffer g_MegaVB : register(t3); +ByteAddressBuffer g_MegaIB : register(t18); +ByteAddressBuffer g_GrassVB : register(t12); +ByteAddressBuffer g_GrassIB : register(t13); + +Texture2D t_Depth : register(t4); +Texture2D t_Normal : register(t5); +Texture2D t_BaseColor : register(t6); +Texture2D t_WorldPos : register(t7); +Texture2D t_ReservoirA : register(t14); +Texture2D t_ReservoirB : register(t15); +Texture2D t_ReservoirC : register(t16); +Texture2D t_SpecA : register(t17); +Texture2D t_SpecB : register(t19); + +RWTexture2D u_ReservoirA : register(u0); +RWTexture2D u_ReservoirB : register(u1); +RWTexture2D u_ReservoirC : register(u2); +RWTexture2D u_SpecReservoirA : register(u3); +RWTexture2D u_SpecReservoirB : register(u4); + +bool VisibilityOK(float3 worldPos, float3 N, float3 samplePos) +{ + float3 biasedPos = worldPos + N * 0.01; + return TraceVisibilityClear( + g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, + biasedPos, samplePos, + g_IdentityStaticCount, g_TerrainBatchCount, g_SkinnedBatchStart, g_GrassBatchStart, + g_DetailAtlasIndex); +} + +float TargetLuminance(GIReservoir r, float3 worldPos, float3 N, float3 albedo, float metallic) +{ + if (!IsReservoirValid(r)) + return 0; + float3 wi = normalize(r.samplePos - worldPos); + float cosTheta = max(dot(N, wi), 0); + float3 F0 = CalculateF0(albedo, metallic); + float3 kD = (1.0 - F_Schlick(cosTheta, F0)) * (1.0 - metallic); + float3 target = r.Lo * kD * albedo / PI * cosTheta; + return Luminance(target); +} + +GIReservoir SpatialReuse( + GIReservoir center, + Texture2D srcA, + Texture2D srcB, + Texture2D srcC, + bool hasC, + uint2 pixel, + float3 worldPos, + float centerMark, + float3 N, + float3 albedo, + float metallic, + float linearDepth, + float radiusScale, + inout uint rng) +{ + float centerLum = TargetLuminance(center, worldPos, N, albedo, metallic); + GIReservoir output = EmptyReservoir(); + + if (centerLum > 0) { + output.samplePos = center.samplePos; + output.sampleNormal = center.sampleNormal; + output.Lo = center.Lo; + output.w_sum = centerLum * center.W; + output.M = max(center.M, 1u); + output.age = center.age; + } + + uint samples = min(g_SpatialSamples, 16u); + for (uint i = 0; i < samples; ++i) { + float ang = rand_float(rng) * 6.2831853; + float rad = sqrt(rand_float(rng)) * g_SpatialRadius * radiusScale; + int2 offset = int2(cos(ang) * rad, sin(ang) * rad); + int2 nPixel = int2(pixel) + offset; + if (nPixel.x < 0 || nPixel.y < 0 || + nPixel.x >= (int)g_ScreenSize.x || nPixel.y >= (int)g_ScreenSize.y) + continue; + + float nDepth = t_Depth.Load(int3(nPixel, 0)); + if (nDepth >= 1.0) + continue; + + float4 nWorldPosData = t_WorldPos.Load(int3(nPixel, 0)); + if (!SameHudSurfClass(centerMark, nWorldPosData.w)) + continue; + float3 nWorldPos = nWorldPosData.xyz; + float3 nN = normalize(t_Normal.Load(int3(nPixel, 0)).xyz); + float nLinearDepth = length(nWorldPos - g_CameraPos.xyz); + if (!ValidateTemporalNeighbor(linearDepth, N, nLinearDepth, nN)) + continue; + + GIReservoir neighbor; + if (hasC) + neighbor = UnpackReservoir(srcA.Load(int3(nPixel, 0)), srcB.Load(int3(nPixel, 0)), srcC.Load(int3(nPixel, 0))); + else + neighbor = UnpackReservoir(srcA.Load(int3(nPixel, 0)), srcB.Load(int3(nPixel, 0))); + if (!IsReservoirValid(neighbor)) + continue; + + float jac = JacobianReconnectionShift(neighbor.sampleNormal, worldPos, nWorldPos, neighbor.samplePos); + if (jac < 1e-4 || jac > 20.0) + continue; + + float neighLum = TargetLuminance(neighbor, worldPos, N, albedo, metallic); + if (neighLum <= 0) + continue; + + float conf = AgeConfidence(neighbor.age, 32u); + uint clampedM = TemporalMClamp(neighbor.M, neighbor.age, g_MMax); + float w = neighLum * neighbor.W * jac * clampedM * conf; + ReservoirUpdate(output, w, neighbor.samplePos, neighbor.sampleNormal, neighbor.Lo, rng); + output.M += clampedM - 1; + } + + float outLum = TargetLuminance(output, worldPos, N, albedo, metallic); + output.W = (outLum > 0 && output.M > 0) ? output.w_sum / (outLum * output.M) : 0; + output.age = min(output.age + 1, 255); + + if (IsReservoirValid(output) && output.W > 0) { + if (!VisibilityOK(worldPos, N, output.samplePos)) { + output.W = 0; + output.w_sum = 0; + } + } + return output; +} + +[numthreads(8, 8, 1)] +void main(uint3 dispatchID : SV_DispatchThreadID) +{ + uint2 pixel = dispatchID.xy; + if (pixel.x >= (uint)g_ScreenSize.x || pixel.y >= (uint)g_ScreenSize.y) + return; + + float depth = t_Depth.Load(int3(pixel, 0)); + if (depth >= 1.0) { + u_ReservoirA[pixel] = 0; + u_ReservoirB[pixel] = 0; + u_ReservoirC[pixel] = 0; + u_SpecReservoirA[pixel] = 0; + u_SpecReservoirB[pixel] = 0; + return; + } + + float4 worldPosData = t_WorldPos.Load(int3(pixel, 0)); + float3 worldPos = worldPosData.xyz; + const float centerMark = worldPosData.w; + float4 normalData = t_Normal.Load(int3(pixel, 0)); + float3 N = normalize(normalData.xyz); + float roughness = max(normalData.w, MIN_ROUGHNESS); + float4 baseColorData = t_BaseColor.Load(int3(pixel, 0)); + float3 albedo = baseColorData.rgb; + float metallic = UnpackMetallicFromBaseA(baseColorData.a); + float linearDepth = length(worldPos - g_CameraPos.xyz); + uint rng = pcg_hash(pixel.x + pixel.y * 6287u + g_FrameIndex * 33461u); + + GIReservoir center = UnpackReservoir( + t_ReservoirA.Load(int3(pixel, 0)), + t_ReservoirB.Load(int3(pixel, 0)), + t_ReservoirC.Load(int3(pixel, 0))); + GIReservoir centerSpec = UnpackReservoir( + t_SpecA.Load(int3(pixel, 0)), + t_SpecB.Load(int3(pixel, 0))); + + GIReservoir output = SpatialReuse(center, t_ReservoirA, t_ReservoirB, t_ReservoirC, true, + pixel, worldPos, centerMark, N, albedo, metallic, linearDepth, 1.0, rng); + float3 specAlbedo = lerp(float3(1, 1, 1), albedo, metallic); + float specRadius = roughness < 0.2 ? 0.35 : (roughness < 0.45 ? 0.6 : 0.15); + GIReservoir outputSpec = centerSpec; + if (roughness < 0.55) { + outputSpec = SpatialReuse(centerSpec, t_SpecA, t_SpecB, t_ReservoirC, false, + pixel, worldPos, centerMark, N, specAlbedo, metallic, linearDepth, specRadius, rng); + } + + float4 outA, outB; + PackReservoir(output, outA, outB); + u_ReservoirA[pixel] = outA; + u_ReservoirB[pixel] = outB; + u_ReservoirC[pixel] = PackReservoirC(output); + + float4 sA, sB; + PackReservoir(outputSpec, sA, sB); + u_SpecReservoirA[pixel] = sA; + u_SpecReservoirB[pixel] = sB; +} diff --git a/res/gamedata/shaders/r5/restir_gi_temporal.cs b/res/gamedata/shaders/r5/restir_gi_temporal.cs index 0162b046aba..b96edb65551 100644 --- a/res/gamedata/shaders/r5/restir_gi_temporal.cs +++ b/res/gamedata/shaders/r5/restir_gi_temporal.cs @@ -1,6 +1,9 @@ -#include "common.h" +#include "bindless_common.h" #include "rt_common.h" +#include "rt_visibility.h" #include "shared/pbr_brdf.h" +#include "shared/basecolor_pack.h" +#include "shared/surface_marks.h" #include "restir_gi_common.h" cbuffer ReSTIRTemporalParams : register(b5) { @@ -9,48 +12,62 @@ float2 g_ScreenSize; float2 g_InvScreenSize; uint g_FrameIndex; - uint3 g_Pad; + uint g_IdentityStaticCount; + uint g_TerrainBatchCount; + uint g_SkinnedBatchStart; + uint g_GrassBatchStart; + uint g_DetailAtlasIndex; + uint2 g_Pad; }; +RaytracingAccelerationStructure g_SceneTLAS : register(t1); +StructuredBuffer g_BatchInfo : register(t2); +ByteAddressBuffer g_MegaVB : register(t3); +ByteAddressBuffer g_MegaIB : register(t18); +ByteAddressBuffer g_GrassVB : register(t12); +ByteAddressBuffer g_GrassIB : register(t13); + Texture2D t_PrevReservoirA : register(t0); -Texture2D t_PrevReservoirB : register(t1); -Texture2D t_MotionVectors : register(t2); -Texture2D t_Depth : register(t3); -Texture2D t_PrevNormal : register(t5); -Texture2D t_BaseColor : register(t6); -Texture2D t_WorldPos : register(t7); -Texture2D t_PrevWorldPos : register(t8); -Texture2D t_Normal : register(t9); +Texture2D t_PrevReservoirB : register(t4); +Texture2D t_MotionVectors : register(t5); +Texture2D t_Depth : register(t6); +Texture2D t_PrevNormal : register(t7); +Texture2D t_BaseColor : register(t14); +Texture2D t_WorldPos : register(t15); +Texture2D t_PrevWorldPos : register(t16); +Texture2D t_Normal : register(t17); +Texture2D t_PrevReservoirC : register(t19); +Texture2D t_PrevSpecA : register(t20); +Texture2D t_PrevSpecB : register(t21); RWTexture2D u_ReservoirA : register(u0); RWTexture2D u_ReservoirB : register(u1); +RWTexture2D u_ReservoirC : register(u2); +RWTexture2D u_SpecReservoirA : register(u3); +RWTexture2D u_SpecReservoirB : register(u4); -[numthreads(8, 8, 1)] -void main(uint3 dispatchID : SV_DispatchThreadID) +bool VisibilityOK(float3 worldPos, float3 N, float3 samplePos) { - uint2 pixel = dispatchID.xy; - if (pixel.x >= (uint)g_ScreenSize.x || pixel.y >= (uint)g_ScreenSize.y) - return; - - float depth = t_Depth.Load(int3(pixel, 0)); - if (depth >= 1.0) { - u_ReservoirA[pixel] = 0; - u_ReservoirB[pixel] = 0; - return; - } - - GIReservoir currRes = UnpackReservoir( - u_ReservoirA[pixel], - u_ReservoirB[pixel] - ); - - float3 worldPos = t_WorldPos.Load(int3(pixel, 0)).xyz; - float4 normalData = t_Normal.Load(int3(pixel, 0)); - float3 N = normalize(normalData.xyz); - float4 baseColorData = t_BaseColor.Load(int3(pixel, 0)); - float3 albedo = baseColorData.rgb; - float metallic = baseColorData.a; + float3 biasedPos = worldPos + N * 0.01; + return TraceVisibilityClear( + g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, + biasedPos, samplePos, + g_IdentityStaticCount, g_TerrainBatchCount, g_SkinnedBatchStart, g_GrassBatchStart, + g_DetailAtlasIndex); +} +void TemporalMerge( + inout GIReservoir output, + GIReservoir currRes, + GIReservoir prevRes, + float3 worldPos, + float3 prevWorldPos, + float3 N, + float3 albedo, + float metallic, + bool requireVis, + inout uint rng) +{ float3 target_curr = 0; if (IsReservoirValid(currRes)) { float3 wi = normalize(currRes.samplePos - worldPos); @@ -61,70 +78,126 @@ void main(uint3 dispatchID : SV_DispatchThreadID) } float targetLum_curr = Luminance(target_curr); - GIReservoir output = EmptyReservoir(); - uint rng = pcg_hash(pixel.x + pixel.y * 7919u + g_FrameIndex * 48611u); - if (targetLum_curr > 0) { output.samplePos = currRes.samplePos; output.sampleNormal = currRes.sampleNormal; output.Lo = currRes.Lo; output.w_sum = targetLum_curr * currRes.W; output.M = 1; + output.age = currRes.age; } - float2 motion = t_MotionVectors.Load(int3(pixel, 0)); - float2 currUV = (float2(pixel) + 0.5) * g_InvScreenSize; - float2 prevUV = currUV + motion; - - if (all(prevUV >= 0) && all(prevUV < 1.0)) { - int2 prevPixel = int2(prevUV * g_ScreenSize); - float3 prevWorldPos = t_PrevWorldPos.Load(int3(prevPixel, 0)).xyz; - float3 prevN = normalize(t_PrevNormal.Load(int3(prevPixel, 0)).xyz); - - float viewDist = length(worldPos - g_CameraPos.xyz); - float posDist = length(worldPos - prevWorldPos); - bool valid = posDist < 0.1 * viewDist && dot(N, prevN) > 0.906; - - if (valid) { - GIReservoir prevRes = UnpackReservoir( - t_PrevReservoirA.Load(int3(prevPixel, 0)), - t_PrevReservoirB.Load(int3(prevPixel, 0)) - ); - - if (IsReservoirValid(prevRes)) { + if (IsReservoirValid(prevRes)) { + if (requireVis && !VisibilityOK(worldPos, N, prevRes.samplePos)) + prevRes = EmptyReservoir(); + if (IsReservoirValid(prevRes)) { + float jac = JacobianReconnectionShift(prevRes.sampleNormal, worldPos, prevWorldPos, prevRes.samplePos); + if (jac >= 1e-4 && jac <= 20.0) { float3 wi_prev = normalize(prevRes.samplePos - worldPos); float cosTheta_prev = max(dot(N, wi_prev), 0); float3 F0 = CalculateF0(albedo, metallic); float3 kD = (1.0 - F_Schlick(cosTheta_prev, F0)) * (1.0 - metallic); float3 target_prev = prevRes.Lo * kD * albedo / PI * cosTheta_prev; float targetLum_prev = Luminance(target_prev); - if (targetLum_prev > 0) { - uint clampedM = min(prevRes.M, RESTIR_M_MAX); - float w_prev = targetLum_prev * prevRes.W * clampedM; - + float conf = AgeConfidence(prevRes.age, 32u); + uint clampedM = TemporalMClamp(prevRes.M, prevRes.age, RESTIR_M_MAX); + float w_prev = targetLum_prev * prevRes.W * jac * clampedM * conf; ReservoirUpdate(output, w_prev, prevRes.samplePos, prevRes.sampleNormal, prevRes.Lo, rng); output.M += clampedM - 1; + output.age = min(prevRes.age + 1, 255); } } } } float outTargetLum = targetLum_curr; - if (output.samplePos.x != currRes.samplePos.x || output.samplePos.y != currRes.samplePos.y) { + if (IsReservoirValid(output)) { float3 wi_out = normalize(output.samplePos - worldPos); float cosTheta_out = max(dot(N, wi_out), 0); float3 F0 = CalculateF0(albedo, metallic); float3 kD = (1.0 - F_Schlick(cosTheta_out, F0)) * (1.0 - metallic); - float3 target_out = output.Lo * kD * albedo / PI * cosTheta_out; - outTargetLum = Luminance(target_out); + outTargetLum = Luminance(output.Lo * kD * albedo / PI * cosTheta_out); } - output.W = (outTargetLum > 0 && output.M > 0) ? output.w_sum / (outTargetLum * output.M) : 0; - output.age = min(output.age + 1, 255); +} + +[numthreads(8, 8, 1)] +void main(uint3 dispatchID : SV_DispatchThreadID) +{ + uint2 pixel = dispatchID.xy; + if (pixel.x >= (uint)g_ScreenSize.x || pixel.y >= (uint)g_ScreenSize.y) + return; + + float depth = t_Depth.Load(int3(pixel, 0)); + if (depth >= 1.0) { + u_ReservoirA[pixel] = 0; + u_ReservoirB[pixel] = 0; + u_ReservoirC[pixel] = 0; + u_SpecReservoirA[pixel] = 0; + u_SpecReservoirB[pixel] = 0; + return; + } + + GIReservoir currRes = UnpackReservoir(u_ReservoirA[pixel], u_ReservoirB[pixel], u_ReservoirC[pixel]); + GIReservoir currSpec = UnpackReservoir(u_SpecReservoirA[pixel], u_SpecReservoirB[pixel]); + + float4 worldPosData = t_WorldPos.Load(int3(pixel, 0)); + float3 worldPos = worldPosData.xyz; + const float centerMark = worldPosData.w; + float4 normalData = t_Normal.Load(int3(pixel, 0)); + float3 N = normalize(normalData.xyz); + float roughness = max(normalData.w, MIN_ROUGHNESS); + float4 baseColorData = t_BaseColor.Load(int3(pixel, 0)); + float3 albedo = baseColorData.rgb; + float metallic = UnpackMetallicFromBaseA(baseColorData.a); + + GIReservoir output = EmptyReservoir(); + GIReservoir outputSpec = EmptyReservoir(); + uint rng = pcg_hash(pixel.x + pixel.y * 7919u + g_FrameIndex * 48611u); + + float2 motion = t_MotionVectors.Load(int3(pixel, 0)); + float2 currUV = (float2(pixel) + 0.5) * g_InvScreenSize; + float2 prevUV = currUV + motion; + + GIReservoir prevRes = EmptyReservoir(); + GIReservoir prevSpec = EmptyReservoir(); + float3 prevWorldPos = worldPos; + + if (all(prevUV >= 0) && all(prevUV < 1.0)) { + int2 prevPixel = int2(prevUV * g_ScreenSize); + float4 prevWorldPosData = t_PrevWorldPos.Load(int3(prevPixel, 0)); + prevWorldPos = prevWorldPosData.xyz; + float3 prevN = normalize(t_PrevNormal.Load(int3(prevPixel, 0)).xyz); + float viewDist = length(worldPos - g_CameraPos.xyz); + float posDist = length(worldPos - prevWorldPos); + bool valid = SameHudSurfClass(centerMark, prevWorldPosData.w) && + posDist < 0.1 * viewDist && dot(N, prevN) > 0.906; + if (valid) { + prevRes = UnpackReservoir( + t_PrevReservoirA.Load(int3(prevPixel, 0)), + t_PrevReservoirB.Load(int3(prevPixel, 0)), + t_PrevReservoirC.Load(int3(prevPixel, 0))); + prevSpec = UnpackReservoir( + t_PrevSpecA.Load(int3(prevPixel, 0)), + t_PrevSpecB.Load(int3(prevPixel, 0))); + } + } + + TemporalMerge(output, currRes, prevRes, worldPos, prevWorldPos, N, albedo, metallic, true, rng); + float3 specAlbedo = lerp(float3(1, 1, 1), albedo, metallic); + TemporalMerge(outputSpec, currSpec, prevSpec, worldPos, prevWorldPos, N, specAlbedo, metallic, true, rng); + if (roughness > 0.55) + outputSpec = currSpec; float4 outA, outB; PackReservoir(output, outA, outB); u_ReservoirA[pixel] = outA; u_ReservoirB[pixel] = outB; + u_ReservoirC[pixel] = PackReservoirC(output); + + float4 sA, sB; + PackReservoir(outputSpec, sA, sB); + u_SpecReservoirA[pixel] = sA; + u_SpecReservoirB[pixel] = sB; } diff --git a/res/gamedata/shaders/r5/restir_gi_temporal_filter.cs b/res/gamedata/shaders/r5/restir_gi_temporal_filter.cs new file mode 100644 index 00000000000..e84352b9592 --- /dev/null +++ b/res/gamedata/shaders/r5/restir_gi_temporal_filter.cs @@ -0,0 +1,135 @@ +#include "shared/surface_marks.h" + +cbuffer TemporalFilterParams : register(b5) { + float2 g_ScreenSize; + float2 g_InvScreenSize; + float g_Alpha; + float g_Pad0; + uint g_Enabled; + uint g_Pad1; +}; + +Texture2D t_CurrDiffuse : register(t0); +Texture2D t_CurrSpecular : register(t1); +Texture2D t_HistDiffuse : register(t2); +Texture2D t_HistSpecular : register(t3); +Texture2D t_MotionVectors : register(t4); +Texture2D t_Depth : register(t5); +Texture2D t_Normal : register(t6); +Texture2D t_WorldPos : register(t7); +Texture2D t_PrevWorldPos : register(t8); +Texture2D t_PrevNormal : register(t9); + +RWTexture2D u_OutDiffuse : register(u0); +RWTexture2D u_OutSpecular : register(u1); + +float Luma(float3 c) +{ + return dot(c, float3(0.2126, 0.7152, 0.0722)); +} + +float3 ClipAABB(float3 hist, float3 minC, float3 maxC) +{ + float3 center = 0.5 * (minC + maxC); + float3 extents = 0.5 * (maxC - minC) + 1e-4; + float3 offset = hist - center; + float3 ts = abs(extents / max(abs(offset), 1e-4)); + float t = saturate(min(min(ts.x, ts.y), ts.z)); + return center + offset * t; +} + +[numthreads(8, 8, 1)] +void main(uint3 dispatchID : SV_DispatchThreadID) +{ + uint2 pixel = dispatchID.xy; + if (pixel.x >= (uint)g_ScreenSize.x || pixel.y >= (uint)g_ScreenSize.y) + return; + + float depth = t_Depth.Load(int3(pixel, 0)); + float4 currD4 = t_CurrDiffuse.Load(int3(pixel, 0)); + float4 currS4 = t_CurrSpecular.Load(int3(pixel, 0)); + float3 currD = currD4.rgb; + float3 currS = currS4.rgb; + float specHitDist = currS4.a; + + if (depth >= 1.0 || g_Enabled == 0) { + u_OutDiffuse[pixel] = float4(currD, 1.0); + u_OutSpecular[pixel] = float4(currS, specHitDist); + return; + } + + float2 mv = t_MotionVectors.Load(int3(pixel, 0)); + float2 histUV = (float2(pixel) + 0.5) * g_InvScreenSize - mv; + if (any(histUV < 0.0) || any(histUV > 1.0)) { + u_OutDiffuse[pixel] = float4(currD, 1.0); + u_OutSpecular[pixel] = float4(currS, specHitDist); + return; + } + + int2 histPixel = int2(histUV * g_ScreenSize); + histPixel = clamp(histPixel, int2(0, 0), int2(g_ScreenSize) - 1); + + float4 worldPosData = t_WorldPos.Load(int3(pixel, 0)); + float4 prevWorldData = t_PrevWorldPos.Load(int3(histPixel, 0)); + float3 worldPos = worldPosData.xyz; + float3 prevWorld = prevWorldData.xyz; + float3 prevN = normalize(t_PrevNormal.Load(int3(histPixel, 0)).xyz); + float3 N = normalize(t_Normal.Load(int3(pixel, 0)).xyz); + + if (!SameHudSurfClass(worldPosData.w, prevWorldData.w)) { + u_OutDiffuse[pixel] = float4(currD, 1.0); + u_OutSpecular[pixel] = float4(currS, specHitDist); + return; + } + + float posErr = length(worldPos - prevWorld) / max(length(worldPos), 1.0); + float nDot = saturate(dot(N, prevN)); + float trust = 1.0; + if (posErr > 0.08 || nDot < 0.7) { + u_OutDiffuse[pixel] = float4(currD, 1.0); + u_OutSpecular[pixel] = float4(currS, specHitDist); + return; + } + if (posErr > 0.03) + trust *= saturate(1.0 - (posErr - 0.03) / 0.05); + if (nDot < 0.9) + trust *= saturate((nDot - 0.7) / 0.2); + + float3 histD = t_HistDiffuse.Load(int3(histPixel, 0)).rgb; + float3 histS = t_HistSpecular.Load(int3(histPixel, 0)).rgb; + + float3 minD = currD, maxD = currD, minS = currS, maxS = currS; + [unroll] for (int iy = -1; iy <= 1; ++iy) { + [unroll] for (int ix = -1; ix <= 1; ++ix) { + int2 np = int2(pixel) + int2(ix, iy); + if (np.x < 0 || np.y < 0 || np.x >= (int)g_ScreenSize.x || np.y >= (int)g_ScreenSize.y) + continue; + if (!SameHudSurfClass(worldPosData.w, t_WorldPos.Load(int3(np, 0)).w)) + continue; + float3 d = t_CurrDiffuse.Load(int3(np, 0)).rgb; + float3 s = t_CurrSpecular.Load(int3(np, 0)).rgb; + minD = min(minD, d); + maxD = max(maxD, d); + minS = min(minS, s); + maxS = max(maxS, s); + } + } + + histD = ClipAABB(histD, minD * 0.15, maxD * 4.5); + histS = ClipAABB(histS, minS * 0.1, maxS * 5.0); + + float alpha = saturate(g_Alpha) * trust; + float3 outD = lerp(currD, histD, alpha); + float3 outS = lerp(currS, histS, alpha * 0.96); + float currLd = max(Luma(currD), 1e-4); + float histLd = max(Luma(outD), 1e-4); + if (currLd > histLd * 3.5) + outD *= (histLd * 3.5) / currLd; + float currLs = max(Luma(currS), 1e-4); + float histLs = max(Luma(outS), 1e-4); + if (currLs > histLs * 4.5) + outS *= (histLs * 4.5) / currLs; + + u_OutDiffuse[pixel] = float4(outD, 1.0); + u_OutSpecular[pixel] = float4(outS, specHitDist); +} diff --git a/res/gamedata/shaders/r5/restir_motion_vectors.cs b/res/gamedata/shaders/r5/restir_motion_vectors.cs index 62a5839b74f..2b8bbe9d860 100644 --- a/res/gamedata/shaders/r5/restir_motion_vectors.cs +++ b/res/gamedata/shaders/r5/restir_motion_vectors.cs @@ -1,22 +1,34 @@ #include "common.h" cbuffer MotionVectorParams : register(b5) { - float4x4 g_InvViewProj; + float4x4 g_ViewProj; float4x4 g_PrevViewProj; + float4x4 g_InvViewProj; float2 g_ScreenSize; float2 g_InvScreenSize; + float4 g_CameraPos; + float4 g_PrevCameraPos; + uint g_HasPrevCamera; + uint3 g_Pad; }; Texture2D t_Depth : register(t0); +Texture2D t_WorldPos : register(t1); RWTexture2D u_MotionVectors : register(u0); -float3 ReconstructWorldPos(uint2 pixel, float depth) +float2 ProjectToUv(float4x4 viewProj, float3 worldPos) { - float2 uv = (float2(pixel) + 0.5) * g_InvScreenSize; - float4 clip = float4(uv * 2.0 - 1.0, depth, 1.0); - clip.y = -clip.y; - float4 world = mul(g_InvViewProj, clip); - return world.xyz / world.w; + float4 clip = mul(viewProj, float4(worldPos, 1.0)); + float2 ndc = clip.xy / max(clip.w, 1e-5); + ndc.y = -ndc.y; + return ndc * 0.5 + 0.5; +} + +float3 ReconstructFarWorld(float2 uv) +{ + float2 ndc = float2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0); + float4 farH = mul(g_InvViewProj, float4(ndc, 1.0, 1.0)); + return farH.xyz / max(farH.w, 1e-6); } [numthreads(8, 8, 1)] @@ -26,21 +38,28 @@ void main(uint3 dtid : SV_DispatchThreadID) if (pixel.x >= (uint)g_ScreenSize.x || pixel.y >= (uint)g_ScreenSize.y) return; + float2 uv = (float2(pixel) + 0.5) * g_InvScreenSize; float depth = t_Depth.Load(int3(pixel, 0)); + if (depth >= 0.9995) + { + if (g_HasPrevCamera == 0) + { + u_MotionVectors[pixel] = 0.0; + return; + } - if (depth >= 1.0) { - u_MotionVectors[pixel] = float2(0, 0); + float3 farW = ReconstructFarWorld(uv); + float3 dir = normalize(farW - g_CameraPos.xyz); + float3 currPt = g_CameraPos.xyz + dir * 1e5; + float3 prevPt = g_PrevCameraPos.xyz + dir * 1e5; + float2 currUV = ProjectToUv(g_ViewProj, currPt); + float2 prevUV = ProjectToUv(g_PrevViewProj, prevPt); + u_MotionVectors[pixel] = prevUV - currUV; return; } - float3 worldPos = ReconstructWorldPos(pixel, depth); - - float4 prevClip = mul(g_PrevViewProj, float4(worldPos, 1.0)); - float2 prevNDC = prevClip.xy / prevClip.w; - prevNDC.y = -prevNDC.y; - float2 prevUV = prevNDC * 0.5 + 0.5; - - float2 currUV = (float2(pixel) + 0.5) * g_InvScreenSize; - + float3 worldPos = t_WorldPos.Load(int3(pixel, 0)).xyz; + float2 prevUV = ProjectToUv(g_PrevViewProj, worldPos); + float2 currUV = ProjectToUv(g_ViewProj, worldPos); u_MotionVectors[pixel] = prevUV - currUV; } diff --git a/res/gamedata/shaders/r5/restir_water_rt.cs b/res/gamedata/shaders/r5/restir_water_rt.cs new file mode 100644 index 00000000000..48840f45db9 --- /dev/null +++ b/res/gamedata/shaders/r5/restir_water_rt.cs @@ -0,0 +1,281 @@ +#include "bindless_common.h" +#include "rt_common.h" +#include "rt_grass_alpha.h" +#include "rt_shade_hit.h" +#include "shared/surface_marks.h" +#include "restir_gi_common.h" + +cbuffer ReSTIRWaterParams : register(b5) { + float4x4 g_InvViewProj; + float4 g_CameraPos; + float4 g_SunDir_Intensity; + float4 g_SunColor_SkyWeight; + float2 g_ScreenSize; + float g_GIIntensity; + uint g_IdentityStaticCount; + uint g_TerrainBatchCount; + uint g_SkinnedBatchStart; + uint g_GrassBatchStart; + uint g_DetailAtlasIndex; + float4 g_HemiColor; +}; + +RaytracingAccelerationStructure g_SceneTLAS : register(t1); +StructuredBuffer g_BatchInfo : register(t2); +ByteAddressBuffer g_MegaVB : register(t3); +TextureCube g_Sky0 : register(t5); +TextureCube g_Sky1 : register(t6); +ByteAddressBuffer g_SkinnedVB : register(t7); +ByteAddressBuffer g_SkinnedIB : register(t11); +ByteAddressBuffer g_GrassVB : register(t12); +ByteAddressBuffer g_GrassIB : register(t13); +Texture2D t_Depth : register(t14); +Texture2D t_Normal : register(t15); +Texture2D t_BaseColor : register(t16); +Texture2D t_WorldPos : register(t17); +ByteAddressBuffer g_MegaIB : register(t18); +Texture2D t_SceneColorIn : register(t19); +Texture2D t_ClassifyWorldPos : register(t20); +Texture2D t_UnderWorldPos : register(t21); + +RWTexture2D u_SceneColor : register(u0); + +bool IsSkinnedBatch(uint batchIdx) +{ + return g_SkinnedBatchStart != 0xFFFFFFFFu && batchIdx >= g_SkinnedBatchStart && + (g_GrassBatchStart == 0xFFFFFFFFu || batchIdx < g_GrassBatchStart); +} + +bool IsGrassBatch(uint batchIdx) +{ + return g_GrassBatchStart != 0xFFFFFFFFu && batchIdx >= g_GrassBatchStart; +} + +float3 SampleSkyW(float3 dir) +{ + float3 d = normalize(dir); + float w = g_SunColor_SkyWeight.w; + float3 s0 = g_Sky0.SampleLevel(smp_linear, d, 0).rgb; + float3 s1 = g_Sky1.SampleLevel(smp_linear, d, 0).rgb; + return max(lerp(s0, s1, w) * 1.15, float3(0.06, 0.07, 0.09)); +} + +float FresnelSchlickWater(float cosTheta) +{ + float F0 = 0.02; + float m = 1.0 - saturate(cosTheta); + float m2 = m * m; + return F0 + (1.0 - F0) * m2 * m2 * m; +} + +float3 DecodeNormal(float3 nEnc) +{ + float3 n = nEnc * 2.0 - 1.0; + float lenSq = dot(n, n); + if (lenSq < 1e-6) + return float3(0.0, 1.0, 0.0); + return n * rsqrt(lenSq); +} + +float3 TraceWaterReflect(float3 origin, float3 dir, float3 skyRefl) +{ + float3 rayOrigin = origin; + float remain = 8000.0; + + [loop] + for (uint step = 0; step < 8; ++step) + { + if (remain <= 0.05) + break; + + RayDesc ray; + ray.Origin = rayOrigin; + ray.Direction = dir; + ray.TMin = 0.02; + ray.TMax = remain; + + RayQuery q; + q.TraceRayInline(g_SceneTLAS, RAY_FLAG_NONE, 0xFF, ray); + while (q.Proceed()) + { + if (q.CandidateType() == CANDIDATE_NON_OPAQUE_TRIANGLE) + { + uint candBatch = q.CandidateInstanceID() + q.CandidateGeometryIndex(); + if (IsGrassBatch(candBatch)) + { + if (GrassTexelOpaque(g_GrassVB, g_GrassIB, g_BatchInfo, candBatch, + q.CandidatePrimitiveIndex(), q.CandidateTriangleBarycentrics(), + g_DetailAtlasIndex)) + q.CommitNonOpaqueTriangleHit(); + } + else + { + RTBatchInfo candInfo = g_BatchInfo[candBatch]; + MaterialData candMat = g_Materials[candInfo.materialID]; + float2 candUV = GetHitUV(g_MegaVB, g_MegaIB, candInfo, + q.CandidatePrimitiveIndex(), q.CandidateTriangleBarycentrics()); + float4 candDiff = SampleDiffuseLevel(candMat, candUV); + bool keep = true; + if (candMat.flags & MAT_FLAG_ALPHA_TEST) + keep = candDiff.a >= candMat.alphaRef; + else if (candMat.flags & MAT_FLAG_ALPHA_BLEND) + keep = candDiff.a >= 0.5; + if (keep) + q.CommitNonOpaqueTriangleHit(); + } + } + } + + if (q.CommittedStatus() != COMMITTED_TRIANGLE_HIT) + break; + + float tHit = q.CommittedRayT(); + uint batchIdx = q.CommittedInstanceID() + q.CommittedGeometryIndex(); + RTBatchInfo info = g_BatchInfo[batchIdx]; + uint primIdx = q.CommittedPrimitiveIndex(); + float2 bary = q.CommittedTriangleBarycentrics(); + float3x4 objectToWorld = q.CommittedObjectToWorld3x4(); + + float3 albedo = float3(0.5, 0.5, 0.5); + float3 hitN = float3(0.0, 1.0, 0.0); + float hemi = 0.5; + bool skipHit = false; + + if (IsGrassBatch(batchIdx)) + { + float2 hitUV = GetSkinnedHitUV(g_GrassVB, g_GrassIB, info, primIdx, bary); + hitN = normalize(TransformNormalToWorld( + GetSkinnedHitNormal(g_GrassVB, g_GrassIB, info, primIdx, bary), objectToWorld)); + if (g_DetailAtlasIndex > 0) + { + float4 texel = GetBindlessTexture(g_DetailAtlasIndex).SampleLevel(smp_linear, hitUV, 0); + if (texel.a < 0.3) + skipHit = true; + else + albedo = texel.rgb; + } + else + { + albedo = lerp(float3(0.08, 0.18, 0.03), float3(0.15, 0.35, 0.06), 1.0 - hitUV.y); + } + } + else if (IsSkinnedBatch(batchIdx)) + { + float2 hitUV = GetSkinnedHitUV(g_SkinnedVB, g_SkinnedIB, info, primIdx, bary); + hitN = normalize(TransformNormalToWorld( + GetSkinnedHitNormal(g_SkinnedVB, g_SkinnedIB, info, primIdx, bary), objectToWorld)); + MaterialData mat = g_Materials[info.materialID]; + float4 diffuse = SampleDiffuseLevel(mat, hitUV); + if ((mat.flags & MAT_FLAG_ALPHA_TEST) && diffuse.a < mat.alphaRef) + skipHit = true; + else if ((mat.flags & MAT_FLAG_ALPHA_BLEND) && diffuse.a < 0.5) + skipHit = true; + else if (mat.flags & MAT_FLAG_WATER) + skipHit = true; + else + albedo = diffuse.rgb; + hemi = 0.55; + } + else + { + float2 hitUV = GetHitUV(g_MegaVB, g_MegaIB, info, primIdx, bary); + hemi = GetHitHemi(g_MegaVB, g_MegaIB, info, primIdx, bary); + hitN = normalize(TransformNormalToWorld( + GetHitNormal(g_MegaVB, g_MegaIB, info, primIdx, bary), objectToWorld)); + MaterialData mat = g_Materials[info.materialID]; + float4 diffuse = SampleDiffuseLevel(mat, hitUV); + if (mat.flags & MAT_FLAG_WATER) + skipHit = true; + else if ((mat.flags & MAT_FLAG_ALPHA_TEST) && diffuse.a < mat.alphaRef) + skipHit = true; + else if ((mat.flags & MAT_FLAG_ALPHA_BLEND) && diffuse.a < 0.5) + skipHit = true; + else + albedo = diffuse.rgb; + } + + if (skipHit) + { + float adv = tHit + 0.003; + rayOrigin = rayOrigin + dir * adv; + remain -= adv; + continue; + } + + if (dot(hitN, dir) > 0.0) + hitN = -hitN; + + float3 sunDir = normalize(-g_SunDir_Intensity.xyz); + float sunI = saturate(g_SunDir_Intensity.w); + float3 sunCol = max(g_SunColor_SkyWeight.rgb, float3(0.25, 0.25, 0.25)) * sunI; + float ndl = saturate(dot(hitN, sunDir)); + float3 baked = ShadeBakedFromHemi(hemi, albedo, g_HemiColor.rgb); + float3 hitCol = baked + albedo * sunCol * (0.35 + 0.65 * ndl); + hitCol = max(hitCol, albedo * 0.2); + return lerp(hitCol, skyRefl, 0.08); + } + + return skyRefl; +} + +[numthreads(8, 8, 1)] +void main(uint3 dispatchID : SV_DispatchThreadID) +{ + uint2 pixel = dispatchID.xy; + if (pixel.x >= (uint)g_ScreenSize.x || pixel.y >= (uint)g_ScreenSize.y) + return; + + float4 under4 = t_SceneColorIn.Load(int3(pixel, 0)); + float guideMark = t_WorldPos.Load(int3(pixel, 0)).w; + float4 classifyData = t_ClassifyWorldPos.Load(int3(pixel, 0)); + + if (IsHudSurfMark(guideMark) || !IsWaterSurfMark(classifyData.w)) + { + u_SceneColor[pixel] = under4; + return; + } + + float depth = t_Depth.Load(int3(pixel, 0)); + if (depth >= 1.0) + { + u_SceneColor[pixel] = under4; + return; + } + + float3 waterPos = classifyData.xyz; + float waterDist = length(waterPos - g_CameraPos.xyz); + + float4 underWP = t_UnderWorldPos.Load(int3(pixel, 0)); + float waterDepth = 8.0; + bool hasUnder = underWP.w > 0.5 && !IsWaterSurfMark(underWP.w); + if (hasUnder) + { + float underDist = length(underWP.xyz - g_CameraPos.xyz); + waterDepth = max(0.0, underDist - waterDist); + } + + float edgeFade = hasUnder ? smoothstep(0.0, 1.6, waterDepth) : 0.75; + if (edgeFade < 0.03) + { + u_SceneColor[pixel] = under4; + return; + } + + float3 N = DecodeNormal(t_Normal.Load(int3(pixel, 0)).xyz); + N = normalize(lerp(float3(0.0, 1.0, 0.0), float3(N.x, abs(N.y) + 0.05, N.z), 0.35)); + + float3 V = normalize(g_CameraPos.xyz - waterPos); + float3 R = normalize(reflect(-V, N)); + float F = FresnelSchlickWater(abs(dot(N, V))); + + float3 skyRefl = SampleSkyW(R); + float3 reflected = TraceWaterReflect(waterPos + N * 0.08, R, skyRefl); + + float3 sunDir = normalize(-g_SunDir_Intensity.xyz); + float3 sunColor = g_SunColor_SkyWeight.rgb * saturate(g_SunDir_Intensity.w); + reflected += sunColor * pow(saturate(dot(R, sunDir)), 384.0) * 0.7; + + float reflA = saturate(0.45 + 0.45 * F) * edgeFade; + float3 color = lerp(under4.rgb, reflected, reflA); + u_SceneColor[pixel] = float4(color, under4.a); +} diff --git a/res/gamedata/shaders/r5/ribbon.vs b/res/gamedata/shaders/r5/ribbon.vs index 3f25f9f35c6..96b4e4bced8 100644 --- a/res/gamedata/shaders/r5/ribbon.vs +++ b/res/gamedata/shaders/r5/ribbon.vs @@ -17,7 +17,7 @@ struct RibbonControlPoint float cumDist; // cumulative distance from head }; -StructuredBuffer g_ControlPoints : register(t10); +StructuredBuffer g_ControlPoints : register(t13); // ═══════════════════════════════════════════════════════ // Per-group constant buffer diff --git a/res/gamedata/shaders/r5/rt_common.h b/res/gamedata/shaders/r5/rt_common.h index 501608ab95b..47d9c1f8c49 100644 --- a/res/gamedata/shaders/r5/rt_common.h +++ b/res/gamedata/shaders/r5/rt_common.h @@ -33,6 +33,38 @@ float2 GetHitUV(ByteAddressBuffer megaVB, ByteAddressBuffer megaIB, return uv0 * w0 + uv1 * barycentrics.x + uv2 * barycentrics.y; } +float2 GetHitLightmapUV(ByteAddressBuffer megaVB, ByteAddressBuffer megaIB, + RTBatchInfo info, uint primitiveIndex, float2 barycentrics) +{ + uint triBase = (info.startIndex + primitiveIndex * 3); + uint i0 = megaIB.Load(triBase * 4 + 0) + info.baseVertex; + uint i1 = megaIB.Load(triBase * 4 + 4) + info.baseVertex; + uint i2 = megaIB.Load(triBase * 4 + 8) + info.baseVertex; + + float2 uv0 = asfloat(megaVB.Load2(i0 * 48 + 32)); + float2 uv1 = asfloat(megaVB.Load2(i1 * 48 + 32)); + float2 uv2 = asfloat(megaVB.Load2(i2 * 48 + 32)); + + float w0 = 1.0 - barycentrics.x - barycentrics.y; + return uv0 * w0 + uv1 * barycentrics.x + uv2 * barycentrics.y; +} + +float GetHitHemi(ByteAddressBuffer megaVB, ByteAddressBuffer megaIB, + RTBatchInfo info, uint primitiveIndex, float2 barycentrics) +{ + uint triBase = (info.startIndex + primitiveIndex * 3); + uint i0 = megaIB.Load(triBase * 4 + 0) + info.baseVertex; + uint i1 = megaIB.Load(triBase * 4 + 4) + info.baseVertex; + uint i2 = megaIB.Load(triBase * 4 + 8) + info.baseVertex; + + float h0 = float((megaVB.Load(i0 * 48 + 12) >> 24) & 0xFF) / 255.0; + float h1 = float((megaVB.Load(i1 * 48 + 12) >> 24) & 0xFF) / 255.0; + float h2 = float((megaVB.Load(i2 * 48 + 12) >> 24) & 0xFF) / 255.0; + + float w0 = 1.0 - barycentrics.x - barycentrics.y; + return saturate(h0 * w0 + h1 * barycentrics.x + h2 * barycentrics.y); +} + float3 DecodePackedNormal(uint packed) { return float3( diff --git a/res/gamedata/shaders/r5/rt_grass_alpha.h b/res/gamedata/shaders/r5/rt_grass_alpha.h new file mode 100644 index 00000000000..d66ce8835a2 --- /dev/null +++ b/res/gamedata/shaders/r5/rt_grass_alpha.h @@ -0,0 +1,23 @@ +#ifndef RT_GRASS_ALPHA_H +#define RT_GRASS_ALPHA_H + +#include "bindless_common.h" +#include "rt_common.h" + +bool GrassTexelOpaque( + ByteAddressBuffer grassVB, + ByteAddressBuffer grassIB, + StructuredBuffer batchInfo, + uint batchIdx, + uint primIdx, + float2 bary, + uint detailAtlasIndex) +{ + if (detailAtlasIndex == 0) + return true; + RTBatchInfo info = batchInfo[batchIdx]; + float2 uv = GetSkinnedHitUV(grassVB, grassIB, info, primIdx, bary); + return GetBindlessTexture(detailAtlasIndex).SampleLevel(smp_linear, uv, 0).a >= 0.3; +} + +#endif diff --git a/res/gamedata/shaders/r5/rt_grass_vertices.cs b/res/gamedata/shaders/r5/rt_grass_vertices.cs index a077aa7366c..e56e67edb0d 100644 --- a/res/gamedata/shaders/r5/rt_grass_vertices.cs +++ b/res/gamedata/shaders/r5/rt_grass_vertices.cs @@ -12,6 +12,10 @@ struct GPUSlotData { uint packed_palette_01; uint packed_palette_23; float hemi; + float sun; + float _pad0; + float _pad1; + float _pad2; }; StructuredBuffer g_AllInstances : register(t0); diff --git a/res/gamedata/shaders/r5/rt_irradiance_cache.h b/res/gamedata/shaders/r5/rt_irradiance_cache.h new file mode 100644 index 00000000000..d8f01f32bf9 --- /dev/null +++ b/res/gamedata/shaders/r5/rt_irradiance_cache.h @@ -0,0 +1,59 @@ +#ifndef RT_IRRADIANCE_CACHE_H +#define RT_IRRADIANCE_CACHE_H + +struct IrradianceCacheEntry +{ + float3 irradiance; + uint stamp; +}; + +uint IrradianceCacheHash(float3 pos, float cellSize, uint cacheSize) +{ + int3 cell = int3(floor(pos / max(cellSize, 0.05))); + uint h = (uint)cell.x * 73856093u ^ (uint)cell.y * 19349663u ^ (uint)cell.z * 83492791u; + return h % max(cacheSize, 1u); +} + +float3 QueryIrradianceCacheRW( + RWStructuredBuffer cache, + float3 pos, + float cellSize, + uint cacheSize, + uint frameIndex, + uint maxAge) +{ + if (cacheSize == 0) + return 0; + uint idx = IrradianceCacheHash(pos, cellSize, cacheSize); + IrradianceCacheEntry e = cache[idx]; + if (e.stamp == 0) + return 0; + uint age = frameIndex - e.stamp; + if (age > maxAge) + return 0; + float fade = 1.0 - saturate((float)age / (float)max(maxAge, 1u)); + return e.irradiance * fade; +} + +void UpdateIrradianceCache( + RWStructuredBuffer cache, + float3 pos, + float3 irradiance, + float cellSize, + uint cacheSize, + uint frameIndex) +{ + if (cacheSize == 0 || !any(irradiance > 0)) + return; + uint idx = IrradianceCacheHash(pos, cellSize, cacheSize); + IrradianceCacheEntry prev = cache[idx]; + float3 blended = irradiance; + if (prev.stamp != 0 && (frameIndex - prev.stamp) < 64u) + blended = lerp(prev.irradiance, irradiance, 0.35); + IrradianceCacheEntry e; + e.irradiance = blended; + e.stamp = max(frameIndex, 1u); + cache[idx] = e; +} + +#endif diff --git a/res/gamedata/shaders/r5/rt_pathtrace.cs b/res/gamedata/shaders/r5/rt_pathtrace.cs index ec3ef01f800..1552594613b 100644 --- a/res/gamedata/shaders/r5/rt_pathtrace.cs +++ b/res/gamedata/shaders/r5/rt_pathtrace.cs @@ -22,13 +22,13 @@ RaytracingAccelerationStructure g_SceneTLAS : register(t1); StructuredBuffer g_BatchInfo : register(t2); ByteAddressBuffer g_MegaVB : register(t3); -ByteAddressBuffer g_MegaIB : register(t4); TextureCube g_Sky0 : register(t5); TextureCube g_Sky1 : register(t6); ByteAddressBuffer g_SkinnedVB : register(t7); ByteAddressBuffer g_SkinnedIB : register(t11); ByteAddressBuffer g_GrassVB : register(t12); ByteAddressBuffer g_GrassIB : register(t13); +ByteAddressBuffer g_MegaIB : register(t18); RWTexture2D g_Accumulation : register(u0); RWTexture2D g_Output : register(u1); @@ -37,13 +37,13 @@ bool IsSkinnedBatch(uint batchIdx) { - return g_SkinnedBatchStart > 0 && batchIdx >= g_SkinnedBatchStart && - !(g_GrassBatchStart > 0 && batchIdx >= g_GrassBatchStart); + return g_SkinnedBatchStart != 0xFFFFFFFFu && batchIdx >= g_SkinnedBatchStart && + (g_GrassBatchStart == 0xFFFFFFFFu || batchIdx < g_GrassBatchStart); } bool IsGrassBatch(uint batchIdx) { - return g_GrassBatchStart > 0 && batchIdx >= g_GrassBatchStart; + return g_GrassBatchStart != 0xFFFFFFFFu && batchIdx >= g_GrassBatchStart; } float4 SampleTerrainTexture(uint index, float2 uv) @@ -327,8 +327,10 @@ void main(uint3 dispatchID : SV_DispatchThreadID) shadowAtten = 0.0; break; } - shadowAtten *= 0.5; - shadowOrigin = shadowOrigin + sunDir * (shadowQ.CommittedRayT() + 0.002); + float tHit = shadowQ.CommittedRayT(); + if (tHit >= 0.04) + shadowAtten *= 0.88; + shadowOrigin = shadowOrigin + sunDir * (tHit + 0.002); continue; } diff --git a/res/gamedata/shaders/r5/rt_shade_hit.h b/res/gamedata/shaders/r5/rt_shade_hit.h new file mode 100644 index 00000000000..aaf63308ad7 --- /dev/null +++ b/res/gamedata/shaders/r5/rt_shade_hit.h @@ -0,0 +1,41 @@ +#ifndef RT_SHADE_HIT_H +#define RT_SHADE_HIT_H + +#include "bindless_common.h" +#include "rt_common.h" +#include "shared/pbr_brdf.h" + +float3 ShadeBakedFromHemi(float hemi, float3 albedo, float3 hemiColor) +{ + float h = max(saturate(hemi), 0.18); + return hemiColor * h * albedo * 0.7; +} + +float3 ShadeBakedFromTerrainLmap(TerrainMaterialData tmat, float2 lmUV, float3 albedo, float3 hemiColor, float hemi) +{ + if ((tmat.flags & MAT_FLAG_HAS_LMAP) != 0 && tmat.lmapIndex != INVALID_TEXTURE_INDEX) { + float2 luv = (dot(lmUV, lmUV) > 1e-8) ? lmUV : float2(0.5, 0.5); + float3 L = GetBindlessTexture(tmat.lmapIndex).SampleLevel(smp_linear, luv, 0).rgb; + return L * albedo; + } + return ShadeBakedFromHemi(hemi, albedo, hemiColor); +} + +float3 ShadeHitDirect( + float3 albedo, + float3 N, + float3 V, + float metallic, + float roughness, + float3 sunDir, + float3 sunColor, + float shadow, + float3 baked) +{ + float3 Lo = baked; + if (shadow > 0.001) + Lo += PBRDirectLighting(albedo, N, V, sunDir, sunColor * shadow, metallic, roughness, 1); + return Lo; +} + +#endif diff --git a/res/gamedata/shaders/r5/rt_visibility.h b/res/gamedata/shaders/r5/rt_visibility.h new file mode 100644 index 00000000000..164a5688092 --- /dev/null +++ b/res/gamedata/shaders/r5/rt_visibility.h @@ -0,0 +1,180 @@ +#ifndef RT_VISIBILITY_H +#define RT_VISIBILITY_H + +#include "bindless_common.h" +#include "rt_common.h" +#include "rt_grass_alpha.h" + +static const uint RT_VIS_MAX_SKIPS = 8; + +float TraceVisibilityAtten( + RaytracingAccelerationStructure tlas, + StructuredBuffer batchInfo, + ByteAddressBuffer megaVB, + ByteAddressBuffer megaIB, + ByteAddressBuffer grassVB, + ByteAddressBuffer grassIB, + float3 origin, + float3 dir, + float tMax, + uint identityStaticCount, + uint terrainBatchCount, + uint skinnedBatchStart, + uint grassBatchStart, + uint detailAtlasIndex, + bool nearSkinnedOccludes) +{ + float atten = 1.0; + float3 shadowOrigin = origin; + float3 shadowDir = normalize(dir); + float remain = tMax; + const float selfSkip = nearSkinnedOccludes ? 0.004 : 0.02; + + for (uint si = 0; si < RT_VIS_MAX_SKIPS; si++) { + RayDesc ray; + ray.Origin = shadowOrigin; + ray.Direction = shadowDir; + ray.TMin = 0.001; + ray.TMax = remain; + + RayQuery q; + q.TraceRayInline(tlas, RAY_FLAG_NONE, 0xFF, ray); + while (q.Proceed()) { + if (q.CandidateType() == CANDIDATE_NON_OPAQUE_TRIANGLE) { + uint candBatch = q.CandidateInstanceID() + q.CandidateGeometryIndex(); + if (grassBatchStart != 0xFFFFFFFFu && candBatch >= grassBatchStart && + GrassTexelOpaque(grassVB, grassIB, batchInfo, candBatch, + q.CandidatePrimitiveIndex(), q.CandidateTriangleBarycentrics(), detailAtlasIndex)) + q.CommitNonOpaqueTriangleHit(); + } + } + + if (q.CommittedStatus() != COMMITTED_TRIANGLE_HIT) + break; + + float tHit0 = q.CommittedRayT(); + if (tHit0 < selfSkip) { + shadowOrigin = shadowOrigin + shadowDir * (tHit0 + 0.002); + remain -= (tHit0 + 0.002); + if (remain <= 0.001) break; + continue; + } + + uint sBatchIdx = q.CommittedInstanceID() + q.CommittedGeometryIndex(); + RTBatchInfo sInfo = batchInfo[sBatchIdx]; + + bool isGrass = grassBatchStart != 0xFFFFFFFFu && sBatchIdx >= grassBatchStart; + if (isGrass) { + float tHit = q.CommittedRayT(); + if (tHit < 0.1) { + shadowOrigin = shadowOrigin + shadowDir * (tHit + 0.002); + remain -= (tHit + 0.002); + if (remain <= 0.001) break; + continue; + } + atten *= (detailAtlasIndex > 0) ? 0.88 : 0.94; + if (atten < 0.18) { atten = 0.18; break; } + shadowOrigin = shadowOrigin + shadowDir * (tHit + 0.002); + remain -= (tHit + 0.002); + if (remain <= 0.001) break; + continue; + } + + MaterialData sMat = g_Materials[sInfo.materialID]; + + if (sMat.flags & MAT_FLAG_EMISSIVE) { + float tHit = q.CommittedRayT() + 0.002; + shadowOrigin = shadowOrigin + shadowDir * tHit; + remain -= tHit; + if (remain <= 0.001) break; + continue; + } + + if (sMat.flags & MAT_FLAG_WATER) { + if (nearSkinnedOccludes) { + atten = 0; + break; + } + atten *= 0.85; + float tHit = q.CommittedRayT() + 0.002; + shadowOrigin = shadowOrigin + shadowDir * tHit; + remain -= tHit; + if (remain <= 0.001) { atten = 0; break; } + continue; + } + + bool isTerrain = sBatchIdx >= identityStaticCount && + sBatchIdx < identityStaticCount + terrainBatchCount; + if (isTerrain) { atten = 0; break; } + + bool isSkinned = skinnedBatchStart != 0xFFFFFFFFu && sBatchIdx >= skinnedBatchStart && + (grassBatchStart == 0xFFFFFFFFu || sBatchIdx < grassBatchStart); + if (isSkinned) { + float tHit = q.CommittedRayT(); + if (!nearSkinnedOccludes && tHit < 0.85) { + shadowOrigin = shadowOrigin + shadowDir * (tHit + 0.002); + remain -= (tHit + 0.002); + if (remain <= 0.001) break; + continue; + } + if (!nearSkinnedOccludes) { + atten *= 0.35; + break; + } + atten = 0; + break; + } + + float2 sUV = GetHitUV(megaVB, megaIB, sInfo, q.CommittedPrimitiveIndex(), q.CommittedTriangleBarycentrics()); + float4 sDiffuse = SampleDiffuseLevel(sMat, sUV); + + if ((sMat.flags & MAT_FLAG_ALPHA_TEST) && sDiffuse.a < sMat.alphaRef) { + float tHit = q.CommittedRayT() + 0.002; + shadowOrigin = shadowOrigin + shadowDir * tHit; + remain -= tHit; + if (remain <= 0.001) { atten = 0; break; } + continue; + } + if ((sMat.flags & MAT_FLAG_ALPHA_BLEND) && sDiffuse.a < 0.5) { + atten *= (1.0 - sDiffuse.a); + float tHit = q.CommittedRayT() + 0.002; + shadowOrigin = shadowOrigin + shadowDir * tHit; + remain -= tHit; + if (remain <= 0.001) { atten = 0; break; } + continue; + } + + atten = 0; + break; + } + return atten; +} + +bool TraceVisibilityClear( + RaytracingAccelerationStructure tlas, + StructuredBuffer batchInfo, + ByteAddressBuffer megaVB, + ByteAddressBuffer megaIB, + ByteAddressBuffer grassVB, + ByteAddressBuffer grassIB, + float3 origin, + float3 target, + uint identityStaticCount, + uint terrainBatchCount, + uint skinnedBatchStart, + uint grassBatchStart, + uint detailAtlasIndex) +{ + float3 dir = target - origin; + float dist = length(dir); + if (dist < 1e-4) + return false; + float atten = TraceVisibilityAtten( + tlas, batchInfo, megaVB, megaIB, grassVB, grassIB, + origin, dir / dist, max(dist - 0.02, 0.001), + identityStaticCount, terrainBatchCount, skinnedBatchStart, grassBatchStart, + detailAtlasIndex, false); + return atten > 0.15; +} + +#endif diff --git a/res/gamedata/shaders/r5/rt_volumetric.cs b/res/gamedata/shaders/r5/rt_volumetric.cs new file mode 100644 index 00000000000..33d888132d8 --- /dev/null +++ b/res/gamedata/shaders/r5/rt_volumetric.cs @@ -0,0 +1,150 @@ +#include "common.h" +#include "rt_grass_alpha.h" + +cbuffer RTVolParams : register(b5) { + float4x4 g_InvViewProj; + float4 g_CameraPos; + float4 g_SunDir_Intensity; + float4 g_SunColor_Fog; + float2 g_ScreenSize; + float g_FogDensity; + float g_FogHeight; + uint g_Steps; + float g_FogFar; + float g_HeightFalloff; + float g_ShaftIntensity; + uint g_GrassBatchStart; + uint g_DetailAtlasIndex; + uint2 g_Pad; +}; + +RaytracingAccelerationStructure g_SceneTLAS : register(t0); +Texture2D t_Depth : register(t1); +Texture2D t_SceneColorIn : register(t2); +Texture2D t_WorldPos : register(t3); +ByteAddressBuffer g_GrassVB : register(t5); +ByteAddressBuffer g_GrassIB : register(t6); +StructuredBuffer g_BatchInfo : register(t7); + +RWTexture2D u_SceneColor : register(u0); + +bool IsGrassBatch(uint batchIdx) +{ + return g_GrassBatchStart != 0xFFFFFFFFu && batchIdx >= g_GrassBatchStart; +} + +float TraceSunVisibility(float3 origin, float3 sunDir, float maxT) +{ + RayDesc ray; + ray.Origin = origin; + ray.Direction = sunDir; + ray.TMin = 0.08; + ray.TMax = maxT; + + RayQuery q; + q.TraceRayInline(g_SceneTLAS, + RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH, + 0x03, ray); + while (q.Proceed()) { + if (q.CandidateType() == CANDIDATE_NON_OPAQUE_TRIANGLE) { + uint candBatch = q.CandidateInstanceID() + q.CandidateGeometryIndex(); + if (IsGrassBatch(candBatch) && + GrassTexelOpaque(g_GrassVB, g_GrassIB, g_BatchInfo, candBatch, + q.CandidatePrimitiveIndex(), q.CandidateTriangleBarycentrics(), g_DetailAtlasIndex)) + q.CommitNonOpaqueTriangleHit(); + } + } + return (q.CommittedStatus() == COMMITTED_TRIANGLE_HIT) ? 0.0 : 1.0; +} + +float InterleavedGradientNoise(float2 p) +{ + return frac(52.9829189 * frac(dot(p, float2(0.06711056, 0.00583715)))); +} + +[numthreads(8, 8, 1)] +void main(uint3 dispatchID : SV_DispatchThreadID) +{ + uint2 pixel = dispatchID.xy; + if (pixel.x >= (uint)g_ScreenSize.x || pixel.y >= (uint)g_ScreenSize.y) + return; + + float4 scene = t_SceneColorIn.Load(int3(pixel, 0)); + float intensity = max(g_ShaftIntensity, 0.0); + if (g_Steps == 0 || intensity < 1e-4) { + u_SceneColor[pixel] = scene; + return; + } + + float2 uv = (float2(pixel) + 0.5) * float2(1.0 / g_ScreenSize.x, 1.0 / g_ScreenSize.y); + float2 ndc = float2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0); + float3 cam = g_CameraPos.xyz; + + float4 farClip = mul(g_InvViewProj, float4(ndc, 1.0, 1.0)); + float3 farPos = farClip.xyz / max(farClip.w, 1e-6); + float3 viewDir = normalize(farPos - cam); + + uint depthW = 0, depthH = 0; + t_Depth.GetDimensions(depthW, depthH); + uint2 depthPixel = uint2( + clamp(uint(uv.x * float(max(depthW, 1u))), 0u, max(depthW, 1u) - 1u), + clamp(uint(uv.y * float(max(depthH, 1u))), 0u, max(depthH, 1u) - 1u)); + + float depth = t_Depth.Load(int3(depthPixel, 0)); + const bool isSky = depth >= 0.9995; + + float3 surfacePos; + if (isSky) { + float marchFar = min(max(g_FogFar, 900.0), 1600.0); + surfacePos = cam + viewDir * marchFar; + } else { + float3 worldPos = t_WorldPos.Load(int3(depthPixel, 0)).xyz; + if (any(abs(worldPos - cam) > 1e-3)) + surfacePos = worldPos; + else + surfacePos = cam + viewDir * min(max(g_FogFar, 900.0), 1600.0); + } + + float3 toEye = cam - surfacePos; + float dist = length(toEye); + if (dist < 0.4) { + u_SceneColor[pixel] = scene; + return; + } + + float3 sunDir = normalize(-g_SunDir_Intensity.xyz); + float3 sunColor = max(g_SunColor_Fog.xyz, float3(0.15, 0.13, 0.1)); + float sunSat = g_SunColor_Fog.w; + if (sunSat < 1e-3) + sunSat = 1.0; + + uint steps = max(g_Steps, 1u); + float jitter = InterleavedGradientNoise(float2(pixel)); + float coeff = (float(steps) - jitter) / float(steps * steps); + float3 stepW = toEye * coeff; + float density = intensity / float(steps); + + float towardSun = saturate(dot(viewDir, sunDir)); + float res = 0.0; + if (isSky) + res = intensity * pow(towardSun, 12.0); + + float3 cur = surfacePos; + float sunTraceT = 2500.0; + float cone = isSky ? pow(towardSun, 3.0) : 1.0; + [loop] for (uint i = 0; i < steps; ++i) { + float3 fromCam = cur - cam; + float viewDist = length(fromCam); + if (viewDist > 0.3) + res += density * TraceSunVisibility(cur, sunDir, sunTraceT) * cone; + cur += stepW; + } + + if (!isSky) + res *= lerp(0.35, 1.0, pow(towardSun, 4.0)); + + res *= sunSat; + float3 shafts = max(res, 0.0) * sunColor; + u_SceneColor[pixel] = float4(scene.rgb + shafts, scene.a); +} diff --git a/res/gamedata/shaders/r5/shadow/local_shadow_clear.vs b/res/gamedata/shaders/r5/shadow/local_shadow_clear.vs new file mode 100644 index 00000000000..6d660255985 --- /dev/null +++ b/res/gamedata/shaders/r5/shadow/local_shadow_clear.vs @@ -0,0 +1,12 @@ +struct VS_OUTPUT +{ + float4 position : SV_Position; +}; + +VS_OUTPUT main(uint vertexID : SV_VertexID) +{ + VS_OUTPUT output; + float2 uv = float2((vertexID == 1) ? 2.0 : 0.0, (vertexID == 2) ? 2.0 : 0.0); + output.position = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 1.0, 1.0); + return output; +} diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade_rain.ps b/res/gamedata/shaders/r5/shadow/shadow_cascade_rain.ps new file mode 100644 index 0000000000000000000000000000000000000000..429f719fa3a408750f4cedae09050a9bd22061d5 GIT binary patch literal 367 zcmYk0T}#6-6o#*xUvb<(+6zl3URib#Y&Q#?Eo=M$kr4Ya18Gv4R0Q3BH=W(w<$0d> zJ-l-+OEFY6%-*@IM6ER~Rog1*f6UPQB>x!ItnEbI>ro|G&T40lWd%wa9M-+iAE2 zcY_ICrFczOS(@N=3_ox}IL+@dC2>K{=`$wr*7M+E34w`R literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_2w_local.vs b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_2w_local.vs new file mode 100644 index 00000000000..23fd4b31cee --- /dev/null +++ b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_2w_local.vs @@ -0,0 +1,42 @@ +#define SM_6_0 +#define LOCAL_SKIN_INSTANCE_SSBO +#include "common.h" +#include "bindless_common.h" +#include "skinned_common.h" + +struct VS_INPUT_2W +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float4 tc : TEXCOORD0; + uint drawId : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; + nointerpolation uint materialID : TEXCOORD1; +}; + +cbuffer ShadowCascadeCB : register(b5) +{ + float4x4 cb_LightVP; +}; + +VS_OUTPUT main(VS_INPUT_2W v) +{ + VS_OUTPUT o; + LocalSkinInstanceGPU inst = g_LocalSkinInstances[v.drawId]; + int id_0 = int(v.tc.z); + int id_1 = int(v.tc.w); + float4x4 bone = lerp(get_bone_ofs(inst.boneOffset, id_0), get_bone_ofs(inst.boneOffset, id_1), v.N.w); + float4 skinnedPos = skinning_pos(v.P, bone); + float3 worldPos3 = mul(inst.world, skinnedPos).xyz; + o.position = mul(cb_LightVP, float4(worldPos3, 1.0)); + o.texcoord = v.tc.xy; + o.materialID = inst.materialID; + return o; +} diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_2w_mdi.vs b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_2w_mdi.vs new file mode 100644 index 00000000000..0cdc0884bc7 --- /dev/null +++ b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_2w_mdi.vs @@ -0,0 +1,41 @@ +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" +#include "skinned_mdi_common.h" + +struct VS_INPUT_2W +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float4 tc : TEXCOORD0; + uint drawIndex : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; + nointerpolation uint materialID : TEXCOORD1; +}; + +cbuffer ShadowCascadeCB : register(b5) +{ + float4x4 cb_LightVP; +}; + +VS_OUTPUT main(VS_INPUT_2W v) +{ + VS_OUTPUT o; + SkinnedDrawRecord rec = skinned_mdi_record(v.drawIndex); + int id_0 = int(v.tc.z); + int id_1 = int(v.tc.w); + float4x4 bone = lerp(mdi_get_bone(rec.boneOffset, id_0), mdi_get_bone(rec.boneOffset, id_1), v.N.w); + float4 skinnedPos = skinning_pos(v.P, bone); + float3 worldPos3 = mul(rec.world, skinnedPos).xyz; + o.position = mul(cb_LightVP, float4(worldPos3, 1.0)); + o.texcoord = v.tc.xy; + o.materialID = skinned_mdi_material(v.drawIndex); + return o; +} diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_3w_local.vs b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_3w_local.vs new file mode 100644 index 00000000000..60bb1a83e97 --- /dev/null +++ b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_3w_local.vs @@ -0,0 +1,48 @@ +#define SM_6_0 +#define LOCAL_SKIN_INSTANCE_SSBO +#include "common.h" +#include "bindless_common.h" +#include "skinned_common.h" + +struct VS_INPUT_3W +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float4 tc : TEXCOORD0; + uint drawId : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; + nointerpolation uint materialID : TEXCOORD1; +}; + +cbuffer ShadowCascadeCB : register(b5) +{ + float4x4 cb_LightVP; +}; + +VS_OUTPUT main(VS_INPUT_3W v) +{ + VS_OUTPUT o; + LocalSkinInstanceGPU inst = g_LocalSkinInstances[v.drawId]; + int id_0 = int(v.tc.z); + int id_1 = int(v.tc.w); + int id_2 = int(v.B.w * 255.0 + 0.3); + float w0 = v.N.w; + float w1 = v.T.w; + float w2 = 1.0 - w0 - w1; + float4x4 bone = get_bone_ofs(inst.boneOffset, id_0) * w0 + + get_bone_ofs(inst.boneOffset, id_1) * w1 + + get_bone_ofs(inst.boneOffset, id_2) * w2; + float4 skinnedPos = skinning_pos(v.P, bone); + float3 worldPos3 = mul(inst.world, skinnedPos).xyz; + o.position = mul(cb_LightVP, float4(worldPos3, 1.0)); + o.texcoord = v.tc.xy; + o.materialID = inst.materialID; + return o; +} diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_3w_mdi.vs b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_3w_mdi.vs new file mode 100644 index 00000000000..e2d7780df5d --- /dev/null +++ b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_3w_mdi.vs @@ -0,0 +1,47 @@ +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" +#include "skinned_mdi_common.h" + +struct VS_INPUT_3W +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float4 tc : TEXCOORD0; + uint drawIndex : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; + nointerpolation uint materialID : TEXCOORD1; +}; + +cbuffer ShadowCascadeCB : register(b5) +{ + float4x4 cb_LightVP; +}; + +VS_OUTPUT main(VS_INPUT_3W v) +{ + VS_OUTPUT o; + SkinnedDrawRecord rec = skinned_mdi_record(v.drawIndex); + int id_0 = int(v.tc.z); + int id_1 = int(v.tc.w); + int id_2 = int(v.B.w * 255.0 + 0.3); + float w0 = v.N.w; + float w1 = v.T.w; + float w2 = 1.0 - w0 - w1; + float4x4 bone = mdi_get_bone(rec.boneOffset, id_0) * w0 + + mdi_get_bone(rec.boneOffset, id_1) * w1 + + mdi_get_bone(rec.boneOffset, id_2) * w2; + float4 skinnedPos = skinning_pos(v.P, bone); + float3 worldPos3 = mul(rec.world, skinnedPos).xyz; + o.position = mul(cb_LightVP, float4(worldPos3, 1.0)); + o.texcoord = v.tc.xy; + o.materialID = skinned_mdi_material(v.drawIndex); + return o; +} diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_4w_local.vs b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_4w_local.vs new file mode 100644 index 00000000000..0970faa82ad --- /dev/null +++ b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_4w_local.vs @@ -0,0 +1,56 @@ +#define SM_6_0 +#define LOCAL_SKIN_INSTANCE_SSBO +#include "common.h" +#include "bindless_common.h" +#include "skinned_common.h" + +struct VS_INPUT_4W +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; + float4 ind : BLENDINDICES; + uint drawId : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; + nointerpolation uint materialID : TEXCOORD1; +}; + +cbuffer ShadowCascadeCB : register(b5) +{ + float4x4 cb_LightVP; +}; + +VS_OUTPUT main(VS_INPUT_4W v) +{ + VS_OUTPUT o; + LocalSkinInstanceGPU inst = g_LocalSkinInstances[v.drawId]; + + float w0 = v.N.w; + float w1 = v.T.w; + float w2 = v.B.w; + float w3 = 1.0 - w0 - w1 - w2; + + int id0 = int(v.ind.x * 255.0 + 0.3); + int id1 = int(v.ind.y * 255.0 + 0.3); + int id2 = int(v.ind.z * 255.0 + 0.3); + int id3 = int(v.ind.w * 255.0 + 0.3); + + float4x4 bone = get_bone_ofs(inst.boneOffset, id0) * w0 + + get_bone_ofs(inst.boneOffset, id1) * w1 + + get_bone_ofs(inst.boneOffset, id2) * w2 + + get_bone_ofs(inst.boneOffset, id3) * w3; + + float4 skinnedPos = skinning_pos(v.P, bone); + float3 worldPos3 = mul(inst.world, skinnedPos).xyz; + o.position = mul(cb_LightVP, float4(worldPos3, 1.0)); + o.texcoord = v.tc; + o.materialID = inst.materialID; + return o; +} diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_4w_mdi.vs b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_4w_mdi.vs new file mode 100644 index 00000000000..d2f4c4490d9 --- /dev/null +++ b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_4w_mdi.vs @@ -0,0 +1,51 @@ +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" +#include "skinned_mdi_common.h" + +struct VS_INPUT_4W +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; + float4 ind : BLENDINDICES; + uint drawIndex : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; + nointerpolation uint materialID : TEXCOORD1; +}; + +cbuffer ShadowCascadeCB : register(b5) +{ + float4x4 cb_LightVP; +}; + +VS_OUTPUT main(VS_INPUT_4W v) +{ + VS_OUTPUT o; + SkinnedDrawRecord rec = skinned_mdi_record(v.drawIndex); + float w0 = v.N.w; + float w1 = v.T.w; + float w2 = v.B.w; + float w3 = 1.0 - w0 - w1 - w2; + int id0 = int(v.ind.x * 255.0 + 0.3); + int id1 = int(v.ind.y * 255.0 + 0.3); + int id2 = int(v.ind.z * 255.0 + 0.3); + int id3 = int(v.ind.w * 255.0 + 0.3); + float4x4 bone = mdi_get_bone(rec.boneOffset, id0) * w0 + + mdi_get_bone(rec.boneOffset, id1) * w1 + + mdi_get_bone(rec.boneOffset, id2) * w2 + + mdi_get_bone(rec.boneOffset, id3) * w3; + float4 skinnedPos = skinning_pos(v.P, bone); + float3 worldPos3 = mul(rec.world, skinnedPos).xyz; + o.position = mul(cb_LightVP, float4(worldPos3, 1.0)); + o.texcoord = v.tc; + o.materialID = skinned_mdi_material(v.drawIndex); + return o; +} diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_hq_local.vs b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_hq_local.vs new file mode 100644 index 00000000000..479e94c38ea --- /dev/null +++ b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_hq_local.vs @@ -0,0 +1,41 @@ +#define SM_6_0 +#define LOCAL_SKIN_INSTANCE_SSBO +#include "common.h" +#include "bindless_common.h" +#include "skinned_common.h" + +struct VS_INPUT_1W +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; + uint drawId : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; + nointerpolation uint materialID : TEXCOORD1; +}; + +cbuffer ShadowCascadeCB : register(b5) +{ + float4x4 cb_LightVP; +}; + +VS_OUTPUT main(VS_INPUT_1W v) +{ + VS_OUTPUT o; + LocalSkinInstanceGPU inst = g_LocalSkinInstances[v.drawId]; + int boneIdx = int(v.N.w * 255.0 + 0.3); + float4x4 bone = get_bone_ofs(inst.boneOffset, boneIdx); + float4 skinnedPos = skinning_pos(v.P, bone); + float3 worldPos3 = mul(inst.world, skinnedPos).xyz; + o.position = mul(cb_LightVP, float4(worldPos3, 1.0)); + o.texcoord = v.tc; + o.materialID = inst.materialID; + return o; +} diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_hq_mdi.vs b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_hq_mdi.vs new file mode 100644 index 00000000000..8fc7de2d738 --- /dev/null +++ b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_hq_mdi.vs @@ -0,0 +1,40 @@ +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" +#include "skinned_mdi_common.h" + +struct VS_INPUT_1W +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; + uint drawIndex : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; + nointerpolation uint materialID : TEXCOORD1; +}; + +cbuffer ShadowCascadeCB : register(b5) +{ + float4x4 cb_LightVP; +}; + +VS_OUTPUT main(VS_INPUT_1W v) +{ + VS_OUTPUT o; + SkinnedDrawRecord rec = skinned_mdi_record(v.drawIndex); + int boneIdx = int(v.N.w * 255.0 + 0.3); + float4x4 bone = mdi_get_bone(rec.boneOffset, boneIdx); + float4 skinnedPos = skinning_pos(v.P, bone); + float3 worldPos3 = mul(rec.world, skinnedPos).xyz; + o.position = mul(cb_LightVP, float4(worldPos3, 1.0)); + o.texcoord = v.tc; + o.materialID = skinned_mdi_material(v.drawIndex); + return o; +} diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_local.vs b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_local.vs new file mode 100644 index 00000000000..cf1d15d29aa --- /dev/null +++ b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_local.vs @@ -0,0 +1,47 @@ +#define SM_6_0 +#define LOCAL_SKIN_INSTANCE_SSBO +#include "common.h" +#include "bindless_common.h" +#include "skinned_common.h" + +struct VS_INPUT +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; + uint drawId : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; + nointerpolation uint materialID : TEXCOORD1; +}; + +cbuffer ShadowCascadeCB : register(b5) +{ + float4x4 cb_LightVP; +}; + +float4 unpack_skinned_position(float4 v) +{ + return float4(v.xyz * 12.0, 1.0); +} + +VS_OUTPUT main(VS_INPUT input) +{ + VS_OUTPUT output; + LocalSkinInstanceGPU inst = g_LocalSkinInstances[input.drawId]; + float4 localPos = unpack_skinned_position(input.P); + int boneIdx = int(input.N.w * 255.0 + 0.3); + float4x4 bone = get_bone_ofs(inst.boneOffset, boneIdx); + float4 skinnedPos = skinning_pos(localPos, bone); + float3 worldPos3 = mul(inst.world, skinnedPos).xyz; + output.position = mul(cb_LightVP, float4(worldPos3, 1.0)); + output.texcoord = input.tc; + output.materialID = inst.materialID; + return output; +} diff --git a/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_mdi.vs b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_mdi.vs new file mode 100644 index 00000000000..cdcfc763982 --- /dev/null +++ b/res/gamedata/shaders/r5/shadow/shadow_cascade_skinned_mdi.vs @@ -0,0 +1,41 @@ +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" +#include "skinned_mdi_common.h" + +struct VS_INPUT +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; + uint drawIndex : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; + nointerpolation uint materialID : TEXCOORD1; +}; + +cbuffer ShadowCascadeCB : register(b5) +{ + float4x4 cb_LightVP; +}; + +VS_OUTPUT main(VS_INPUT v) +{ + VS_OUTPUT o; + SkinnedDrawRecord rec = skinned_mdi_record(v.drawIndex); + float4 localPos = float4(v.P.xyz * 12.0, 1.0); + int boneIdx = int(v.N.w * 255.0 + 0.3); + float4x4 bone = mdi_get_bone(rec.boneOffset, boneIdx); + float4 skinnedPos = skinning_pos(localPos, bone); + float3 worldPos3 = mul(rec.world, skinnedPos).xyz; + o.position = mul(cb_LightVP, float4(worldPos3, 1.0)); + o.texcoord = v.tc; + o.materialID = skinned_mdi_material(v.drawIndex); + return o; +} diff --git a/res/gamedata/shaders/r5/shadow_hzb_build.cs b/res/gamedata/shaders/r5/shadow_hzb_build.cs new file mode 100644 index 00000000000..c4ec3e98a0d --- /dev/null +++ b/res/gamedata/shaders/r5/shadow_hzb_build.cs @@ -0,0 +1,26 @@ +#define SM_6_0 +#include "common.h" + +cbuffer ShadowHZBParams : register(b5) +{ + uint2 g_output_dimensions; + uint g_input_mip_level; + uint g_is_first_mip; +}; + +Texture2D g_input_depth : register(t0); +RWTexture2D g_output_hiz : register(u0); + +[numthreads(8, 8, 1)] +void main(uint3 dispatch_id : SV_DispatchThreadID) +{ + if (dispatch_id.x >= g_output_dimensions.x || dispatch_id.y >= g_output_dimensions.y) + return; + + uint2 input_coord = dispatch_id.xy * 2; + float d0 = g_input_depth.Load(int3(input_coord + uint2(0, 0), g_input_mip_level)); + float d1 = g_input_depth.Load(int3(input_coord + uint2(1, 0), g_input_mip_level)); + float d2 = g_input_depth.Load(int3(input_coord + uint2(0, 1), g_input_mip_level)); + float d3 = g_input_depth.Load(int3(input_coord + uint2(1, 1), g_input_mip_level)); + g_output_hiz[dispatch_id.xy] = max(max(d0, d1), max(d2, d3)); +} diff --git a/res/gamedata/shaders/r5/shadow_mask.cs b/res/gamedata/shaders/r5/shadow_mask.cs new file mode 100644 index 00000000000..5f9a1851757 --- /dev/null +++ b/res/gamedata/shaders/r5/shadow_mask.cs @@ -0,0 +1,100 @@ +#define SM_6_0 +#define CLUSTERED_LIGHTING_FORWARD +#define CSM_SHADOW_FORWARD +#define LOCAL_SHADOW_FORWARD +#define SHADOW_MASK_CS +#include "common.h" +#include "bindless_common.h" +#include "shared/shadow_sampling.h" +#include "shared/clustered_lighting.h" + +cbuffer ShadowMaskParams : register(b5) +{ + uint2 g_maskSize; + uint2 g_fullSize; +}; + +Texture2D g_SceneDepth : register(t0); +RWTexture2D g_OutMask : register(u0); + +float3 ReconstructWorldPos(float2 uv, float rawDepth) +{ + float2 ndc = float2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0); + float4 worldH = mul(m_InvVP, float4(ndc, rawDepth, 1.0)); + return worldH.xyz / max(worldH.w, 1e-5); +} + +float3 ReconstructNormal(float2 uv, float3 worldPos) +{ + float2 texel = 1.0 / float2(g_fullSize); + float dC = g_SceneDepth.SampleLevel(smp_nofilter, uv, 0).x; + float dR = g_SceneDepth.SampleLevel(smp_nofilter, uv + float2(texel.x, 0), 0).x; + float dU = g_SceneDepth.SampleLevel(smp_nofilter, uv + float2(0, -texel.y), 0).x; + float3 pR = ReconstructWorldPos(uv + float2(texel.x, 0), dR); + float3 pU = ReconstructWorldPos(uv + float2(0, -texel.y), dU); + float3 N = normalize(cross(pR - worldPos, pU - worldPos)); + if (dot(N, N) < 1e-6) + return float3(0, 1, 0); + return N; +} + +float EvalLocalShadowProduct(float3 worldPos, float3 N, float2 pixelPos, float linearDepth) +{ + uint numLights = (uint)cluster_params.w; + if (numLights == 0) + return 1.0; + + uint clusterIdx = GetClusterIndexFromPixel(pixelPos, linearDepth); + uint2 clusterData = g_ClusterGrid[clusterIdx]; + uint lightOffset = clusterData.x; + uint lightCount = clusterData.y; + + float product = 1.0; + for (uint i = 0; i < lightCount; i++) + { + uint lightIdx = g_LightIndexList[lightOffset + i]; + GPULightData light = g_LightData[lightIdx]; + if (light.localShadowRect.w <= 0.5) + continue; + + float3 toLight = light.positionAndInvRangeSq.xyz - worldPos; + float distSq = dot(toLight, toLight); + float atten = PointLightAttenuation(distSq, light.positionAndInvRangeSq.w); + if (atten <= 0.001f) + continue; + + float soft = (light.spotParamsAndType.y > 0.5f) + ? max(dev_param_1.z, 0.1) + : max(dev_param_1.y, 0.1); + float s = SampleLocalShadow(worldPos, N, light.spotVP, light.localShadowRect, soft); + product *= saturate(s); + } + return product; +} + +[numthreads(8, 8, 1)] +void main(uint3 id : SV_DispatchThreadID) +{ + if (id.x >= g_maskSize.x || id.y >= g_maskSize.y) + return; + + float2 uv = (float2(id.xy) + 0.5) / float2(g_maskSize); + float rawD = g_SceneDepth.SampleLevel(smp_nofilter, uv, 0).x; + if (rawD >= 0.9995) + { + g_OutMask[id.xy] = float4(1, 1, 1, 1); + return; + } + + float3 worldPos = ReconstructWorldPos(uv, rawD); + float3 N = ReconstructNormal(uv, worldPos); + float2 fullPixel = uv * float2(g_fullSize); + float viewZ = abs(mul(m_V, float4(worldPos, 1.0)).z); + + float sun = SampleCSM(worldPos, N); + float3 L = normalize(-L_sun_dir_w); + float contact = ContactShadow(worldPos, L); + float localS = EvalLocalShadowProduct(worldPos, N, fullPixel, viewZ); + + g_OutMask[id.xy] = float4(saturate(sun), saturate(contact), saturate(localS), 1.0); +} diff --git a/res/gamedata/shaders/r5/shared/basecolor_pack.h b/res/gamedata/shaders/r5/shared/basecolor_pack.h new file mode 100644 index 00000000000..4189963057f --- /dev/null +++ b/res/gamedata/shaders/r5/shared/basecolor_pack.h @@ -0,0 +1,21 @@ +#ifndef BASECOLOR_PACK_H +#define BASECOLOR_PACK_H + +float PackBaseColorA(float metallic, float sssMask) +{ + if (sssMask > 0.01) + return saturate(0.5 + 0.5 * sssMask); + return saturate(metallic) * 0.499; +} + +float UnpackMetallicFromBaseA(float a) +{ + return (a < 0.5) ? saturate(a / 0.499) : 0.0; +} + +float UnpackSSSMaskFromBaseA(float a) +{ + return (a >= 0.5) ? saturate((a - 0.5) * 2.0) : 0.0; +} + +#endif diff --git a/res/gamedata/shaders/r5/shared/clustered_lighting.h b/res/gamedata/shaders/r5/shared/clustered_lighting.h index d86194c9ac8..e7aee65b16f 100644 --- a/res/gamedata/shaders/r5/shared/clustered_lighting.h +++ b/res/gamedata/shaders/r5/shared/clustered_lighting.h @@ -5,14 +5,20 @@ struct GPULightData { float4 positionAndInvRangeSq; float4 colorAndRange; float4 directionAndSpotScale; - float4 spotParamsAndType; + float4 spotParamsAndType; // .z cookie idx (0xFFFFFFFF=none); .w shadow idx/want (0xFFFFFFFF=none) float4x4 spotVP; + float4 localShadowRect; +}; + +struct GPUShadowData { + float4 atlasOffsetScale; + float4x4 viewProj; }; // Point light distance attenuation (smooth window function) float PointLightAttenuation(float distSq, float invRangeSq) { - float factor = saturate(1.0f - distSq * invRangeSq); + float factor = saturate(1.0f - distSq * abs(invRangeSq)); return factor * factor; } @@ -28,28 +34,82 @@ float SpotLightAttenuation(float3 toLight, float3 spotDir, float scale, float of StructuredBuffer g_LightData : register(t20); StructuredBuffer g_ClusterGrid : register(t21); StructuredBuffer g_LightIndexList : register(t22); +StructuredBuffer g_ShadowData : register(t50); #endif -// Compute cluster index from screen position and linear depth +// Compute cluster index from pixel coords (top-left origin, Y down) + positive view-Z. uint GetClusterIndex(float2 screenPos, float linearDepth, float3 gridDims, float4 depthParams) { uint tileX = (uint)(screenPos.x / depthParams.w); uint tileY = (uint)(screenPos.y / depthParams.w); - // Clamp to grid bounds tileX = min(tileX, (uint)gridDims.x - 1); tileY = min(tileY, (uint)gridDims.y - 1); - // Exponential depth slice float zNear = depthParams.x; float logRatio = depthParams.z; + // linearDepth must be positive view-space Z (use abs at call site) uint slice = (uint)(log2(max(linearDepth / zNear, 1.0f)) * logRatio); slice = min(slice, (uint)gridDims.z - 1); return tileX + tileY * (uint)gridDims.x + slice * (uint)gridDims.x * (uint)gridDims.y; } +// Same screen-space convention as cluster_light_assign.cs / NVRHI DX viewport +// (NDC +Y up → pixel Y down). Prefer SV_Position for shading pixels — it is the +// authoritative tile; reconstructing from worldPos can drift a few pixels and +// cross 64px tile boundaries (square flicker). +float2 ClusterWorldToScreen(float3 worldPos) +{ + float4 clipPos = mul(m_VP, float4(worldPos, 1.0)); + if (clipPos.w <= 1e-5) + return float2(0, 0); + float3 ndc = clipPos.xyz / clipPos.w; + float2 screenPos; + screenPos.x = (ndc.x * 0.5 + 0.5) * screen_res.x; + screenPos.y = (0.5 - ndc.y * 0.5) * screen_res.y; + return screenPos; +} + +uint GetClusterIndexFromWorld(float3 worldPos, float linearDepth) +{ + return GetClusterIndex( + ClusterWorldToScreen(worldPos), + linearDepth, + cluster_params.xyz, + cluster_scales); +} + +uint GetClusterIndexFromPixel(float2 pixelPos, float linearDepth) +{ + return GetClusterIndex(pixelPos, linearDepth, cluster_params.xyz, cluster_scales); +} + +// r_cluster_debug: 1 = tile XY checker, 2 = depth slice heatmap +// Prefer pixelPos (SV_Position.xy) so overlay matches EvaluateClusteredLights. +float3 ClusterDebugColor(float2 pixelPos, float linearDepth, int mode) +{ + float tileSize = cluster_scales.w; + uint tileX = (uint)(pixelPos.x / tileSize); + uint tileY = (uint)(pixelPos.y / tileSize); + uint slice = (uint)(log2(max(linearDepth / cluster_scales.x, 1.0f)) * cluster_scales.z); + slice = min(slice, (uint)cluster_params.z - 1); + if (mode == 2) + { + float t = (float)slice / max(cluster_params.z - 1.0, 1.0); + return float3(t, 1.0 - t, 0.25); + } + float rx = (float)(tileX & 1u); + float ry = (float)(tileY & 1u); + return float3(rx, ry, 1.0 - abs(rx - ry)); +} + +float3 ClusterDebugColor(float3 worldPos, float linearDepth, int mode) +{ + return ClusterDebugColor(ClusterWorldToScreen(worldPos), linearDepth, mode); +} + // Linearize depth from projection matrix depth value float LinearizeDepth(float ndcDepth, float zNear, float zFar) { @@ -66,16 +126,17 @@ float3 EvaluateClusteredLights( float3 albedo, float metallic, float roughness, - float2 screenPos, + float2 pixelPos, float linearDepth, - uint diffuseMode) + uint diffuseMode, + float localShadowMask = 1.0) { uint numLights = (uint)cluster_params.w; if (numLights == 0) return 0; - float4 depthParams = cluster_scales; - uint clusterIdx = GetClusterIndex(screenPos, linearDepth, cluster_params.xyz, depthParams); + // Pixel position is authoritative (matches rasterizer / NVRHI DX viewport). + uint clusterIdx = GetClusterIndexFromPixel(pixelPos, linearDepth); uint2 clusterData = g_ClusterGrid[clusterIdx]; uint lightOffset = clusterData.x; uint lightCount = clusterData.y; @@ -98,7 +159,7 @@ float3 EvaluateClusteredLights( if (lightType > 0.5f) { uint texIdx = asuint(light.spotParamsAndType.z); - if (texIdx != 0) + if (texIdx != 0xFFFFFFFFu) { float4 projPos = mul(light.spotVP, float4(worldPos, 1.0)); if (projPos.w > 0) @@ -126,12 +187,16 @@ float3 EvaluateClusteredLights( if (atten > 0.001f) { float shadow = 1.0; - // spotParamsAndType.w = asfloat(tile+1) when this light owns an atlas slice - uint localTile = asuint(light.spotParamsAndType.w); - if (localTile != 0) - shadow = SampleLocalShadow(worldPos, light.spotVP, localTile); + if (light.localShadowRect.w > 0.5) + { + float soft = (lightType > 0.5f) + ? max(dev_param_1.z, 0.1) + : max(dev_param_1.y, 0.1); + shadow = SampleLocalShadow( + worldPos, N, light.spotVP, + light.localShadowRect, soft); + } - // Contact history is sun-marched — do not apply to clustered local lights. float3 litColor = PBRDirectLighting( albedo, N, V, L, lightColor * atten * shadow, diff --git a/res/gamedata/shaders/r5/shared/foliage_sss.h b/res/gamedata/shaders/r5/shared/foliage_sss.h index bb670703b2c..23da4f22512 100644 --- a/res/gamedata/shaders/r5/shared/foliage_sss.h +++ b/res/gamedata/shaders/r5/shared/foliage_sss.h @@ -1,20 +1,15 @@ -// foliage_sss.h — basic subsurface / transmission for grass, leaves, bushes -// Thickness defaults to 1; pass a texture sample later (MAT_FLAG_HAS_SSS_MAP). #ifndef FOLIAGE_SSS_H #define FOLIAGE_SSS_H -// Wrap + backlit transmission (Jimenez-style cheap foliage SSS) -// Extra view↔light term so CoP density billboards still glow with sun behind them -// (flat cards often face the camera → classic -N·L transmission dies). float3 EvaluateFoliageSSS( float3 albedo, float3 N, float3 V, - float3 L, // direction toward sun + float3 L, float3 lightColor, - float shadow, // 0..1 (can be softened for transmission) + float shadow, float3 sssTint, - float thickness, // 1 = opaque leaf, lower = thinner + float thickness, float intensity) { if (intensity <= 1e-5) @@ -24,30 +19,27 @@ float3 EvaluateFoliageSSS( L = normalize(L); V = normalize(V); - const float wrap = 0.45; + float thin = saturate(thickness); + float thick = saturate(1.0 - thin); + const float wrap = lerp(0.55, 0.35, thick); float NdotL = dot(N, L); float wrapDiffuse = saturate((NdotL + wrap) / (1.0 + wrap)); - // Transmission through the blade (either face) float backLit = saturate(-NdotL); float3 H = normalize(L + N * 0.35); float VdotH = saturate(dot(V, -H)); - float scatter = pow(VdotH, 1.6) * backLit; - - // Looking into the sun through foliage (billboard-friendly) - float through = pow(saturate(dot(V, -L)), 3.0); - through *= lerp(0.35, 1.0, saturate(1.0 - abs(NdotL))); + float scatter = pow(VdotH, lerp(1.35, 2.1, thick)) * backLit; - float thin = saturate(thickness); - float term = (wrapDiffuse * 0.35 + scatter * 1.1 + through * 0.85) * thin * intensity; + float through = pow(saturate(dot(V, -L)), lerp(2.4, 3.6, thick)); + through *= lerp(0.45, 1.0, saturate(1.0 - abs(NdotL))); - float sssShadow = lerp(shadow, 1.0, 0.65); + float term = (wrapDiffuse * 0.4 + scatter * 1.15 + through * 0.95) * thin * intensity; + float sssShadow = lerp(shadow, 1.0, lerp(0.75, 0.45, thick)); return albedo * sssTint * lightColor * term * sssShadow; } #if defined(SKY_IBL) -// Soft sky bleed through foliage (cheap multi-scatter ambient) float3 EvaluateFoliageSkySSS( float3 albedo, float3 N, @@ -57,10 +49,11 @@ float3 EvaluateFoliageSkySSS( { if (intensity <= 1e-5) return float3(0, 0, 0); + float thin = saturate(thickness); float3 sky = SampleSkyRGB(-normalize(N)); float weather = length(L_ambient.rgb + L_hemi_color.rgb * L_hemi_color.w); - return albedo * sssTint * sky * weather * thickness * intensity * 0.35; + return albedo * sssTint * sky * weather * thin * intensity * 0.38; } #endif -#endif // FOLIAGE_SSS_H +#endif diff --git a/res/gamedata/shaders/r5/shared/glass_sss.h b/res/gamedata/shaders/r5/shared/glass_sss.h new file mode 100644 index 00000000000..7a31370b66a --- /dev/null +++ b/res/gamedata/shaders/r5/shared/glass_sss.h @@ -0,0 +1,32 @@ +#ifndef GLASS_SSS_H +#define GLASS_SSS_H + +float3 EvaluateGlassTransmission( + float3 albedo, + float3 N, + float3 V, + float3 L, + float3 lightColor, + float shadow, + float thickness, + float intensity) +{ + if (intensity <= 1e-5) + return float3(0, 0, 0); + + N = normalize(N); + V = normalize(V); + L = normalize(L); + + float thin = saturate(thickness); + float NdV = saturate(dot(N, V)); + float fresnel = pow(1.0 - NdV, 4.0); + float through = pow(saturate(dot(V, -L)), 2.2); + float wrap = saturate((-dot(N, L) + 0.35) / 1.35); + float term = (through * 0.85 + wrap * 0.45) * thin * intensity * (0.35 + fresnel * 0.65); + float tShadow = lerp(shadow, 1.0, 0.8); + float3 tint = lerp(albedo, float3(0.85, 0.92, 1.0), 0.55); + return tint * lightColor * term * tShadow; +} + +#endif diff --git a/res/gamedata/shaders/r5/shared/lighting_common.h b/res/gamedata/shaders/r5/shared/lighting_common.h index b2c2829dd6b..db7cb42cd6d 100644 --- a/res/gamedata/shaders/r5/shared/lighting_common.h +++ b/res/gamedata/shaders/r5/shared/lighting_common.h @@ -1,47 +1,8 @@ -// xrRender/Shaders/shared/lighting_common.h -// Phase 1.2: Stub for future lighting functions (Phase 3+) -// -// This file will eventually contain: -// - PBR BRDF functions (Cook-Torrance, GGX, Smith, Schlick) -// - Light evaluation helpers -// - Shadow sampling helpers -// -// For now, it's a placeholder for Phase 3 implementation - #ifndef LIGHTING_COMMON_H #define LIGHTING_COMMON_H -// ══════════════════════════════════════════════════════════ -// CONSTANTS -// ══════════════════════════════════════════════════════════ - -#define PI 3.14159265359 -#define INV_PI 0.31830988618 - -// ══════════════════════════════════════════════════════════ -// FUTURE: PBR BRDF FUNCTIONS (Phase 3) -// ══════════════════════════════════════════════════════════ - -// These will be implemented in Phase 3.1: -// - float D_GGX(float NoH, float roughness) // GGX normal distribution -// - float G_Smith(float NoV, float NoL, float roughness) // Smith geometric shadowing -// - float3 F_Schlick(float VoH, float3 f0) // Schlick Fresnel approximation -// - float3 EvaluatePBR(...) // Complete PBR lighting equation - -// ══════════════════════════════════════════════════════════ -// FUTURE: SHADOW SAMPLING (Phase 4) -// ══════════════════════════════════════════════════════════ - -// These will be implemented in Phase 4.2: -// - float SampleShadowCascade(...) // CSM shadow sampling with PCF -// - uint GetCascadeIndex(float depth) // Select cascade based on depth - -// ══════════════════════════════════════════════════════════ -// FUTURE: CLUSTERED LIGHTING (Phase 5) -// ══════════════════════════════════════════════════════════ - -// These will be implemented in Phase 5.4: -// - uint3 GetClusterIndex(float3 worldPos) // Map world pos to cluster -// - float3 EvaluateClusteredLights(...) // Evaluate all lights in cluster +#include "shared/pbr_brdf.h" +#include "shared/shadow_sampling.h" +#include "shared/clustered_lighting.h" -#endif // LIGHTING_COMMON_H +#endif diff --git a/res/gamedata/shaders/r5/shared/nrd_helpers.h b/res/gamedata/shaders/r5/shared/nrd_helpers.h new file mode 100644 index 00000000000..032729b093a --- /dev/null +++ b/res/gamedata/shaders/r5/shared/nrd_helpers.h @@ -0,0 +1,101 @@ +#ifndef NRD_HELPERS_H +#define NRD_HELPERS_H + +#ifndef NRD_MATERIAL_FACTOR_MIN_SCALE +#define NRD_MATERIAL_FACTOR_MIN_SCALE 0.02 +#endif +#ifndef NRD_ROUGHNESS_FACTOR_MIN_SCALE +#define NRD_ROUGHNESS_FACTOR_MIN_SCALE 0.1 +#endif +#ifndef NRD_EPS +#define NRD_EPS 1e-6 +#endif +#ifndef NRD_FP16_MAX +#define NRD_FP16_MAX 65504.0 +#endif + +float3 NRD_EnvironmentTerm_Rtg(float3 Rf0, float NoV, float roughness) +{ + float m = saturate(roughness * roughness); + + float4 X; + X.x = 1.0; + X.y = NoV; + X.z = NoV * NoV; + X.w = NoV * X.z; + + float4 Y; + Y.x = 1.0; + Y.y = m; + Y.z = m * m; + Y.w = m * Y.z; + + const float2x2 M1 = float2x2(0.99044, -1.28514, 1.29678, -0.755907); + const float3x3 M2 = float3x3(1.0, 2.92338, 59.4188, 20.3225, -27.0302, 222.592, 121.563, 626.13, 316.627); + const float2x2 M3 = float2x2(0.0365463, 3.32707, 9.0632, -9.04756); + const float3x3 M4 = float3x3(1.0, 3.59685, -1.36772, 9.04401, -16.3174, 9.22949, 5.56589, 19.7886, -20.2123); + + float bias = dot(mul(M1, X.xy), Y.xy) * rcp(max(dot(mul(M2, X.xyw), Y.xyw), NRD_EPS)); + float scale = dot(mul(M3, X.xy), Y.xy) * rcp(max(dot(mul(M4, X.xzw), Y.xyw), NRD_EPS)); + + return saturate(Rf0 * scale + bias); +} + +void NRD_MaterialFactors(float3 N, float3 V, float3 albedo, float3 Rf0, float roughness, out float3 diffFactor, out float3 specFactor) +{ + float NoV = abs(dot(N, V)); + float3 Fenv = NRD_EnvironmentTerm_Rtg(Rf0, NoV, roughness); + + diffFactor = (1.0 - Fenv) * albedo; + diffFactor = lerp(NRD_MATERIAL_FACTOR_MIN_SCALE.xxx, float3(1.0, 1.0, 1.0), diffFactor); + + specFactor = Fenv; + specFactor *= lerp(NRD_ROUGHNESS_FACTOR_MIN_SCALE.xxx, float3(1.0, 1.0, 1.0), roughness); + specFactor = lerp(NRD_MATERIAL_FACTOR_MIN_SCALE.xxx, float3(1.0, 1.0, 1.0), specFactor); +} + +float3 NRD_LinearToYCoCg(float3 color) +{ + float Y = dot(color, float3(0.25, 0.5, 0.25)); + float Co = dot(color, float3(0.5, 0.0, -0.5)); + float Cg = dot(color, float3(-0.25, 0.5, -0.25)); + return float3(Y, Co, Cg); +} + +float3 NRD_YCoCgToLinear(float3 color) +{ + float t = color.x - color.z; + float3 r; + r.y = color.x + color.z; + r.x = t + color.y; + r.z = t - color.y; + return max(r, 0.0); +} + +float NRD_GetSpecMagicCurve(float roughness) +{ + float f = 1.0 - exp2(-200.0 * roughness * roughness); + f *= pow(saturate(roughness), 0.5); + return f; +} + +float REBLUR_GetNormHitDist(float hitDist, float viewZ, float3 hitDistParams, float roughness) +{ + float smc = NRD_GetSpecMagicCurve(roughness); + float f = (hitDistParams.x + abs(viewZ) * hitDistParams.y) * lerp(hitDistParams.z, 1.0, smc); + return saturate(hitDist / max(f, 1e-4)); +} + +float NRD_TrimHitDistance(float hitDist, float threshold) +{ + return hitDist < threshold ? 0.0 : hitDist; +} + +float3 NRD_SanitizeRadiance(float3 radiance) +{ + if (any(isnan(radiance)) || any(isinf(radiance))) + return 0; + return clamp(radiance, 0.0, NRD_FP16_MAX); +} + +#endif diff --git a/res/gamedata/shaders/r5/shared/object_parallax.h b/res/gamedata/shaders/r5/shared/object_parallax.h new file mode 100644 index 00000000000..8a9b30f0eb5 --- /dev/null +++ b/res/gamedata/shaders/r5/shared/object_parallax.h @@ -0,0 +1,60 @@ +#ifndef OBJECT_PARALLAX_H +#define OBJECT_PARALLAX_H + +float2 ObjectParallaxUV(MaterialData mat, float2 uv, float3 worldPos, + float3 T, float3 B, float3 N) +{ + if (parallax.x <= 0.0) + return uv; + if ((mat.flags & MAT_FLAG_HAS_NORMAL_X) == 0 || mat.normalXIndex == INVALID_TEXTURE_INDEX) + return uv; + if (mat.flags & (MAT_FLAG_ALPHA_TEST | MAT_FLAG_FOLIAGE)) + return uv; + + float3 V = normalize(eye_position - worldPos); + float3 Vts = normalize(float3(dot(V, T), dot(V, B), dot(V, N))); + if (Vts.z < 0.02) + return uv; + + float viewZ = abs(mul(m_V, float4(worldPos, 1.0)).z); + const float startFade = 8.0; + const float stopFade = 20.0; + if (viewZ >= stopFade) + return uv; + + const float maxSamples = 16.0; + const float minSamples = 6.0; + float nSteps = lerp(maxSamples, minSamples, saturate(Vts.z)); + float stepSize = 1.0 / nSteps; + float2 vDelta = Vts.xy * (-parallax.x) * 1.2; + float2 offPerStep = stepSize * vDelta; + + Texture2D hx = GetBindlessTexture(mat.normalXIndex); + float2 curOff = uv; + float curH = 0.0; + float curBound = 1.0; + + [loop] + for (int i = 0; i < (int)nSteps; ++i) + { + if (curH < curBound) + { + curOff += offPerStep; + curH = hx.Sample(smp_linear, curOff).a; + curBound -= stepSize; + } + } + + float2 prevOff = curOff - offPerStep; + float prevH = hx.Sample(smp_linear, prevOff).a; + float d2 = (curBound + stepSize) - prevH; + float d1 = curBound - curH; + float denom = d2 - d1; + float amt = (abs(denom) > 1e-5) + ? (curBound * d2 - (curBound + stepSize) * d1) / denom + : curBound; + float fade = smoothstep(stopFade, startFade, viewZ); + return uv + vDelta * ((1.0 - amt) * fade); +} + +#endif diff --git a/res/gamedata/shaders/r5/shared/pbr_brdf.h b/res/gamedata/shaders/r5/shared/pbr_brdf.h index 16be35e9460..8611f188a6e 100644 --- a/res/gamedata/shaders/r5/shared/pbr_brdf.h +++ b/res/gamedata/shaders/r5/shared/pbr_brdf.h @@ -70,10 +70,15 @@ float3 CookTorranceSpecular( return numerator / denominator; } -// Calculate F0 (base reflectivity) from metallic and albedo +float3 SanitizeMetalAlbedo(float3 albedo, float metallic) +{ + float3 metalFloor = float3(0.08, 0.08, 0.085); + return lerp(albedo, max(albedo, metalFloor), saturate(metallic)); +} + float3 CalculateF0(float3 albedo, float metallic) { - return lerp(DIELECTRIC_F0, albedo, metallic); + return lerp(DIELECTRIC_F0, SanitizeMetalAlbedo(albedo, metallic), metallic); } // Lambertian diffuse (simplest, uniform) @@ -152,7 +157,20 @@ float3 PBRDirectLighting( return (diffuse + specular) * lightColor * NdotL; } -// Simplified ambient term (placeholder for future IBL) +float2 EnvBRDFApprox(float NdotV, float roughness) +{ + const float4 c0 = float4(-1.0, -0.0275, -0.572, 0.022); + const float4 c1 = float4(1.0, 0.0425, 1.04, -0.04); + float4 r = roughness * c0 + c1; + float a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y; + return float2(-1.04, 1.04) * a004 + r.zw; +} + +float3 EnvBRDF(float3 F0, float NdotV, float roughness, float2 lut) +{ + return F0 * lut.x + lut.y; +} + float3 PBRAmbient( float3 albedo, float3 N, @@ -169,8 +187,8 @@ float3 PBRAmbient( float3 kD = (1.0f - F) * (1.0f - metallic); float3 diffuseAmbient = kD * albedo * ambientColor; - // Approximate specular ambient (will be replaced by IBL) - float3 specularAmbient = F * ambientColor * 0.3f; + float2 brdf = EnvBRDFApprox(NdotV, roughness); + float3 specularAmbient = EnvBRDF(F0, NdotV, roughness, brdf) * ambientColor; return (diffuseAmbient + specularAmbient) * ao; } diff --git a/res/gamedata/shaders/r5/shared/shadow_sampling.h b/res/gamedata/shaders/r5/shared/shadow_sampling.h index 7b55cc73870..aa75c1dd6d0 100644 --- a/res/gamedata/shaders/r5/shared/shadow_sampling.h +++ b/res/gamedata/shaders/r5/shared/shadow_sampling.h @@ -8,6 +8,9 @@ Texture2D g_ShadowMap0 : register(t23); Texture2D g_ShadowMap1 : register(t29); Texture2D g_ShadowMap2 : register(t30); +Texture2D g_ShadowHZB0 : register(t31); +Texture2D g_ShadowHZB1 : register(t32); +Texture2D g_ShadowHZB2 : register(t33); SamplerComparisonState smp_shadowcmp : register(s4); SamplerState smp_shadow_point : register(s5); #endif @@ -17,11 +20,28 @@ Texture2DArray g_HUDShadowMap : register(t24); #endif #ifdef LOCAL_SHADOW_FORWARD -// Spot / OMNIPART local shadow atlas (t27 — free after g_ContactDepth removal) Texture2DArray g_LocalShadowAtlas : register(t27); +Texture2DArray g_LocalShadowESM : register(t51); #ifndef CSM_SHADOW_FORWARD SamplerComparisonState smp_shadowcmp : register(s4); #endif +#ifndef CSM_SHADOW_FORWARD +SamplerState smp_shadow_point : register(s5); +#endif +#endif + +#ifdef SHADOW_MASK_FORWARD +Texture2D g_ShadowMask : register(t34); + +bool ShadowMaskEnabled() +{ + return dev_param_2.z > 0.5; +} + +float3 SampleShadowMask(float2 screenUV) +{ + return g_ShadowMask.SampleLevel(smp_rtlinear, screenUV, 0).rgb; +} #endif #include "shared/contact_shadows.h" @@ -87,70 +107,103 @@ float CascadeUVRim(float2 uv) return min(d.x, d.y); } -float Gaussian11(int i) +float ShadowHZBMax(uint cascade, float2 uv, float mip) { - static const float k[11] = { - 0.0093, 0.0280, 0.0650, 0.1210, 0.1760, 0.2000, - 0.1760, 0.1210, 0.0650, 0.0280, 0.0093 - }; - return k[i + 5]; + if (cascade == 0) + return g_ShadowHZB0.SampleLevel(smp_shadow_point, uv, mip).x; + if (cascade == 1) + return g_ShadowHZB1.SampleLevel(smp_shadow_point, uv, mip).x; + return g_ShadowHZB2.SampleLevel(smp_shadow_point, uv, mip).x; } -int BlockerKernelRadius(float sunQuality) +bool ShadowHZBEnabled() { - return (sunQuality >= 2.5) ? 5 : (sunQuality >= 1.5) ? 3 : 2; + return dev_param_2.y > 0.5; } -int FilterKernelRadius(float sunQuality) +float TryShadowHZBEarlyOut(uint cascade, float2 shadowUV, float refDepth) { - // Default tier (quality 1) uses radius 3: radius 2 (5 taps) left penumbrae too - // crisp — with more taps the variable-width PCSS filter reads as genuinely soft. - return (sunQuality >= 2.5) ? 5 : (sunQuality >= 1.5) ? 4 : 3; +#if defined(SHADOW_MASK_CS) + return -1.0; +#else + if (!ShadowHZBEnabled()) + return -1.0; + float smapSize = CascadeSmapSize(cascade); + float2 hzbSize = float2(smapSize, smapSize) * 0.5; + float2 dx = abs(ddx(shadowUV)) * hzbSize; + float2 dy = abs(ddy(shadowUV)) * hzbSize; + float footprint = max(max(dx.x, dx.y), max(dy.x, dy.y)); + float mip = clamp(log2(max(footprint, 1.0)), 0.0, 6.0); + float maxD = ShadowHZBMax(cascade, shadowUV, mip); + float rd = saturate(refDepth - ReceiverBias(cascade)); + if (rd > maxD + 1e-4) + return 0.0; + return -1.0; +#endif } float SampleCascadePCF(uint cascade, float2 shadowUV, float refDepth, float softScale) { + float hzb = TryShadowHZBEarlyOut(cascade, shadowUV, refDepth); + if (hzb >= 0.0) + return hzb; float smapSize = CascadeSmapSize(cascade); - float ts = (0.6 * max(softScale, 1.0)) / smapSize; + float dsm = max(dev_param_1.x, 0.1); + float ts = (0.75 * max(softScale, 1.0) * (dsm / 0.7)) / smapSize; float rd = saturate(refDepth - ReceiverBias(cascade)); float lit = 0.0; - lit += ShadowCmpLit(float3(shadowUV + float2(-ts, -ts), (float)cascade), rd); - lit += ShadowCmpLit(float3(shadowUV + float2(+ts, -ts), (float)cascade), rd); - lit += ShadowCmpLit(float3(shadowUV + float2(-ts, +ts), (float)cascade), rd); - lit += ShadowCmpLit(float3(shadowUV + float2(+ts, +ts), (float)cascade), rd); - return lit * 0.25; + [unroll] + for (int y = -1; y <= 1; ++y) + { + [unroll] + for (int x = -1; x <= 1; ++x) + { + float2 o = float2((float)x, (float)y) * ts; + lit += ShadowCmpLit(float3(shadowUV + o, (float)cascade), rd); + } + } + return lit * (1.0 / 9.0); +} + +float2 VogelDisk(uint i, uint n) +{ + float r = sqrt((float(i) + 0.5) / float(n)); + float theta = float(i) * 2.399963229728653; + return float2(r * cos(theta), r * sin(theta)); } -float FindAverageBlockerDepth( +float2x2 ShadowDiskRotation(float2 shadowUV, float smapSize) +{ + float n = frac(sin(dot(shadowUV * smapSize, float2(12.9898, 78.233))) * 43758.5453); + float a = n * 6.28318530718; + float ca = cos(a); + float sa = sin(a); + return float2x2(ca, -sa, sa, ca); +} + +float FindAverageBlockerDepth5( uint cascade, float2 shadowUV, float refDepth, float searchRadiusUV, - int kernelRadius, out float blockerCount) { float avgBlocker = 0.0; blockerCount = 0.0; - int R = clamp(kernelRadius, 1, 5); + float thresh = refDepth - ReceiverBias(cascade) - 1e-5; [unroll] - for (int y = -5; y <= 5; ++y) + for (int y = -2; y <= 2; ++y) { - if (abs(y) > R) - continue; [unroll] - for (int x = -5; x <= 5; ++x) + for (int x = -2; x <= 2; ++x) { - if (abs(x) > R) - continue; - float2 uv = shadowUV + float2((float)x, (float)y) * searchRadiusUV; float d = ShadowDepth(cascade, uv); - if (d < refDepth - ReceiverBias(cascade) - 1e-5) + if (d < thresh) { - float w = Gaussian11(x) * Gaussian11(y); - avgBlocker += d * w; - blockerCount += w; + avgBlocker += d; + blockerCount += 1.0; } } } @@ -160,84 +213,104 @@ float FindAverageBlockerDepth( return 0.0; } -float FilterPCSS( +float FindAverageBlockerDepth7( uint cascade, float2 shadowUV, float refDepth, - float filterRadiusUV, - int kernelRadius) + float searchRadiusUV, + out float blockerCount) { - float sum = 0.0; - float wsum = 0.0; - float rd = saturate(refDepth - ReceiverBias(cascade)); - int R = clamp(kernelRadius, 1, 5); + float avgBlocker = 0.0; + blockerCount = 0.0; + float thresh = refDepth - ReceiverBias(cascade) - 1e-5; [unroll] - for (int y = -5; y <= 5; ++y) + for (int y = -3; y <= 3; ++y) { - if (abs(y) > R) - continue; [unroll] - for (int x = -5; x <= 5; ++x) + for (int x = -3; x <= 3; ++x) { - if (abs(x) > R) - continue; - - float w = Gaussian11(x) * Gaussian11(y); - float2 uv = shadowUV + float2((float)x, (float)y) * filterRadiusUV; - sum += w * ShadowCmpLit(float3(uv, (float)cascade), rd); - wsum += w; + float2 uv = shadowUV + float2((float)x, (float)y) * searchRadiusUV; + float d = ShadowDepth(cascade, uv); + if (d < thresh) + { + avgBlocker += d; + blockerCount += 1.0; + } } } - return (wsum > 1e-5) ? (sum / wsum) : 1.0; + if (blockerCount > 1e-5) + return avgBlocker / blockerCount; + return 0.0; +} + +float FilterPCSSPoisson( + uint cascade, + float2 shadowUV, + float refDepth, + float filterRadiusUV, + float smapSize, + uint tapCount) +{ + float rd = saturate(refDepth - ReceiverBias(cascade)); + float2x2 rot = ShadowDiskRotation(shadowUV, smapSize); + float sum = 0.0; + + [loop] + for (uint i = 0; i < tapCount; ++i) + { + float2 o = mul(rot, VogelDisk(i, tapCount)) * filterRadiusUV; + sum += ShadowCmpLit(float3(shadowUV + o, (float)cascade), rd); + } + + return sum / float(tapCount); } -// Full PCSS: blocker search + penumbra estimate + variable-size weighted filter. -// sunQuality in dev_param_3.y (r2_sun_quality 0..3). Tunables in dev_param_4: -// .x = r2_sun_soft (max penumbra, texels) — how soft shadows get far from the occluder -// .y = r2_sun_blocker (blocker-search spacing, texels) -// .z = r2_sun_contact (min penumbra, texels) — contact sharpness float SampleCascadePCSS(uint cascade, float2 shadowUV, float refDepth, float viewZ, float midSplit) { + float hzb = TryShadowHZBEarlyOut(cascade, shadowUV, refDepth); + if (hzb >= 0.0) + return hzb; float smapSize = CascadeSmapSize(cascade); float sunQuality = dev_param_3.y; + float dsm = max(dev_param_1.x, 0.1) / 0.7; - float softTexels = dev_param_4.x > 0.0 ? dev_param_4.x : 6.0; - float blockerTexels = dev_param_4.y > 0.0 ? dev_param_4.y : 2.5; - float contactTexels = dev_param_4.z > 0.0 ? dev_param_4.z : 1.0; + float softTexels = (dev_param_4.x > 0.0 ? dev_param_4.x : 6.0) * dsm; + float blockerTexels = (dev_param_4.y > 0.0 ? dev_param_4.y : 2.5) * dsm; + float contactTexels = (dev_param_4.z > 0.0 ? dev_param_4.z : 1.0) * dsm; - // Lowest tier: a single crisp contact-sized tap. if (sunQuality < 0.5) return SampleCascadePCF(cascade, shadowUV, refDepth, contactTexels / 0.6); - int blockerRadius = BlockerKernelRadius(sunQuality); - int filterRadius = FilterKernelRadius(sunQuality); - float blockerSearchUV = max(blockerTexels, 0.5) / smapSize; float blockerCount = 0.0; - float avgBlocker = FindAverageBlockerDepth( - cascade, shadowUV, refDepth, blockerSearchUV, blockerRadius, blockerCount); + float avgBlocker; + if (sunQuality >= 2.5) + avgBlocker = FindAverageBlockerDepth7( + cascade, shadowUV, refDepth, blockerSearchUV, blockerCount); + else + avgBlocker = FindAverageBlockerDepth5( + cascade, shadowUV, refDepth, blockerSearchUV, blockerCount); - // No occluder standing above this receiver → sharp contact tap, no false blur. if (blockerCount <= 0.12) return SampleCascadePCF(cascade, shadowUV, refDepth, contactTexels / 0.6); - // PCSS penumbra grows with the receiver→blocker gap. The gap is in normalized - // cascade depth (~0.1-0.4 for real occluders), so keep the gain low: a high gain - // saturates penumbra to max for almost everything, widening the filter so much that - // thin shadows (tree trunks/branches) only cover the center tap and wash out to lit. float gap = max(refDepth - avgBlocker, 0.0); float penumbra = saturate(gap / max(avgBlocker, 1e-3) * 3.0); float cascadeScale = 1.0 + float(cascade) * 0.3; float maxTexels = softTexels * cascadeScale; float widthTexels = clamp(lerp(contactTexels, maxTexels, penumbra), contactTexels, maxTexels); + float filterRadiusUV = widthTexels / smapSize; - // widthTexels is the half-extent of the kernel; convert to per-tap spacing so the - // filter always spans ±widthTexels regardless of tap count (smooth, no aliasing). - float filterRadiusUV = (widthTexels / float(max(filterRadius, 1))) / smapSize; - return FilterPCSS(cascade, shadowUV, refDepth, filterRadiusUV, filterRadius); + uint taps = 16; + if (sunQuality >= 2.5) + taps = 32; + else if (sunQuality >= 1.5) + taps = 24; + + return FilterPCSSPoisson(cascade, shadowUV, refDepth, filterRadiusUV, smapSize, taps); } // Try cascade c with full PCSS; returns lit in [0,1] or -1 if UV/depth invalid. @@ -272,8 +345,11 @@ float SampleCSM(float3 worldPos, float3 N) float viewZ = abs(mul(m_V, float4(worldPos, 1.0)).z); - // Soft end of shadow distance — keep most of r2_sun_far usable (was 0.72→too early). - float rangeFade = 1.0 - smoothstep(farSplit * 0.88, farSplit, viewZ); + // Soft end of shadow distance. r2_slight_fade (dev_param_1.w) extends how far + // CSM stays opaque — classic used the same cvar for light shadow LOD. + float slightFade = saturate(max(dev_param_1.w, 0.2)); + float fadeStart = farSplit * lerp(0.78, 0.92, slightFade); + float rangeFade = 1.0 - smoothstep(fadeStart, farSplit, viewZ); if (rangeFade <= 1e-3) return 1.0; @@ -302,10 +378,8 @@ float SampleCSM(float3 worldPos, float3 N) float litPrimary = -1.0; uint used = primary; [unroll] - for (uint c = 0; c < cascadeCount; ++c) + for (uint c = primary; c < cascadeCount; ++c) { - if (c < primary) - continue; float lit = TrySampleCascadePCSS(c, worldPos, N, viewZ, midSplit); if (lit >= 0.0) { @@ -365,7 +439,9 @@ float SampleCSM_Fast(float3 worldPos, float3 N) float farSplit = max(splits.z, splits.y + 1.0); float viewZ = abs(mul(m_V, float4(worldPos, 1.0)).z); - float rangeFade = 1.0 - smoothstep(farSplit * 0.88, farSplit, viewZ); + float slightFade = saturate(max(dev_param_1.w, 0.2)); + float fadeStart = farSplit * lerp(0.78, 0.92, slightFade); + float rangeFade = 1.0 - smoothstep(fadeStart, farSplit, viewZ); if (rangeFade <= 1e-3) return 1.0; @@ -377,10 +453,8 @@ float SampleCSM_Fast(float3 worldPos, float3 N) float lit = -1.0; [unroll] - for (uint c = 0; c < cascadeCount; ++c) + for (uint c = primary; c < cascadeCount; ++c) { - if (c < primary) - continue; lit = TrySampleCascadePCF(c, worldPos, N); if (lit >= 0.0) break; @@ -391,19 +465,28 @@ float SampleCSM_Fast(float3 worldPos, float3 N) return lerp(1.0, lit, rangeFade); } -// Cheap single-tap CSM for volumetrics. Sunshafts ray-march this 20-40× per pixel at -// full res; the PCSS path (blocker search + PCF kernel) there is the dominant cost and -// its soft penumbra is invisible in scattered light. One hardware comparison per probe. +float TrySampleCascadeVolumetric(uint c, float3 worldPos) +{ + float4 sc = mul(shadow_matrices[c], float4(worldPos, 1.0)); + if (!CascadeCoordValid(sc)) + return -1.0; + float rd = saturate(sc.z - ReceiverBias(c)); + return ShadowCmpLit(float3(sc.xy, (float)c), rd); +} + float SampleCSM_Volumetric(float3 worldPos) { const uint cascadeCount = 3; float3 splits = cascade_splits.xyz; + float rimWidth = cascade_splits.w > 1e-4 ? cascade_splits.w : 0.12; float farSplit = max(splits.z, splits.y + 1.0); float viewZ = abs(mul(m_V, float4(worldPos, 1.0)).z); - float rangeFade = 1.0 - smoothstep(farSplit * 0.88, farSplit, viewZ); + float slightFade = saturate(max(dev_param_1.w, 0.2)); + float fadeStart = farSplit * lerp(0.78, 0.92, slightFade); + float rangeFade = 1.0 - smoothstep(fadeStart, farSplit, viewZ); if (rangeFade <= 1e-3) - return 1.0; + return 0.0; uint primary = 2; if (viewZ < splits.x) @@ -411,19 +494,72 @@ float SampleCSM_Volumetric(float3 worldPos) else if (viewZ < splits.y) primary = 1; + float blendStart = 0.0; + float blendEnd = 0.0; + uint nextC = primary; + if (primary == 0) + { + blendEnd = splits.x; + blendStart = splits.x * 0.65; + nextC = 1; + } + else if (primary == 1) + { + blendEnd = splits.y; + blendStart = lerp(splits.x, splits.y, 0.65); + nextC = 2; + } + + float litPrimary = -1.0; + uint used = primary; [unroll] - for (uint c = 0; c < cascadeCount; ++c) + for (uint c = primary; c < cascadeCount; ++c) { - if (c < primary) - continue; - float4 sc = mul(shadow_matrices[c], float4(worldPos, 1.0)); - if (!CascadeCoordValid(sc)) - continue; - float rd = saturate(sc.z - ReceiverBias(c)); - float lit = ShadowCmpLit(float3(sc.xy, (float)c), rd); - return lerp(1.0, lit, rangeFade); + float lit = TrySampleCascadeVolumetric(c, worldPos); + if (lit >= 0.0) + { + litPrimary = lit; + used = c; + break; + } } - return 1.0; + + if (litPrimary < 0.0) + return 0.0; + + float result = litPrimary; + + if (primary < 2 && used == primary) + { + float splitBlend = saturate((viewZ - blendStart) / max(blendEnd - blendStart, 1e-3)); + splitBlend = smoothstep(0.0, 1.0, splitBlend); + if (splitBlend > 1e-3) + { + float litNext = TrySampleCascadeVolumetric(nextC, worldPos); + if (litNext >= 0.0) + result = lerp(result, litNext, splitBlend); + } + } + + float4 scUsed = mul(shadow_matrices[used], float4(worldPos, 1.0)); + float rim = saturate(CascadeUVRim(scUsed.xy) / max(rimWidth, 1e-3)); + if (rim < 0.999) + { + if (used + 1 < cascadeCount) + { + float litNext = TrySampleCascadeVolumetric(used + 1, worldPos); + if (litNext >= 0.0) + result = lerp(litNext, result, rim); + else + result = lerp(0.0, result, rim); + } + else + { + result *= smoothstep(0.0, 1.0, rim); + } + } + + return result * rangeFade; } // Overload without a normal (volumetric march, debug): no normal-offset bias. @@ -524,46 +660,92 @@ float SampleHUDShadow(float3 worldPos) #endif } -// Spot / OMNIPART local shadows. tilePlusOne = asuint(spotParamsAndType.w): -// 0 = no atlas tile, else slice = tilePlusOne - 1. spotVP is clip-space (same as -// projector sampling); UV uses Y-flip to match D3D→Vulkan viewport. -float SampleLocalShadow(float3 worldPos, float4x4 spotVP, uint tilePlusOne) +// Spot / OMNIPART local shadows. +// localShadowRect.w = (page + 1) + opacity * 0.999 (opacity = temporal fade). +// spotVP is clip-space; UV uses Y-flip to match D3D→Vulkan (NVRHI) viewport. +// softKernel = r2_ls_psm_kernel / r2_ls_ssm_kernel (classic KERNEL texels, ~0.6–0.7). +// Atlas res in dev_param_4.w (r2_smap_size × r2_ls_squality). +float SampleLocalShadow(float3 worldPos, float3 N, float4x4 spotVP, float4 shadowRect, float softKernel) { #ifndef LOCAL_SHADOW_FORWARD return 1.0; #else - if (tilePlusOne == 0) + float pagePlus = shadowRect.w; + if (pagePlus < 0.5) + return 1.0; + + float opacity = frac(pagePlus); + if (opacity < 1e-3) return 1.0; - float4 sc = mul(spotVP, float4(worldPos, 1.0)); + float scale = shadowRect.z; + if (scale < 1e-4) + return 1.0; + + float3 wp = worldPos + normalize(N) * 0.001; + + float4 sc = mul(spotVP, float4(wp, 1.0)); if (sc.w <= 1e-4) return 1.0; float3 ndc = sc.xyz / sc.w; - float2 uv = ndc.xy * 0.5 + 0.5; - uv.y = 1.0 - uv.y; + float2 lightUV = ndc.xy * 0.5 + 0.5; + lightUV.y = 1.0 - lightUV.y; float depth = ndc.z; - if (any(uv < 0.0) || any(uv > 1.0) || depth < 0.0 || depth > 1.0) + if (any(lightUV < 0.0) || any(lightUV > 1.0) || depth < 0.0 || depth > 1.0) return 1.0; - // Classic KERNEL 0.6 4-tap PCF. Soft UV rim only (like accum_sun_far edge fade) — - // hard [0,1] reject made shadows pop on/off right in front of the camera as the - // light frustum swept the floor. Keep rim narrow so long mid-frustum shadows stay. - uint slice = tilePlusOne - 1; - float ts = 0.6 / 1024.0; - float rd = saturate(depth - 0.0003); + float2 uv = lightUV * scale + shadowRect.xy; + uint page = uint(pagePlus) - 1; + + float atlas = max(floor(dev_param_4.w), 512.0); + bool useEsm = frac(dev_param_4.w) > 0.25; + float kernel = max(softKernel, 0.1); + float ts = kernel / atlas; + float2 uvMin = shadowRect.xy + (0.5 / atlas); + float2 uvMax = shadowRect.xy + scale - (0.5 / atlas); float lit = 0.0; - lit += g_LocalShadowAtlas.SampleCmp(smp_shadowcmp, float3(uv + float2(-ts, -ts), slice), rd); - lit += g_LocalShadowAtlas.SampleCmp(smp_shadowcmp, float3(uv + float2(+ts, -ts), slice), rd); - lit += g_LocalShadowAtlas.SampleCmp(smp_shadowcmp, float3(uv + float2(-ts, +ts), slice), rd); - lit += g_LocalShadowAtlas.SampleCmp(smp_shadowcmp, float3(uv + float2(+ts, +ts), slice), rd); - lit *= 0.25; - - float2 edge = min(uv, 1.0 - uv); - float rim = smoothstep(0.0, 0.05, min(edge.x, edge.y)); - return lerp(1.0, lit, rim); + + if (useEsm) + { + const float k = 80.0; + float2 o[4] = { float2(-ts, -ts), float2(+ts, -ts), float2(-ts, +ts), float2(+ts, +ts) }; + [unroll] for (int i = 0; i < 4; ++i) + { + float2 suv = clamp(uv + o[i], uvMin, uvMax); + float occluder = g_LocalShadowESM.SampleLevel(smp_shadow_point, float3(suv, page), 0).x; + float receiver = exp(k * saturate(depth)); + lit += saturate(receiver / max(occluder, 1e-4)); + } + lit *= 0.25; + } + else + { + float depthBias = max(abs(dev_param_2.x), 0.00005); + float rd = saturate(depth - depthBias); + lit += g_LocalShadowAtlas.SampleCmp(smp_shadowcmp, float3(clamp(uv + float2(-ts, -ts), uvMin, uvMax), page), rd); + lit += g_LocalShadowAtlas.SampleCmp(smp_shadowcmp, float3(clamp(uv + float2(+ts, -ts), uvMin, uvMax), page), rd); + lit += g_LocalShadowAtlas.SampleCmp(smp_shadowcmp, float3(clamp(uv + float2(-ts, +ts), uvMin, uvMax), page), rd); + lit += g_LocalShadowAtlas.SampleCmp(smp_shadowcmp, float3(clamp(uv + float2(+ts, +ts), uvMin, uvMax), page), rd); + lit *= 0.25; + } + + float2 edge = min(lightUV, 1.0 - lightUV); + float rim = smoothstep(0.0, 0.012, min(edge.x, edge.y)); + return lerp(1.0, lit, rim * opacity); #endif } +float SampleLocalShadow(float3 worldPos, float3 N, float4x4 spotVP, float4 shadowRect) +{ + float soft = max(max(dev_param_1.y, dev_param_1.z), 0.6); + return SampleLocalShadow(worldPos, N, spotVP, shadowRect, soft); +} + +float SampleLocalShadow(float3 worldPos, float4x4 spotVP, float4 shadowRect) +{ + return SampleLocalShadow(worldPos, float3(0, 1, 0), spotVP, shadowRect); +} + #endif diff --git a/res/gamedata/shaders/r5/shared/skin_sss.h b/res/gamedata/shaders/r5/shared/skin_sss.h new file mode 100644 index 00000000000..665763609da --- /dev/null +++ b/res/gamedata/shaders/r5/shared/skin_sss.h @@ -0,0 +1,94 @@ +#ifndef SKIN_SSS_H +#define SKIN_SSS_H + +float3 SkinSSSTint() +{ + return float3(1.0, 0.42, 0.32); +} + +float3 LeafSSSTint() +{ + return float3(0.5, 0.82, 0.4); +} + +float3 PetalSSSTint() +{ + return float3(0.92, 0.5, 0.58); +} + +float3 WaxSSSTint() +{ + return float3(1.0, 0.85, 0.62); +} + +void EvalSkinSSSParams( + float3 albedo, + float metallic, + float3 N, + float3 V, + float gloss, + out float sssStrength, + out float3 sssTint, + out float sssThickness) +{ + sssStrength = 0.0; + sssTint = SkinSSSTint(); + sssThickness = 1.0; + if (metallic >= 0.35) + return; + + float flesh = saturate((albedo.r - albedo.b) * 1.4 + 0.08); + flesh *= saturate(1.08 - albedo.g * 0.22); + if (flesh < 0.12) + return; + + float ndv = saturate(dot(normalize(N), normalize(V))); + sssStrength = 0.28 * flesh; + sssThickness = saturate(0.4 + ndv * 0.35 + gloss * 0.08); +} + +void ApplySSSMapSample( + float4 sssSample, + inout float sssStrength, + inout float3 sssTint, + inout float sssThickness, + out float sssMask, + out float sssProfile) +{ + sssThickness = saturate(lerp(sssThickness, 1.0 - sssSample.r, 0.65)); + float mapStr = saturate(sssSample.g) * 0.72; + sssProfile = sssSample.b * 5.01; + sssStrength = max(sssStrength, mapStr); + sssMask = 0.0; + if (sssProfile < 0.5) + { + sssTint = lerp(sssTint, SkinSSSTint(), 0.7); + sssMask = saturate(mapStr * 0.85); + sssStrength = max(sssStrength, mapStr * 0.4); + } + else if (sssProfile < 1.5) + { + sssTint = lerp(LeafSSSTint(), sssTint, 0.45); + sssMask = saturate(mapStr * 0.9); + sssStrength = max(sssStrength, mapStr * 0.48); + } + else if (sssProfile < 2.5) + { + sssTint = lerp(PetalSSSTint(), sssTint, 0.4); + sssMask = saturate(mapStr * 0.85); + sssStrength = max(sssStrength, mapStr * 0.45); + } + else if (sssProfile < 3.5) + { + sssTint = lerp(WaxSSSTint(), sssTint, 0.35); + sssMask = saturate(mapStr * 0.75); + sssStrength = max(sssStrength, mapStr * 0.4); + } + else + { + sssTint = float3(0.85, 0.92, 1.0); + sssMask = 0.0; + } +} + +#endif diff --git a/res/gamedata/shaders/r5/shared/sky_ibl.h b/res/gamedata/shaders/r5/shared/sky_ibl.h index 80f0252196c..bd0d7518a63 100644 --- a/res/gamedata/shaders/r5/shared/sky_ibl.h +++ b/res/gamedata/shaders/r5/shared/sky_ibl.h @@ -1,4 +1,3 @@ -// sky_ibl.h — IX-Ray-style sky cubemap IBL (fake irradiance + rough specular) #ifndef SKY_IBL_H #define SKY_IBL_H @@ -8,76 +7,203 @@ TextureCube s_env1 : register(t26); #define SKY_IBL_CUBES_DECLARED #endif -// L_ambient.w = skybox lerp; parallax.w = IBL intensity (0 = classic ambient) +#ifndef SKY_IBL_SPLITSUM_DECLARED +Texture2D s_envBRDF : register(t52); +StructuredBuffer g_IBLSkySH : register(t53); +StructuredBuffer g_IBLProbes : register(t54); +TextureCubeArray s_probeSpec : register(t55); +#define SKY_IBL_SPLITSUM_DECLARED +#endif + +static const uint IBL_SH_COUNT = 9; +static const uint IBL_PROBE_STRIDE = 16; +static const uint IBL_MAX_PROBES = 16; + float3 SampleSkyRGB(float3 dir) { - float3 d = normalize(dir); - float3 s0 = s_env0.SampleLevel(smp_rtlinear, d, 0.0).rgb; - float3 s1 = s_env1.SampleLevel(smp_rtlinear, d, 0.0).rgb; - return lerp(s0, s1, saturate(L_ambient.w)); + float3 d = normalize(dir); + float3 s0 = s_env0.SampleLevel(smp_rtlinear, d, 0.0).rgb; + float3 s1 = s_env1.SampleLevel(smp_rtlinear, d, 0.0).rgb; + return lerp(s0, s1, saturate(L_ambient.w)); +} + +float3 EvalSH9(StructuredBuffer sh, uint base, float3 n) +{ + float x = n.x; + float y = n.y; + float z = n.z; + float3 c = 0.0.xxx; + c += sh[base + 0].xyz * 0.282095; + c += sh[base + 1].xyz * (-0.488603 * y); + c += sh[base + 2].xyz * (0.488603 * z); + c += sh[base + 3].xyz * (-0.488603 * x); + c += sh[base + 4].xyz * (1.092548 * x * y); + c += sh[base + 5].xyz * (-1.092548 * y * z); + c += sh[base + 6].xyz * (0.315392 * (3.0 * z * z - 1.0)); + c += sh[base + 7].xyz * (-1.092548 * x * z); + c += sh[base + 8].xyz * (0.546274 * (x * x - y * y)); + return max(c, 0.0.xxx); +} + +float3 SampleSkyIrradianceSH(float3 N) +{ + float3 n = normalize(N); + float3 a = EvalSH9(g_IBLSkySH, 0, n); + float3 b = EvalSH9(g_IBLSkySH, IBL_SH_COUNT, n); + return lerp(a, b, saturate(L_ambient.w)); +} + +float RoughnessToMip(float roughness, float mipCount) +{ + return saturate(roughness) * max(mipCount - 1.0, 0.0); } -// Fake irradiance: N + hemisphere taps (cubes often lack useful mips) -float3 SampleSkyIrradiance(float3 N) +float3 SampleSkySpecularPrefiltered(float3 R, float roughness, float mipCount) { - N = normalize(N); - // Orthonormal basis around N - float3 up = abs(N.y) < 0.999 ? float3(0, 1, 0) : float3(1, 0, 0); - float3 T = normalize(cross(up, N)); - float3 B = cross(N, T); - - float3 irr = SampleSkyRGB(N) * 0.30; - irr += SampleSkyRGB(normalize(N + T * 0.55)) * 0.14; - irr += SampleSkyRGB(normalize(N - T * 0.55)) * 0.14; - irr += SampleSkyRGB(normalize(N + B * 0.55)) * 0.14; - irr += SampleSkyRGB(normalize(N - B * 0.55)) * 0.14; - irr += SampleSkyRGB(normalize(N + T * 0.35 + B * 0.35)) * 0.07; - irr += SampleSkyRGB(normalize(N - T * 0.35 + B * 0.35)) * 0.07; - return irr; + float mip = RoughnessToMip(roughness, mipCount); + float3 d = normalize(R); + float3 s0 = s_env0.SampleLevel(smp_rtlinear, d, mip).rgb; + float3 s1 = s_env1.SampleLevel(smp_rtlinear, d, mip).rgb; + return lerp(s0, s1, saturate(L_ambient.w)); } -float3 SampleSkySpecular(float3 R, float roughness) +float3 ParallaxCorrectDir(float3 R, float3 worldPos, float3 boxMin, float3 boxMax, float3 probePos) { - // Fake LOD: bias sample direction toward N for rough materials - float lodMix = saturate(roughness * roughness); - // Remap: keep some sky color even when rough (IBL_FAKE_IRRADANCE style) - float3 dir = normalize(lerp(R, float3(R.x, abs(R.y) + lodMix * 0.35, R.z), lodMix)); - float3 spec = SampleSkyRGB(dir); - // Soften with irradiance for rough - if (lodMix > 0.05) - spec = lerp(spec, SampleSkyIrradiance(dir), lodMix); - return spec; + float3 first = (boxMax - worldPos) / max(R, float3(1e-4, 1e-4, 1e-4)); + float3 second = (boxMin - worldPos) / min(R, float3(-1e-4, -1e-4, -1e-4)); + float3 further = max(first, second); + float dist = min(further.x, min(further.y, further.z)); + float3 hit = worldPos + R * max(dist, 0.0); + return normalize(hit - probePos); +} + +void SampleLocalProbe( + float3 worldPos, + float3 N, + float3 R, + float roughness, + float mipCount, + inout float3 irradiance, + inout float3 specular, + inout float probeWeight) +{ + probeWeight = 0.0; + uint count = min((uint)g_IBLProbes[0].w, IBL_MAX_PROBES); + if (count == 0) + return; + + float3 accIrr = 0.0.xxx; + float3 accSpec = 0.0.xxx; + float wSum = 0.0; + float mip = RoughnessToMip(roughness, mipCount); + float3 n = normalize(N); + + [loop] for (uint i = 0; i < count; ++i) + { + uint base = 1 + i * IBL_PROBE_STRIDE; + float3 pos = g_IBLProbes[base + 0].xyz; + float radius = max(g_IBLProbes[base + 0].w, 0.01); + float3 bmin = g_IBLProbes[base + 1].xyz; + float3 bmax = g_IBLProbes[base + 2].xyz; + float intensity = max(g_IBLProbes[base + 2].w, 0.0); + float d = length(worldPos - pos); + float w = saturate(1.0 - d / radius); + w = w * w * (3.0 - 2.0 * w); + w *= intensity; + if (any(worldPos < bmin) || any(worldPos > bmax)) + w *= 0.35; + if (w < 1e-3) + continue; + + float3 pr = ParallaxCorrectDir(R, worldPos, bmin, bmax, pos); + float3 spec = s_probeSpec.SampleLevel(smp_rtlinear, float4(pr, (float)i), mip).rgb; + float3 irr = EvalSH9(g_IBLProbes, base + 3, n); + if (dot(irr, 1.0.xxx) < 1e-4) + irr = SampleSkyIrradianceSH(n) * 0.35; + + accIrr += irr * w; + accSpec += spec * w; + wSum += w; + } + + if (wSum < 1e-3) + return; + + float influence = saturate(wSum); + irradiance = lerp(irradiance, accIrr / wSum, influence); + specular = lerp(specular, accSpec / wSum, influence); + probeWeight = influence; +} + +float3 SoftSky(float3 c) +{ + float3 x = max(c, 0.0); + float lum = dot(x, float3(0.2126, 0.7152, 0.0722)); + return x * rcp(1.0 + max(lum, 0.0) * 0.65); } float3 PBRAmbientIBL( - float3 albedo, - float3 N, - float3 V, - float metallic, - float roughness, - float ao, - float hemiOcclusion, - float intensity) + float3 albedo, + float3 N, + float3 V, + float3 worldPos, + float metallic, + float roughness, + float ao, + float hemiOcclusion, + float intensity) { - float3 F0 = CalculateF0(albedo, metallic); - float NdotV = max(dot(N, V), 0.0); - float3 F = F_SchlickRoughness(NdotV, F0, roughness); + float3 F0 = CalculateF0(albedo, metallic); + float NdotV = max(dot(N, V), 0.0); + float3 F = F_SchlickRoughness(NdotV, F0, roughness); + float hemiDir = 0.55 + 0.45 * saturate(N.y); + float3 weatherScale = max(L_ambient.rgb + L_hemi_color.rgb * L_hemi_color.w * hemiDir, 0.0); - float3 irradiance = SampleSkyIrradiance(N); - // Weather tint / night scale from classic constants - float3 weatherScale = L_ambient.rgb + L_hemi_color.rgb * L_hemi_color.w; - irradiance *= weatherScale * intensity; + float4 iblMeta = g_IBLSkySH[IBL_SH_COUNT * 2]; + float mipCount = max(iblMeta.x, 1.0); + float prefilterBlend = saturate(iblMeta.y); + float ssgiScale = saturate(iblMeta.z); - float3 kD = (1.0 - F) * (1.0 - metallic); - float3 diffuseAmbient = kD * albedo * irradiance; + float3 R = reflect(-V, N); + float lodMix = saturate(roughness * roughness); + float3 roughDir = normalize(lerp(R, float3(R.x, abs(R.y) + lodMix * 0.35, R.z), lodMix)); + float3 irrRaw = SampleSkyRGB(N); + float3 specRaw = SampleSkyRGB(roughDir); + if (lodMix > 0.05) + specRaw = lerp(specRaw, SampleSkyRGB(N), lodMix); - float3 R = reflect(-V, N); - float3 specEnv = SampleSkySpecular(R, roughness); - specEnv *= weatherScale * intensity; - // Specular ambient strength scales with gloss - float3 specularAmbient = F * specEnv * (0.25 + 0.75 * (1.0 - roughness)); + float3 irrPF = SampleSkyIrradianceSH(N); + float3 specPF = SampleSkySpecularPrefiltered(R, roughness, mipCount); + float probeW = 0.0; + SampleLocalProbe(worldPos, N, R, roughness, mipCount, irrPF, specPF, probeW); - return (diffuseAmbient + specularAmbient) * ao * saturate(hemiOcclusion); + float3 irradiance = SoftSky(lerp(irrRaw, irrPF, max(prefilterBlend, 0.65))); + float3 specEnv = SoftSky(lerp(specRaw, specPF, prefilterBlend)); + + float weatherLum = max(dot(weatherScale, float3(0.2126, 0.7152, 0.0722)), 1e-4); + float irrLum = max(dot(max(irradiance, 0.0), float3(0.2126, 0.7152, 0.0722)), 1e-4); + float weatherChroma = length(weatherScale - weatherLum.xxx); + float overcast = saturate(1.0 - weatherChroma * 6.0) * saturate(weatherLum * 1.1); + + float match = weatherLum / irrLum; + match = clamp(match, 0.75, 1.6); + irradiance *= match * max(intensity, 0.35) * max(ssgiScale, 0.65); + irradiance = max(irradiance, weatherScale * lerp(0.85, 0.7, overcast)); + + float roughSpec = saturate(1.05 - roughness * 1.15); + specEnv *= weatherScale * max(intensity, 0.35) * roughSpec; + + float3 kD = (1.0 - F) * (1.0 - metallic); + float wetComp = saturate((0.4 - roughness) * 2.5); + float3 diffuseAmbient = kD * albedo * irradiance * (1.0 + wetComp * 0.45); + + float2 lutApprox = EnvBRDFApprox(NdotV, roughness); + float2 lutSplit = s_envBRDF.SampleLevel(smp_rtlinear, float2(NdotV, saturate(roughness)), 0).rg; + float2 lut = lerp(lutApprox, lutSplit, prefilterBlend); + float3 specularAmbient = EnvBRDF(F0, NdotV, roughness, lut) * specEnv; + specularAmbient *= roughSpec; + + return (diffuseAmbient + specularAmbient) * ao * saturate(hemiOcclusion); } -#endif // SKY_IBL_H +#endif diff --git a/res/gamedata/shaders/r5/shared/ssr.h b/res/gamedata/shaders/r5/shared/ssr.h index 9be6c104d18..e56fd90d0a6 100644 --- a/res/gamedata/shaders/r5/shared/ssr.h +++ b/res/gamedata/shaders/r5/shared/ssr.h @@ -67,6 +67,14 @@ float SampleSceneViewZ(float2 uv, out float rawDepth) return abs(mul(m_V, float4(worldPos, 1.0)).z); } +// Eye-space distance to reconstructed scene position (used by SSGI march). +float SampleSceneDistance(float2 uv, out float3 scenePos, out float rawDepth) +{ + rawDepth = g_SceneDepth.SampleLevel(smp_nofilter, uv, 0).x; + scenePos = ReconstructWorld(uv, rawDepth); + return length(scenePos - eye_position); +} + float4 compute_ssr_ex( float3 position, float3 normal, @@ -100,20 +108,24 @@ float4 compute_ssr_ex( maxDist = max(maxDist, 8.0); thickness = max(thickness, 0.35); + float viewZ0 = abs(mul(m_V, float4(position, 1.0)).z); + float nearAmt = saturate(1.0 - (viewZ0 - 0.25) / 3.0); + float3 origin = position; float2 startUV = 0.0; float startW = 1.0; #ifdef SSR_OPAQUE_REFLECTOR - origin = position + N * 0.45; - thickness = max(thickness, 2.0); - steps = max(steps, 40); - maxDist = max(maxDist, 180.0); + float bias = lerp(0.45, 0.03, nearAmt); + origin = position + N * bias; + thickness = max(thickness, lerp(2.0, 0.35, nearAmt)); + steps = max(steps, nearAmt > 0.4 ? 32 : 40); + maxDist = max(maxDist, lerp(180.0, 28.0, nearAmt)); #endif WorldToUv(origin, startUV, startW); float tMin = max(0.5, maxDist / float(steps) * 0.2); #ifdef SSR_OPAQUE_REFLECTOR - tMin = max(tMin, 1.25); + tMin = lerp(max(tMin, 1.25), 0.04, nearAmt); #endif float2 hitUV = 0.0; @@ -174,8 +186,7 @@ float4 compute_ssr_ex( if (t > tMin * 1.25) { hitUV = uv; - // Low conf — sky sample stays dim via fill lerp (avoid plate washout) - hitConf = 0.22; + hitConf = 0.06; hit = true; break; } @@ -189,7 +200,8 @@ float4 compute_ssr_ex( if (delta <= thickness && delta >= -thickness * 5.0) { #ifdef SSR_OPAQUE_REFLECTOR - if (length(uv - startUV) < 0.028) + float minUvSep = lerp(0.028, 0.0035, nearAmt); + if (length(uv - startUV) < minUvSep) { prevUV = uv; tPrev = t; @@ -221,7 +233,7 @@ float4 compute_ssr_ex( } hitUV = 0.5 * (uvA + uvB); #ifdef SSR_OPAQUE_REFLECTOR - if (length(hitUV - startUV) < 0.028) + if (length(hitUV - startUV) < minUvSep) { prevUV = uv; tPrev = t; @@ -270,9 +282,9 @@ float4 compute_ssr_ex( float conf = edge * vertFade * hitConf; float2 texel = screen_res.zw; - float br = saturate(roughnessBlur) * 6.0; - float3 img = g_SceneColor.SampleLevel(smp_rtlinear, hitUV, 0).xyz; - if (br > 0.01) + float br = saturate(roughnessBlur) * 3.5; + float3 img = g_SceneColor.SampleLevel(smp_nofilter, hitUV, 0).xyz; + if (br > 0.08) { img += g_SceneColor.SampleLevel(smp_rtlinear, hitUV + float2( br, 0) * texel, 0).xyz; img += g_SceneColor.SampleLevel(smp_rtlinear, hitUV + float2(-br, 0) * texel, 0).xyz; @@ -290,9 +302,8 @@ float4 compute_ssr_ex( return float4(skybox, 0.0); conf *= saturate(hitLum * 6.0 + 0.2); #endif - // On miss-like border: blend toward skybox so content doesn't pop to black if (borderFade) - img = lerp(skybox, img, saturate(conf * 1.5)); + img = lerp(skybox * 0.2, img, saturate(conf * 2.4)); return float4(img, conf); } @@ -303,4 +314,170 @@ float4 compute_ssr(float3 position, float3 normal, float3 skybox) SSR_SAMPLES, SSR_DISTANCE, SSR_THICKNESS, SSR_REFINE, 0.0); } +float4 compute_ssr_near( + float3 position, + float3 normal, + float3 skybox, + int steps, + float maxDist, + float thickness, + int refineCount, + float roughnessBlur) +{ + float3 N = normalize(normal); + float3 V = normalize(position - eye_position); + float3 R = normalize(reflect(V, N)); + steps = clamp(steps, 12, 48); + refineCount = clamp(refineCount, 1, 6); + maxDist = clamp(maxDist, 4.0, 48.0); + thickness = max(thickness, 0.2); + + float3 origin = position + N * 0.02; + float2 startUV = 0.0; + float startW = 1.0; + WorldToUv(origin, startUV, startW); + + float tMin = 0.04; + float2 hitUV = 0.0; + bool hit = false; + float hitConf = 0.0; + bool borderFade = false; + + float tPrev = tMin; + float2 prevUV = startUV; + { + float3 p0 = origin + R * tMin; + float pw; + WorldToUv(p0, prevUV, pw); + } + + [loop] + for (int i = 1; i <= steps; ++i) + { + float u = float(i) / float(steps); + float t = max(tMin, maxDist * u * u); + float3 marchPos = origin + R * t; + + float2 uv; + float w; + if (!WorldToUv(marchPos, uv, w)) + { + if (tPrev > tMin && all(prevUV >= 0.0) && all(prevUV <= 1.0)) + { + hitUV = prevUV; + hitConf = 0.4; + hit = true; + borderFade = true; + } + break; + } + if (any(uv < 0.0) || any(uv > 1.0)) + { + hitUV = saturate(tPrev > tMin ? prevUV : uv); + hitConf = 0.35 * RayAttenBorder(hitUV, 0.08); + hit = true; + borderFade = true; + break; + } + + float rawDepth; + float sceneVZ = SampleSceneViewZ(uv, rawDepth); + float marchVZ = abs(mul(m_V, float4(marchPos, 1.0)).z); + + if (rawDepth >= 0.9995) + { + if (t > tMin * 1.5) + { + hitUV = uv; + hitConf = 0.05; + hit = true; + break; + } + prevUV = uv; + tPrev = t; + continue; + } + + float delta = sceneVZ - marchVZ; + if (delta <= thickness && delta >= -thickness * 4.0) + { + if (length(uv - startUV) < 0.0025) + { + prevUV = uv; + tPrev = t; + continue; + } + float2 uvA = prevUV; + float2 uvB = uv; + float distA = tPrev; + float distB = t; + [loop] + for (int r = 0; r < refineCount; ++r) + { + float midT = 0.5 * (distA + distB); + float2 uvM = 0.5 * (uvA + uvB); + float dM; + float sVZ = SampleSceneViewZ(uvM, dM); + float mVZ = abs(mul(m_V, float4(origin + R * midT, 1.0)).z); + if ((sVZ - mVZ) <= thickness) + { + uvB = uvM; + distB = midT; + } + else + { + uvA = uvM; + distA = midT; + } + } + hitUV = 0.5 * (uvA + uvB); + float dHit; + float sHit = SampleSceneViewZ(hitUV, dHit); + float mHit = abs(mul(m_V, float4(origin + R * (0.5 * (distA + distB)), 1.0)).z); + float err = abs(sHit - mHit); + hitConf = saturate(1.0 - err / max(thickness * 2.0, 1e-3)); + hit = hitConf > 0.03; + break; + } + else if (delta < -thickness * 4.0) + { + if (tPrev > tMin * 1.2 && all(prevUV >= 0.0) && all(prevUV <= 1.0)) + { + hitUV = prevUV; + hitConf = 0.3; + hit = true; + borderFade = true; + } + break; + } + + prevUV = uv; + tPrev = t; + } + + if (!hit) + return float4(skybox, 0.0); + + float edge = RayAttenBorder(hitUV, borderFade ? 0.1 : 0.04); + float conf = max(edge, 0.65) * hitConf; + float2 texel = screen_res.zw; + float br = saturate(roughnessBlur) * 2.0; + float3 img = g_SceneColor.SampleLevel(smp_nofilter, hitUV, 0).xyz; + if (br > 0.08) + { + img += g_SceneColor.SampleLevel(smp_rtlinear, hitUV + float2( br, 0) * texel, 0).xyz; + img += g_SceneColor.SampleLevel(smp_rtlinear, hitUV + float2(-br, 0) * texel, 0).xyz; + img += g_SceneColor.SampleLevel(smp_rtlinear, hitUV + float2(0, br) * texel, 0).xyz; + img += g_SceneColor.SampleLevel(smp_rtlinear, hitUV + float2(0, -br) * texel, 0).xyz; + img *= 0.2; + } + float hitLum = dot(max(img, 0.0), float3(0.2126, 0.7152, 0.0722)); + conf *= step(1e-5, hitLum); + img *= step(1e-5, hitLum); + if (borderFade) + img = lerp(skybox * 0.18, img, saturate(conf * 2.6)); + conf = saturate(conf); + return float4(img, conf); +} + #endif diff --git a/res/gamedata/shaders/r5/shared/surface_marks.h b/res/gamedata/shaders/r5/shared/surface_marks.h new file mode 100644 index 00000000000..7b2d3290acc --- /dev/null +++ b/res/gamedata/shaders/r5/shared/surface_marks.h @@ -0,0 +1,41 @@ +#ifndef SURFACE_MARKS_H +#define SURFACE_MARKS_H + +static const float SURF_MARK_HUD = 2.0; +static const float SURF_MARK_CHAR = 3.0; +static const float SURF_MARK_TERRAIN = 4.0; +static const float SURF_MARK_WATER = 5.0; + +bool IsHudSurfMark(float w) +{ + return w > 1.5 && w < 2.5; +} + +bool IsCharSurfMark(float w) +{ + return w > 2.5 && w < 3.5; +} + +bool IsTerrainSurfMark(float w) +{ + return w > 3.5 && w < 4.5; +} + +bool IsWaterSurfMark(float w) +{ + return w > 4.5; +} + +bool SkipRtSurfLighting(float classifyW, float guideW) +{ + if (IsHudSurfMark(guideW)) + return false; + return IsWaterSurfMark(classifyW); +} + +bool SameHudSurfClass(float a, float b) +{ + return IsHudSurfMark(a) == IsHudSurfMark(b); +} + +#endif diff --git a/res/gamedata/shaders/r5/shared/tess_pn.h b/res/gamedata/shaders/r5/shared/tess_pn.h new file mode 100644 index 00000000000..49c4ecbc3e2 --- /dev/null +++ b/res/gamedata/shaders/r5/shared/tess_pn.h @@ -0,0 +1,75 @@ +#ifndef SHARED_TESS_PN_H +#define SHARED_TESS_PN_H + +void TessComputePNPatch( + float3 P0, float3 P1, float3 P2, + float3 N0, float3 N1, float3 N2, + out float3 b210, out float3 b120, out float3 b021, + out float3 b012, out float3 b102, out float3 b201, out float3 b111, + out float3 n110, out float3 n011, out float3 n101) +{ + b210 = (2.0 * P0 + P1 - dot(P1 - P0, N0) * N0) / 3.0; + b120 = (2.0 * P1 + P0 - dot(P0 - P1, N1) * N1) / 3.0; + b021 = (2.0 * P1 + P2 - dot(P2 - P1, N1) * N1) / 3.0; + b012 = (2.0 * P2 + P1 - dot(P1 - P2, N2) * N2) / 3.0; + b102 = (2.0 * P2 + P0 - dot(P0 - P2, N2) * N2) / 3.0; + b201 = (2.0 * P0 + P2 - dot(P2 - P0, N0) * N0) / 3.0; + + float3 f3E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0; + float3 f3V = (P0 + P1 + P2) / 3.0; + b111 = f3E + ((f3E - f3V) / 2.0); + + float fV12 = 2.0 * dot(P1 - P0, N0 + N1) / max(dot(P1 - P0, P1 - P0), 1e-8); + n110 = normalize(N0 + N1 - fV12 * (P1 - P0)); + float fV23 = 2.0 * dot(P2 - P1, N1 + N2) / max(dot(P2 - P1, P2 - P1), 1e-8); + n011 = normalize(N1 + N2 - fV23 * (P2 - P1)); + float fV31 = 2.0 * dot(P0 - P2, N2 + N0) / max(dot(P0 - P2, P0 - P2), 1e-8); + n101 = normalize(N2 + N0 - fV31 * (P0 - P2)); +} + +void TessPNEvaluate( + float3 p0, float3 p1, float3 p2, + float3 n0, float3 n1, float3 n2, + float3 uvw, + out float3 pos, out float3 N) +{ + float3 N0 = normalize(n0); + float3 N1 = normalize(n1); + float3 N2 = normalize(n2); + + float3 b210, b120, b021, b012, b102, b201, b111; + float3 n110, n011, n101; + TessComputePNPatch(p0, p1, p2, N0, N1, N2, + b210, b120, b021, b012, b102, b201, b111, + n110, n011, n101); + + float u = uvw.y; + float v = uvw.x; + float w = uvw.z; + + pos = p0 * w * w * w + + p1 * u * u * u + + p2 * v * v * v + + b210 * 3.0 * w * w * u + + b120 * 3.0 * w * u * u + + b201 * 3.0 * w * w * v + + b021 * 3.0 * u * u * v + + b102 * 3.0 * w * v * v + + b012 * 3.0 * u * v * v + + b111 * 6.0 * w * u * v; + + N = n0 * w * w + + n1 * u * u + + n2 * v * v + + n110 * w * u + + n011 * u * v + + n101 * w * v; + N = normalize(N); +} + +float TessHMDisplace(float height) +{ + return height * 0.07; +} + +#endif diff --git a/res/gamedata/shaders/r5/shared/wmark.h b/res/gamedata/shaders/r5/shared/wmark.h index 7159efa7896..b3f5c8044f7 100644 --- a/res/gamedata/shaders/r5/shared/wmark.h +++ b/res/gamedata/shaders/r5/shared/wmark.h @@ -7,8 +7,6 @@ #define MAX_SHIFT 0.011f #define RANGE 100.f -uniform float3 eye_direction; - float4 wmark_shift (float3 pos, float3 norm) { float3 P = pos; @@ -16,9 +14,9 @@ float4 wmark_shift (float3 pos, float3 norm) float3 sd = eye_position-P; float d = length(sd); float w = min(d/RANGE,1.f); - float s = lerp(MIN_SHIFT,MAX_SHIFT,d); + float s = lerp(MIN_SHIFT,MAX_SHIFT,w); P += N.xyz*NORMAL_SHIFT; - P -= normalize(eye_direction + normalize(P-eye_position)) * s; + P -= normalize(camera_direction.xyz + normalize(P-eye_position)) * s; return float4 (P,1.f); } #endif diff --git a/res/gamedata/shaders/r5/skinned_common.h b/res/gamedata/shaders/r5/skinned_common.h index 0b8e44739b7..f164dc2aa53 100644 --- a/res/gamedata/shaders/r5/skinned_common.h +++ b/res/gamedata/shaders/r5/skinned_common.h @@ -3,6 +3,21 @@ StructuredBuffer g_BoneMatrices : register(t3); +#ifdef LOCAL_SKIN_INSTANCE_SSBO +struct LocalSkinInstanceGPU +{ + float4x4 world; + uint materialID; + uint boneOffset; + uint2 pad; +}; +StructuredBuffer g_LocalSkinInstances : register(t12); + +float4x4 get_bone_ofs(uint boneOffset, int legacy_index) +{ + return g_BoneMatrices[boneOffset + (legacy_index / 3)]; +} +#else cbuffer SkinnedMaterialCB : register(b4) { uint g_SkinnedMaterialID; @@ -10,25 +25,7 @@ cbuffer SkinnedMaterialCB : register(b4) uint g_SplatOffset; uint g_SplatCount; }; - -struct PaintSplat -{ - float4 posRadius; // xyz = rest-pose position, w = world-space radius - float4 color; // rgb + alpha - uint4 boneIdx; // up to 4 bone indices (local to skeleton) - float4 boneWeights; // corresponding weights (sum = 1) - float2 hitUV; // hit UV in target diffuse UV space - float uvRadius; // radius in UV space for stamp sampling - uint wallmarkMaterialID; // bindless material ID for wallmark texture - float4 evolution; // x=spawnTime, y=invLifetime, z=seed, w=mode -}; - -StructuredBuffer g_PaintSplats : register(t11); - -float4x4 get_bone(int legacy_index) -{ - return g_BoneMatrices[g_SkeletonBoneOffset + (legacy_index / 3)]; -} +#endif float4 skinning_pos(float4 pos, float4x4 bone) { @@ -45,6 +42,28 @@ float3 unpack_d3dcolor_normal(float3 packed) return packed * 2.0 - 1.0; } +#ifndef LOCAL_SKIN_INSTANCE_SSBO +float4x4 get_bone(int legacy_index) +{ + return g_BoneMatrices[g_SkeletonBoneOffset + (legacy_index / 3)]; +} +#endif + +#if !defined(LOCAL_SKIN_INSTANCE_SSBO) && !defined(SKINNED_SKIP_PAINT_SPLATS) +struct PaintSplat +{ + float4 posRadius; // xyz = rest-pose position, w = world-space radius + float4 color; // rgb + alpha + uint4 boneIdx; // up to 4 bone indices (local to skeleton) + float4 boneWeights; // corresponding weights (sum = 1) + float2 hitUV; // hit UV in target diffuse UV space + float uvRadius; // radius in UV space for stamp sampling + uint wallmarkMaterialID; // bindless material ID for wallmark texture + float4 evolution; // x=spawnTime, y=invLifetime, z=seed, w=mode +}; + +StructuredBuffer g_PaintSplats : register(t11); + float3 skin_splat_pos(PaintSplat splat) { float3 result = 0; @@ -228,5 +247,6 @@ float3 apply_splat_color(float3 albedo, float3 worldPos, float2 meshUV) } return albedo; } +#endif // !LOCAL_SKIN_INSTANCE_SSBO && !SKINNED_SKIP_PAINT_SPLATS #endif // SKINNED_COMMON_H diff --git a/res/gamedata/shaders/r5/smoke_trail.vs b/res/gamedata/shaders/r5/smoke_trail.vs index d04295f869e..37905353ae2 100644 --- a/res/gamedata/shaders/r5/smoke_trail.vs +++ b/res/gamedata/shaders/r5/smoke_trail.vs @@ -18,8 +18,8 @@ struct TrailControlPoint float cumDist; // cumulative distance from head }; -StructuredBuffer g_ControlPoints : register(t10); -ByteAddressBuffer g_TrailState : register(t11); +StructuredBuffer g_ControlPoints : register(t13); +ByteAddressBuffer g_TrailState : register(t14); // ═══════════════════════════════════════════════════════ // Per-group constant buffer (matches TrailParamsCB) diff --git a/res/gamedata/shaders/r5/ssgi_apply.ps b/res/gamedata/shaders/r5/ssgi_apply.ps index a68d519082b93a854db2463f639e517179e6c2b4..003cbf2c335a45f1d547441aed9c160dd8c86313 100644 GIT binary patch delta 350 zcmZqVJ)?6W}6(yxbsYWge>G3Y91tl2@ zn|CnIXEaer%q>x{RVYpb$|jbiYGme>q~;Z8mQ(`uC;&lbnt}#MiH(9`s;-G9SZs0v zoBU*7X0FLonB6?!I+GHMQ}v3{lb{;Ya`F>P6mnBb5_57glYv%*<`pC+XZu10+=}vZ zoq#$VHNg53H7EaP_MNQCD$TB-4YJB~vIC2xFx>FG;Kba5oK(GX&B;kDM<@HSz7_ym zV62dy2{b!5u|i{V9J6+Wj)H-n0n`P_`8oMT3fi^`*)9rN3W+&MsVVtD22cZ#mswiflI64 zBDe-hP@-dnGTu|T7;Y_QXZI&yFymy=v~91`_nf0W0#0Dr1SQ0Cph(9CQlpB>xll49 zR%n5S{2bahoULF#@Axnx1m?tuKtkF5%1_xG&5S$7U@9=NTuC=-Wh@Ph!> zL(Juv%M1u@^%a4sdu5eNphe#s;;o$;JPaU<0@OBt{duXidXRBAbx;+%{Hh(4-}S+E z%m{sx#eEOlNrhq-3bafJo;dZAW+iW2t%Fh1ESE^+a2YN``m?gx{3zcV8|8cRZ)aH^ g?71Zzc3~2@i-{P=$_=h(l&S5T?~!ig;5m9_CGoqSKd#44bEuamHVa5-3qAV1w+aKZX^2Obc#p| zyd((D{bzR}h-FQgls>jvz+K@6&;!i4kEz({^kj%ul0yR64`llx&tVGg{vR zLc^3i`7yt^glWAui{e&@_ zJ>Jjhz#J9-a@R@7hCy(Xkqj9SKje_Ax<<33e76V6*XyZSfh#q-7hQ-$TtH&Lo1(D` zqQwj?6goczfyUq_+7Qc)vN?fzwwV?Y7NjQBGVQ3c5Hsb;l@9!n;MIaSA8%2h<>Dq# z`C`GVeotogZ)ihQJq+j|!U>@#5pze_*3v^oONDA7zm|_zaW5nF5o+MrJKJ*2s2Un9 H4~+Fcs5z_a delta 445 zcmY+AF-`(O7=;5yi;b1BGbXPw+0|uPNNfa2ObpS8jRazc!|uS2S#~Bf1A@Wm5sYhT zW90$7fVZ%>_YMv&5uEa$_rCwV|6~2rY7~o5s?R!_4g{AZ_JqRM!!yp?kW@&1O#_WR zgiP@uh=+>t1l^b>A?#+eeu>it1kswx#IYW~t16I`CQWurD;CV_#hhp$BI<-G&E=#5 zv(X2aC@l**f`n;3_wv4xujLLF6eq*59&D0K?8rb`ZB*fVzJuu$OT!vUo}a$1Ty144 z7yjIxg%FaCj?;_0bHDia#u?vaIufSsm<@=g|J#ljK}dy;Ac(QKE%?$El=mt7n^KqZ`q9rk>e%QTF_&wViE~*OUFO TiyO-Nk#<}JLn%FCTHRScX>N>Q diff --git a/res/gamedata/shaders/r5/ssgi_trace.ps b/res/gamedata/shaders/r5/ssgi_trace.ps index 8048905d99757fb6d17c89ae8aa71fcefaec1ff7..890065aa0e17ffbd2c36afccdf50fcd99fff53af 100644 GIT binary patch delta 1005 zcmah|%Wl&^6qVHnQW9lTu|ssABG|!o{LDj1D;1$Cl}eF{Qi&Cs#N*bgAJNz@6)du3 ziK1B{vEUaf?-3i;d;tHzH*m)>$%GAF*;nVDd(OSbUw41*&5wukyB$fN|9HG%D1G)& zdjGV7;PHYOr^ur?BqxzeZu_gMh2BoF+s||9P|9~VK1wYM4gK_m?|E55&avy`XhP(I z!9-VnI=WCQA6ES~v#Zu@jSxUb!I-!)IzS0dGm29pL-9FIh(<5Rih$;fFG-4nz@Gqo z6ixBu`fuTw#vyMyT6W`qd(Y$S@(NXjL2OXpYZN+Fq$}DxvBToyPW==nh4@xxzbobp zP?`>)l_d@;{fiC^O-K8p-jZ2BHxE!i=v3A<$59b9sA~%-#478vW?WGgmWtdsILn{}1$-{|h|LMeKYJ$;+lgP}DO~qammnaFtyq)m!=J`aS6nEm|@S delta 512 zcmYk2K~EDw7>2V13MrBpAu$GwU+QHS+G%$i;LzZK8ij)-#)}6{Gu{2VJ7IQbGc!xk z6I|4rHtP@Y7kJsk-@uI@z>PnmA8kM;bD8JOeDC{ym(PDad)01(F|im}%|m3lf$OuM zU_yj@6 zbs8TJ-i4UqOuu`+#M~w@(kdl?%@S~J&}rMi_a^6`*Ma_V10$)pg%Qo|?*oN77Fk?1 zoEJ`u^!_wVYm6dJEJpWJ8J^}6fooM{5gb*6xo=MSrS_+c7k@XJU`2}Buv6Fsf0uP( z>z0g(beUhiUD|o{pME0RVI9Bvx}@fp6Jcjn_MqHY2%KKY>ucrX`QSa%@vv8idxuiK z5i$ZUhZrex$t;Y8O;kax3MnDNR7Hj4R;UbIexgJ~tr4Rkm4%^H<0AK_22W}a_GZ4D zAW`S;bT?hsZ^F&>nqassG`Hwd!hLS=rXAZRp~Je{9>rW%#+Eto;N^Uj}_&VVn0l*ro8h3a-*m#KU=GuQ`GrH@(;E6wpruP>P|%#FR@v; z5uMw`9TE-fY<+WFMI%RcOB`G4^d!7?NQYpD@Nh-ex&E_Q&SA0(u%ONCc0fOV(7C>b zy|Vp@LBCgPVq~_&yd!cU3uH9ic$BMBMc`jAU=;fgm7j&NN;FnMTQX;tBLlMU5!aJ3 zbdUvG5nA32YfC$&;8Z1Sb8eN1e-`O!msO8dpzYHMub|_hkq7i*mOWA7dh-dam_k7B zpCZZ9DEgOH*acRY7oW(#7UgDjO@(_McccEk1bz>)YCaOP^ku(%TPS#EH zo@K@AwssL_RJpar{*AT-IVMq>6CTNyYQbE(fE{}>!@TsnN4En4oI~1a{L?b*d+a4NJGmnG-m1D?N2w4s9pfCOF*P|_ zq_lIb@~%8hi$g4$2g@3|Ve{+Q@4-+do6siYaERKyJ22Q<6fbQ`9PMU?+hhaFS&!RK z*172cF{c}7>VrE{$vWRyoM_DH5XDDJ%c*bguYJAgc41&JnFqsnee2rp%+OT)Lwaxa zM9adZRd1u#Hx=a}RV|F=egZcxYOaOrgVXu8VZjeX85U8tNHvQuEScst^WK5ol->$T zaadlXlNc8~b`F|10W*%6wgDqE8XraD%EP z=j5*u{$v<&?Tx+@N{gH=FI*Kso~uxSca&-@)SAbE3Rq;Ws^MkYeJ^mHjuj+r-2*g5 jys40*4y$l;p?9;hfULVKLhPR#Ou)3VXE>Ft{bT<>qJv=; delta 434 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 literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/sssss_blur.ps b/res/gamedata/shaders/r5/sssss_blur.ps new file mode 100644 index 0000000000000000000000000000000000000000..8c51f97f076618d74af41fb115351b8405d8a55e GIT binary patch literal 2462 zcmb7G+iu%N5PjEI43d|XEy>g^g{vq)95;r6+A$)hDf%L;$Q88+F9I*NYQz8DGrLQQ zk}5dS6HT5ob7uC;tltJ|sS`!BoB6xB*LtfHKPm%7Z}OGQRB*PCx$@H}&E{+A|LCpW zv|2M6uOpSs3Rx)1Va*W<)#4K9H?@wJFL>4K691 zf8u;iNWsCHh)cCDRyzm|(nQ>*SuCR+$Y2k0fzoyWLl)vM%Hhnl#?hqYjGI%$HFUOL>v0BrH}kT=#ba$$!1lt z%zuFWEnzF6yEpjzbC$*&?4vNjaRPO3hh&Ur@)=7UB(!#Fq|3qu%HjxmNuur|eITzl zZkrGMF;nnb;(8`nRtHUhxD((8jc0G%p?;6uR|hq?_rQ6BV*!oq9inM8`6|I^{9_6- z4n8QTnxsZa8`}}Cr{aH@BK`UK`O%7w2pH0>kK~CN4LDYWb2}zKrQ_cxI;Fnqj+7<}c2D;2a@gwX)fmb*i9l=R1*a;2cns$d^ zrateTZ4-u+moca?P;awwr)&H%q{qEamW5r&DM*9WwW%lgn9%8|wqATT47Rv+ z+>$xwQD~>X{x~44gsB--9Bg%rC$!YoaWxQ%fpcs5z$(=Yx`8(n$W;DW5nSz&nSXVW zRBL2kJUtmnuT7DoyDry~FbG`IFAYK!YJUvspcJqsxyZ#6b*ouF&>is2^&LR9S&J?x zC-e?hIKWwL3|yy=B9~ZLxNb#|7UI_LKtH={^gl%2PfWuNy~d0U>;{9Q_frfh1k5^5 z)V|?zj5Go7uo>EPMq<$8nRDY?JwUJ6@SK zY>mu=bvlKSv1+z&&Aj2Yd-bw2ri-Y|@E;ITF!{`^1SVjjRd?jX*!EFc7Q9(a&)xEM ZbgYl+?A#4g-S<^JyO$A1s;-Wg)_;74)uaFb literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/taa.ps b/res/gamedata/shaders/r5/taa.ps index c933ab2eff359e68be46c34930ed571efdb6fe52..be2a1ef9c9e9a83332a404e2e2f86db750fe2768 100644 GIT binary patch delta 461 zcmaE1_11Pnm5_W|PJUvEkwR%%XkJ!kNl9vvf-R7vpsN7kO%4`P-~3ESnsKtSh^}8| znu11deo1D2UO!mv8g7P0uWS#Ne~Mn4K&0+&%hc@6sQSFyms<;VfD!YBB!L0 z%`{BaHG!B3GR=N+A*ZTHX_=0Kfu>$jdJ^0~tI3k0Mw6fM@z*Qll;%2@f}EY3SfP=U zU!sv*T2z#pSE8c;_M5Q=(3wE}dd3zY(#S|tQ%3=83&MF|g&>1GGK)))Ov(Uq^NT9+ znN*L}sMLyr{NmE0)ZpaAoK&EJ$vKI+1sYJ7>ceaTxyMvT!AQ?Q6X6!9oeEmE3Mi&P ZRMc{TgFs(j!7nv4JtHZ9bEv2=2LKz-fPnx2 delta 135 zcmaEB`@(8Nl~Ag_zCv(GVp2}3LP=_FL4HwUjzUptaehu&s)9yAQGQlxa!F=>oPj1)@CLi3}e z$RPFJEN|ZEQ^t0@WU%>>%oEqLVs?@T!Tbl1$y+Ai1_aylMlwO9Yu}#Vf7#^~mG=l^ zr@Cs8Gs1d^-O!@Wnb=4w88k(m2$^FpI*Qt~QLS`)*zK5rxGGse>opa$Xodl|u5JCb z0Dmek?33dgm+W?Ql~$z`w2^~bACWc9Zmkc@(nxVgC9F5e@@4%qhC#p?U=v*Os>H0D zU&(s4S;ouN%P5p&r?jd>hLHI2_xIK6^}+2TcSS{qf|s;R8Byl|Qd6{GsA>sWB}qEz zS_p{i1QWo=qJ8?ljY51_mZm=Oyx^(U!^9K&t!6`$(wv!t(!v~!3VrVV7Ve;+CxUeN zY?!XoDt}fq(x7t_2)8HTvCH`gc=R<=WyOFxI|73dFp>x)5h94^xNrdxOyP)q;-9%0 z5&|r;u33hxu2O;#rVwdN|FW)*q;03WXvK4}&i2N%uBr}NLEHYc_zlGwpcOR_UmCSx zS)##nM&DWXENI4ADPJny=sEgO_gZ#y+zFbJ(08sE{8s^DEDD z!?`Na4Hvc%qHj zPJ9ZUzi<%hcV-*J@4SQd(2ETB?HD~9zM9oW5oQjq*SRJ$T`UV}{b|VTFK++~=*X#7 zV!To6c4C%6TiDRCu|eL9%QyTs-TmJt-5|^t#5w)3;S&1^hXq{RfethUKMdRIR1WP< SHMSY)XRGRK*H5kKhxHHJXtuEc delta 642 zcmaJCN2c=0>rYJ?9l+J}s5q`>*r*1QT8U+k`D7?(c!L}0yDUCkOCR%!B zw2Sj4UKq7%%C|@FvDZ5oAgrUb6PGi#R-@d*olP;aB2}cli{jwx^|!mzM{V>fo%0`S zk7rTvIRGC!8e+S<(AWv;RHbB9>Mm-jP}-Ar%1^UnjtfssB?qIPRnSPxJO_hJmUP$WN?BSdW6R88_JM>XNM1Gi(%bI g_ControlPoints : register(t10); -ByteAddressBuffer g_TrailState : register(t11); // GPU-driven: {head, totalSpawned, liveCount, totalDist_bits} +StructuredBuffer g_ControlPoints : register(t13); +ByteAddressBuffer g_TrailState : register(t14); // GPU-driven: {head, totalSpawned, liveCount, totalDist_bits} // ═══════════════════════════════════════════════════════ // Per-group constant buffer diff --git a/res/gamedata/shaders/r5/volumetric/apply_lighting.cs b/res/gamedata/shaders/r5/volumetric/apply_lighting.cs index 78228142ef7..df46d939bed 100644 --- a/res/gamedata/shaders/r5/volumetric/apply_lighting.cs +++ b/res/gamedata/shaders/r5/volumetric/apply_lighting.cs @@ -1,8 +1,54 @@ -// apply_lighting.cs — sun lighting of froxel density (unshadowed MVP) +#define SM_6_0 +#define CSM_SHADOW_FORWARD +#include "common.h" +#include "shared/shadow_sampling.h" +#include "shared/clustered_lighting.h" #include "volumetric/shared_froxel.h" RWTexture3D u_FroxelVolume : register(u0); +StructuredBuffer g_LightData : register(t20); +StructuredBuffer g_ClusterGrid : register(t21); +StructuredBuffer g_LightIndexList : register(t22); + +float3 EvaluateClusteredLightsVolumetric(float3 worldPos, float linearDepth) +{ + uint numLights = (uint)cluster_params.w; + if (numLights == 0) + return 0; + + uint clusterIdx = GetClusterIndexFromWorld(worldPos, linearDepth); + uint2 clusterData = g_ClusterGrid[clusterIdx]; + uint lightOffset = clusterData.x; + uint lightCount = clusterData.y; + + float3 total = 0; + for (uint i = 0; i < lightCount; i++) + { + uint lightIdx = g_LightIndexList[lightOffset + i]; + GPULightData light = g_LightData[lightIdx]; + float3 lightPos = light.positionAndInvRangeSq.xyz; + float invRangeSq = light.positionAndInvRangeSq.w; + float3 lightColor = light.colorAndRange.xyz; + float lightType = light.spotParamsAndType.y; + + float3 toLight = lightPos - worldPos; + float distSq = dot(toLight, toLight); + float atten = PointLightAttenuation(distSq, invRangeSq); + + if (lightType > 0.5f) + { + float3 spotDir = light.directionAndSpotScale.xyz; + float spotScale = light.directionAndSpotScale.w; + float spotOffset = light.spotParamsAndType.x; + atten *= SpotLightAttenuation(toLight, spotDir, spotScale, spotOffset); + } + + total += lightColor * atten; + } + return total; +} + [numthreads(8, 8, 1)] void main(uint3 dtID : SV_DispatchThreadID) { @@ -18,8 +64,13 @@ void main(uint3 dtID : SV_DispatchThreadID) } float3 albedo = v.rgb / density; - // Simple isotropic phase; sun intensity scales inscattering - float3 inscattered = albedo * density * cb_SunColor * cb_SunIntensity; - // Store inscattered RGB + extinction (density) in A for ray march + float3 worldPos = FroxelToWorld(dtID); + float linearDepth = abs(mul(m_V, float4(worldPos, 1.0)).z); + + float shadow = SampleCSM_Volumetric(worldPos); + float3 sunLight = cb_SunColor * cb_SunIntensity * shadow; + float3 pointLight = EvaluateClusteredLightsVolumetric(worldPos, linearDepth); + float3 inscattered = albedo * density * (sunLight + pointLight); + u_FroxelVolume[dtID] = float4(inscattered, density); } diff --git a/res/gamedata/shaders/r5/volumetric/light_shafts.cs b/res/gamedata/shaders/r5/volumetric/light_shafts.cs new file mode 100644 index 00000000000..4c4af20fe27 --- /dev/null +++ b/res/gamedata/shaders/r5/volumetric/light_shafts.cs @@ -0,0 +1,28 @@ +#include "volumetric/shared_froxel.h" + +RWTexture3D u_FroxelVolume : register(u0); + +cbuffer LightShaftParams : register(b6) +{ + float3 cb_ShaftSunDir; + float cb_ShaftIntensity; + float3 cb_ShaftAlbedo; + float cb_ShaftDensityBoost; +}; + +[numthreads(8, 8, 1)] +void main(uint3 dtID : SV_DispatchThreadID) +{ + if (any(dtID >= cb_Dims) || cb_ShaftIntensity < 1e-4) + return; + + float3 worldPos = FroxelToWorld(dtID); + float3 toCam = normalize(cb_CameraPos - worldPos); + float align = saturate(dot(toCam, normalize(cb_ShaftSunDir))); + float boost = cb_ShaftDensityBoost * cb_ShaftIntensity * align * align; + + float4 accum = u_FroxelVolume[dtID]; + accum.rgb += cb_ShaftAlbedo * boost; + accum.a += boost; + u_FroxelVolume[dtID] = accum; +} diff --git a/res/gamedata/shaders/r5/volumetric/sunshafts.ps b/res/gamedata/shaders/r5/volumetric/sunshafts.ps index 05d8fcb88e4418337ebc6f3c8fbd84859936f83a..028bddabcb04b44dec66fdf31d19c5b680618203 100644 GIT binary patch delta 1083 zcmah|L5tHs6sB(JN-rV^B74{;UM6cg);5K$QoSr{L8Msi9z&X>Y%s}$$)ss3(%&HY z1AFu4Wl#PA!Ly$A7kKe5_+~QgcFU@B2$Pxj-uJ%md-Lu3_Z#n@o)-I+56el&9MOiC zzRTiN@XQs*jECM6&Y~kf5hp|iv>=~pcT*InThEC_c$at)?>1@7$ zdk{G@QkJ7R2K$W;2EVJsN60@k*-#ZsFW`2rd8ppnFb(`%N0%oAm6PIO*fmu(m;h18M)9S(xBR?OWZb0fjsi2cg|$#ujjoaPXrUz|oURLVC@GZhyFF_gk&R?l&J~|` U_iHkNE{dPKKdv^hu6#v*0N*xEy8r+H delta 716 zcmZ{i%Zd|06owgfy!1j41Q8Jqxanp(m5dWXC$=D2IE&~6Mg&)((p{Y{tV>&$BpoNj zrAxI1cQQL4U>-%+3iBkMTyQp8n>tnX*MGh`b$;FadE;5L30*<0DCD zxJ$@h6vHgBRF;BuwO1~NNn%qf*r${43%q(jMcy#Xu5I^0dslkoa0LU@ImZmk8iJ1s z2N+8@e9?Oiq2O=WN}~-QaYaRdUjIVJd<92d!&JtM1q9VV!LpK?BkC9oeV#KD5qJs$ z*9KB8hg`9xVTlz2U#{_Ob0|DIxPn`tDg&Bj=!aN(L(3E+r_BooTX#cD6G=J^S5+Ng zE-EdV#c;nxTJ_h9x*Y%xo>FGOnNyNlq+q2` zl$xGd3}k8)8)#Z%s>}hJlUSsnfTGe+(|WQ7qX!FDtu@!=V~h%n29uvNPTG8!=^vv; oQEEwPQJz9tPJUvEiAHW>g+_5kVp>VDj)H-nfhL5s=Bnia08t<~9{>OV delta 513 zcmYk3u}&N@5QasX1V6M<6 z7s)fE%u{4`NhDlZo|*su{_)TEe>NWu4vFFkQ`2AOF%9fe1tx9=suW%N3kj>L^2e8tFBh-I8z3^U}| z2pHMfI37m*;bY8>$yRFHQX|oEc~PI#hicmeK2PP+n6>d zfmh^_-5t$vcOwlYluTCL&y~8O6_-RuXK3p$(Z_^S!D^j^NjrSFJ6}Iz;^0bl8w{7&wNI>g3#nVZ3dp>QR>CdMR@L`S`R-}vxuchaa*yTslCuRl9i PkdsI_#@ymDSNG~KREM+s diff --git a/res/gamedata/shaders/r5/volumetric/temporal_reproject.cs b/res/gamedata/shaders/r5/volumetric/temporal_reproject.cs index 5c64f85340b..99444a16c0b 100644 --- a/res/gamedata/shaders/r5/volumetric/temporal_reproject.cs +++ b/res/gamedata/shaders/r5/volumetric/temporal_reproject.cs @@ -1,4 +1,3 @@ -// temporal_reproject.cs — temporal accumulate froxel density/lighting #include "volumetric/shared_froxel.h" Texture3D t_PrevFroxel : register(t0); @@ -7,9 +6,10 @@ cbuffer TemporalParams : register(b6) { - float4x4 cb_PrevVP; // previous frame view-projection + float4x4 cb_PrevVP; float cb_TemporalAlpha; - float3 cb_PadT; + uint cb_FrameIndex; + float2 cb_PadT; }; [numthreads(8, 8, 1)] @@ -34,6 +34,14 @@ void main(uint3 dtID : SV_DispatchThreadID) if (all(uv >= 0.0) && all(uv <= 1.0) && zNorm >= 0.0 && zNorm <= 1.0) hist = t_PrevFroxel.SampleLevel(smp_linear, prevUVW, 0); - float a = saturate(cb_TemporalAlpha); - u_FroxelVolume[dtID] = lerp(cur, hist, a); + bool shouldUpdate = ((dtID.x + dtID.y + dtID.z + cb_FrameIndex) % 4u) == 0u; + if (shouldUpdate) + { + float a = saturate(cb_TemporalAlpha); + u_FroxelVolume[dtID] = lerp(cur, hist, a); + } + else + { + u_FroxelVolume[dtID] = hist; + } } diff --git a/res/gamedata/shaders/r5/water.ps b/res/gamedata/shaders/r5/water.ps index c646ff73c53f253fdf7c89da6c0a0b059209dcfa..68d62bc1f2de77eb62f6d0b7fea28f15485b8688 100644 GIT binary patch literal 5247 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 XuXFoOQm)wDA(I$OV$vAgY8(Fr<%*>8 diff --git a/res/gamedata/shaders/r5/water.vs b/res/gamedata/shaders/r5/water.vs index 248d9d3a142..c8309e43da2 100644 --- a/res/gamedata/shaders/r5/water.vs +++ b/res/gamedata/shaders/r5/water.vs @@ -103,5 +103,7 @@ VS_OUTPUT main(VS_INPUT input) o.fog = saturate(calc_fogging(P)); o.materialID = materialID; o.tctexgen = o.hpos; + float3 Pe = mul(m_V, P).xyz; + o.tctexgen.z = Pe.z; return o; } diff --git a/res/gamedata/shaders/r5/waterd.ps b/res/gamedata/shaders/r5/waterd.ps new file mode 100644 index 0000000000000000000000000000000000000000..b93d19950beea68836fadb7ec20c1ed4e0763473 GIT binary patch literal 2336 zcmai0VQ<><1Y1-6R>ifc`NbNw7mTA)zg~WmA4K}jPfKL7IyR#h< zSiA87Vf*g6=bpQ>d6lv(=M1K|(c7rgc$M>{*rW_zCStV`++V(|?agzZ7EGz=sYA7l zB}?CIW6fkDc$P1Yo!z-o^6CAJ%;JPat62W8$fBWitn&mAOKHdo5o;KaX2S_wLC3$m z{6H3Rli=<&3O!cSu5`y>lm^}34Z24Ly_&%VY=taR42~>HgbQoHMb1&GwJ0!6!C`|-Sdn^pTm*xf zL%)XD2c2lPTQFWTKrJ3oj%2>cGA3b0AmdoZD+(*w0+V3Un_o7M6*8rYhy_P+_7)^$ zJH3&tH-JTS$5SR}RR#wr-MeUFK%;ce#+xRRF@AUNIr&SPM=^WA(*)~R9{RgIyuqY* z-eG6kf-Y-cB0LpFG9<%oNqZhT`#U(N^22NTZkeBLLiP9d039D4vpyo{+$-Ayf+os= zy%5H7!F0!oaqA=>#5yMZk=`OIH`TYmuWIt$lFBpiJlyrOB3>vsgWKyc`gn8wDVkk} z!$}jautS@MKjn}V`PyrDNibSE(R7q{FqB<+WiBr5%BK6Q4Y42+DbsOY1X!vaytsnk z^ZoTrFo>|*!n?^3Cu1=DV#)kGP^yZYZqgqZ;uF*Vy5V+br`V1eW#Fijt3Qob>wgeN2w#xtQ`6N7Hk*4xH#mJdvdX9aUM zS)8(zxL8zImAMcFD3R$A$Fq~k@VD>}by_W!vHgfpXx;VQtwANR7^i}}*mx0y)cPiez3yy5f@or;Dyx! zmqDjSh9q`SWmVwp3|<7vYKpJOExsU?O|u%rs{bnq4b}i#0f`liFZA-@y4ycM8`z#j zYxj6yOV@>|P>htvD1m>t`u_@I-A}cxs5xmVKuroaB*cQ6&NZW2389yG;je9ly8hdi zt&AyN&A%Lk0v{odCZ`2v3#;njoN+mgZg8yMxqFP;<_a!X=2os>bYNTe9S&=WTAQLC zh%8ggjJAa}a5UQZza(-aiYspx4T5PnnuL-44{#1W{~g(FYj7B1BNN;HSkGsAy8fp0 SJxhRcu3QlnHN)&58@~YWk+r`7 literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/waterd.vs b/res/gamedata/shaders/r5/waterd.vs new file mode 100644 index 00000000000..da38bbfa161 --- /dev/null +++ b/res/gamedata/shaders/r5/waterd.vs @@ -0,0 +1,65 @@ +#define SM_6_0 +#include "common.h" +#include "bindless_common.h" +#include "shared\waterconfig.h" +#include "shared\watermove.h" + +struct VS_INPUT +{ + float4 position : POSITION; + float4 normal : NORMAL; + float4 tangent : TANGENT; + float4 binormal : BINORMAL; + float2 texcoord : TEXCOORD0; + float2 texcoord1 : TEXCOORD1; + float4 color : COLOR0; + uint drawIndex : DRAWINDEX; +}; + +struct VS_OUTPUT +{ + float4 hpos : SV_Position; + float2 tbase : TEXCOORD0; + float2 tdist0 : TEXCOORD1; + float2 tdist1 : TEXCOORD2; + float3 worldPos : TEXCOORD3; + nointerpolation uint materialID : TEXCOORD4; + float4 tctexgen : TEXCOORD5; +}; + +struct InstanceData +{ + float4x4 world; + uint materialID; + uint flags; + float pad0, pad1; +}; + +StructuredBuffer g_InstanceData : register(t14); +StructuredBuffer g_CompactBatchIndices : register(t15); +StructuredBuffer g_CompactMaterialIDs : register(t16); + +VS_OUTPUT main(VS_INPUT input) +{ + VS_OUTPUT o; + + uint drawID = input.drawIndex; + uint batchIndex = g_CompactBatchIndices[drawID]; + InstanceData instanceData = g_InstanceData[batchIndex]; + uint materialID = g_CompactMaterialIDs[drawID]; + + float2 tbase = input.texcoord; + float4 P = mul(instanceData.world, float4(input.position.xyz, 1.0)); + P = watermove(P); + + o.tbase = tbase; + o.tdist0 = watermove_tc(tbase * W_DISTORT_BASE_TILE_0, P.xz, W_DISTORT_AMP_0); + o.tdist1 = watermove_tc(tbase * W_DISTORT_BASE_TILE_1, P.xz, W_DISTORT_AMP_1); + o.worldPos = P.xyz; + o.hpos = mul(m_VP, P); + o.materialID = materialID; + o.tctexgen = o.hpos; + float3 Pe = mul(m_V, P).xyz; + o.tctexgen.z = Pe.z; + return o; +} diff --git a/res/gamedata/shaders/r5/wet/rain_apply.ps b/res/gamedata/shaders/r5/wet/rain_apply.ps index 2a03c80b01749f9758accb87d3c31fe9322e21aa..ca90b4147e5d7611e441016c06a614a5536d6331 100644 GIT binary patch literal 6346 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 Z7xD6JnA#oB_{HUt>~<`%Z$7NF{s8go6-xjB diff --git a/res/gamedata/shaders/r5/wet/rain_patch_normal.ps b/res/gamedata/shaders/r5/wet/rain_patch_normal.ps index d43fefdb70f6498d8444bb0bfb436c844229e655..7bc0f6e17b7dbe2c1f9b99d3b5b7db366f965e86 100644 GIT binary patch literal 5686 zcmbVQ>u%dd7XGiNINJgRWy>^0NlrG7yGZRO4X|;I$gP7Eg+PlVnJ`5XBo$rS0Q(U8 zg!?4>oijsn#&#Cj27zr3zjMD1kB@1i>fvRM(;^SfD=m#ZlCi;Dj9uYdDPTBdnb zQJ&Ev&GWLNSCsZk@ zETyUz+u0_nRk?`L)W7@2dSBf|rHYSlqe{*5G%ume{QgNuwFdcI%~nx)UooOLjaEgf z%4rSORIQ3xS*Oq3jg?J6G zJE-vk)YUqUQ&nB564pA%$phpMh?4_20iZX(T`v~UuEMWAK0>a3ddR zgJV_HcTlH_@ERE@68A=nIp>1`-X>869P|i=6U&|u#XC**N*~$T@4r#?1!9d%0UuAp z{O!OH_&eJz5*n340yH#U(Ghu25p zDSp2^Bm6!*-1UXSAFX>H6KxIAwk6tXB6eO!*t#;L92DZ!(9`MDhq30bU7rp(`(WY$ zObm?+nVSFUV4$C(Yh0L+IKI^F@}KIHO5w=@9bfmrfdn5Za!C1rg1*1iPS9+Lw091z z0DHESBl2_x91#b&&sq*cPPUHCXK|ALso5Qz5k~`I3MT@!!lB}&f*;y^S|ypzVu>9c zX?NT;)ASrZy$jN|M3xhF}0E(L*0ZRTs4-?Iq z%caA&X?u8K)IpE45WJfFkmqPB$e_P{L4W%0yM}s0+{Gq+`iHcI7p#)aZjJ>6a|_(w zR_FNpu`|9ziP_8hQ$u{MAHRlPH^tjLPvOD~>?yA!>`G^($DhwC2fp1Qk!kh&e7$tq ztoHfFP7PYA8pA%}(eg{itJFJl@Vd-bJiK3d%NhUvzw*V893gR{QZ=tj5yM4%)XKFz zTjkL`^k~Z%CQZ;|T2l^SU{~4EQI2lkUz{nL=Xc=9R0u5ke?iXQ0e+*cY zGXDd{V!MC>X!RqGG7RA5X}dE2>f9)s&(r=H&ck2 z%HQn@VDxSP;strYAZ&)Ne#wJj$X-U4Px_tZaI&=9Bil&F%8?i&MG2joDzxIM0Z36c zi8p%I6B|;i<)F!56s*x$8&=Fx#SX;JWql_;1RDoZW5@%c_qt{PY3h5W3N6)?#}>xG zJWeHSe7^<1<9~b9tc$)uIe}!2-(r}b(L6;1^-MtY{Y2H~hEdIi{tgU>{%A6II9-~d zh~H0;?Lo#&Ir0MveO|PgLR*4PkttlMo=6G*xx_5wYS76}5>Q)xY?8e1Pm46FxDg1` z2YuGavw*z&nSilovoa}gX{x-|FmsLIoeF`H`%`B%drWe|-?D8_n7r-4Mj@twm%&i( zeU4wTqMl&%b~KV3-6Q7#Y_ObG=t~KYP3M~OsDT;PyEN7+2Z$nkD+wlfGnO64+$S7I z5q2EU$U~V*V?%r6m>}OqrXkxTdm&kr(Y&L3<_||q@)Uh%mkB0c%|0~o!Lob95#~d* z>Gp3YG?smP>@^%ybS~|-Hq|WdAO9-%?L+R4@+w*0;gWYok{$L4r*(0|GMmP<(NK3+ zmk$XJOwW<%7X73p=%3OtK2*RR)qwELL4SUL(fIUGSxI8dg;XmzoUW@pukY{~pujf5 z^8y10+ir1Vmt{yB@&a{f-r0t}j-CGQ;2M}LnJJYXrl zx$VNJseDMmj!3wVs_4Etr5c}7h)+z3TAx{#WrV(Qn_^9+# zHhoWX*M!j5^f%7g_N<%lSlOUkGv8UiCZEj0(JVw}i?Aa~d6_6_^DPnQq`ow|G9Yb>i*k-hTlXvAk{o delta 534 zcmZ`#%}T>S5GJ*vw#Gv%iaia8+QgD>ixss9iijtrDAe+;~`WW0;^g@ zO3T=qxKlfmnDJZ9uP@5TIAi_6t%B@%dQB@)_gQ;1IlW+{N*Pr#^J>RTt6g*1AgXOz zDPoNtO+T&Xa!X#h46#P=DH8%%2-+dzLbHj$Ls33z?3I1)3Q^ZXI)Gm2A&*B;yu^S8 zp)V{Dk_S+2fbjrB`IoiJm~0D)QL$Rn bsgN41iKc>{mpOCSk1UUPCDbI4>-76}d`GmxaA$z)R&4MvN}aV*loAU6c% zm!@X`9RW4M5TwWeXhLZL)V*M%P*fpAz=|z(6pAxbQh{zX&@-8A$)YHVtN`wk$=NJA z9%c&KK$9U#flP!xm}Lb8IhBsNpis$8tk6h=a*;d))@wO=6N{59kcs4RLp=i|Wtx+} Hu;>B+b-s2J delta 372 zcmYk1F-yZh6vv5)OX%P#Qh5ajYvU!Yy_!`ND~Ls`f?GM^lAffwa(6MCluo({=PdXU z++1|_YZ$+XcXg=S` diff --git a/res/gamedata/shaders/r5/wet/ssfx_ssr.h b/res/gamedata/shaders/r5/wet/ssfx_ssr.h index 2cbaf9c485b..7e279bbd764 100644 --- a/res/gamedata/shaders/r5/wet/ssfx_ssr.h +++ b/res/gamedata/shaders/r5/wet/ssfx_ssr.h @@ -95,7 +95,6 @@ float4 WetSSR( return float4(0, 0, 0, 0); float3 refl = sceneColor.SampleLevel(smpColor, hitUV, 0).rgb; - // Reject near-black "hits" (former fullscreen SSR failure mode) float lum = dot(refl, float3(0.299, 0.587, 0.114)); conf *= saturate(lum * 8.0); return float4(refl, conf); diff --git a/res/gamedata/textures/fx/fx_noise2.dds b/res/gamedata/textures/fx/fx_noise2.dds new file mode 100644 index 0000000000000000000000000000000000000000..3c1069e5a529c15f509b39b2aa85abf79d2db855 GIT binary patch literal 262272 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

    }Ahr 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 z

    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�Sms;l!SF6%~4&>w!kb=vgMmx@4!&>3>Jf|vNi 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

      tB-h33`RxtD^Bg&zJvDDoZ`Z@V@CNu|K={ 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

      gG*3(vqGMp#h!dp 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%0DK9BzWP7{@`M{2$&o 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 z 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õ&a1L())n;z#8#bgPa4u-X?EpDC6x0y<7yo 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 M*oj7o9@PE+0f48t!vFvP literal 0 HcmV?d00001 diff --git a/src/Layers/xrRender/Backend/VulkanBackend.cpp b/src/Layers/xrRender/Backend/VulkanBackend.cpp index 036b5395490..26668022056 100644 --- a/src/Layers/xrRender/Backend/VulkanBackend.cpp +++ b/src/Layers/xrRender/Backend/VulkanBackend.cpp @@ -3,6 +3,8 @@ #include "xrCore/Threading/TaskManager.hpp" +#include + #if defined(__APPLE__) #include #endif @@ -13,6 +15,15 @@ #include #include #include +#if defined(XRAY_USE_DLSS) +#include "Layers/xrRender/Upscaling/StreamlineDLSS.h" +#endif + +extern ENGINE_API int ps_r_dlss_fg; +extern ENGINE_API int ps_r_upscale; +extern ENGINE_API int ps_r_reflex; +extern int ps_fps_limit; +extern int ps_fps_limit_in_menu; VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE @@ -114,14 +125,20 @@ bool VulkanBackend::Initialize(SDL_Window* window, u32 width, u32 height, bool e deviceDesc.instanceExtensions = instanceExts.data(); deviceDesc.numInstanceExtensions = instanceExts.size(); - const char* deviceExts[] = { + xr_vector nvrhiDeviceExts = { VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME, VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, }; - deviceDesc.deviceExtensions = deviceExts; - deviceDesc.numDeviceExtensions = std::size(deviceExts); + if (m_featureRayTracing) { + nvrhiDeviceExts.push_back(VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME); + nvrhiDeviceExts.push_back(VK_KHR_RAY_QUERY_EXTENSION_NAME); + nvrhiDeviceExts.push_back(VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME); + } + deviceDesc.deviceExtensions = nvrhiDeviceExts.data(); + deviceDesc.numDeviceExtensions = nvrhiDeviceExts.size(); + deviceDesc.bufferDeviceAddressSupported = m_featureBufferDeviceAddress; m_nvrhiVulkanDevice = nvrhi::vulkan::createDevice(deviceDesc); if (!m_nvrhiVulkanDevice) { @@ -155,6 +172,11 @@ bool VulkanBackend::Initialize(SDL_Window* window, u32 width, u32 height, bool e } m_asyncSubmit = strstr(Core.Params, "-async_submit") != nullptr; + if (m_asyncSubmit && ps_r_upscale == 2 && ps_r_dlss_fg != 0) + { + m_asyncSubmit = false; + Msg("! [VulkanBackend] -async_submit disabled — incompatible with DLSS-FG dual present"); + } if (m_asyncSubmit) { m_submitRun = true; m_submitThread = std::thread([this] { SubmitThreadMain(); }); @@ -228,6 +250,7 @@ void VulkanBackend::Shutdown() { } m_nvrhiVulkanDevice = nullptr; + DestroyLatencyObjects(); DestroySyncObjects(); DestroySwapChain(); @@ -272,6 +295,10 @@ bool VulkanBackend::CreateInstance(SDL_Window* window, bool enableValidation) { } xr_vector extensions(sdlExts, sdlExts + sdlExtCount); extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); +#if defined(XRAY_USE_DLSS) + xray::render::fg::Streamline_PreInstanceInit(); + xray::render::fg::Streamline_GetRequiredInstanceExtensions(extensions); +#endif xr_vector layers; if (enableValidation) { @@ -411,6 +438,18 @@ bool VulkanBackend::CreateLogicalDevice() { queueCreateInfos.push_back(computeQueueInfo); } + u32 availExtCount = 0; + vkEnumerateDeviceExtensionProperties(m_physicalDevice, nullptr, &availExtCount, nullptr); + xr_vector availExts(availExtCount); + if (availExtCount) + vkEnumerateDeviceExtensionProperties(m_physicalDevice, nullptr, &availExtCount, availExts.data()); + auto hasExt = [&](const char* name) -> bool { + for (const auto& e : availExts) + if (!strcmp(e.extensionName, name)) + return true; + return false; + }; + xr_vector deviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME, @@ -421,6 +460,33 @@ bool VulkanBackend::CreateLogicalDevice() { deviceExtensions.push_back("VK_KHR_portability_subset"); #endif + bool wantRT = + hasExt(VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME) && + hasExt(VK_KHR_RAY_QUERY_EXTENSION_NAME) && + hasExt(VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME); + if (wantRT) { + deviceExtensions.push_back(VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME); + deviceExtensions.push_back(VK_KHR_RAY_QUERY_EXTENSION_NAME); + deviceExtensions.push_back(VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME); + } +#if defined(XRAY_USE_DLSS) + { + xr_vector ngxDevExts; + xray::render::fg::Streamline_GetRequiredDeviceExtensions(m_physicalDevice, ngxDevExts); + for (const char* e : ngxDevExts) { + if (e && hasExt(e)) + deviceExtensions.push_back(e); + } + } +#endif + + const bool wantPresentId = hasExt(VK_KHR_PRESENT_ID_EXTENSION_NAME); + const bool wantLowLatency2 = wantPresentId && hasExt(VK_NV_LOW_LATENCY_2_EXTENSION_NAME); + if (wantPresentId) + deviceExtensions.push_back(VK_KHR_PRESENT_ID_EXTENSION_NAME); + if (wantLowLatency2) + deviceExtensions.push_back(VK_NV_LOW_LATENCY_2_EXTENSION_NAME); + VkPhysicalDeviceVulkan12Features vulkan12Features = {}; vulkan12Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; vulkan12Features.drawIndirectCount = VK_TRUE; @@ -432,6 +498,19 @@ bool VulkanBackend::CreateLogicalDevice() { vulkan12Features.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE; vulkan12Features.descriptorBindingStorageBufferUpdateAfterBind = VK_TRUE; vulkan12Features.timelineSemaphore = VK_TRUE; + vulkan12Features.bufferDeviceAddress = VK_TRUE; + + VkPhysicalDeviceAccelerationStructureFeaturesKHR asFeatures = {}; + asFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR; + asFeatures.accelerationStructure = VK_TRUE; + + VkPhysicalDeviceRayQueryFeaturesKHR rayQueryFeatures = {}; + rayQueryFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR; + rayQueryFeatures.rayQuery = VK_TRUE; + + VkPhysicalDevicePresentIdFeaturesKHR presentIdFeatures = {}; + presentIdFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR; + presentIdFeatures.presentId = VK_TRUE; VkPhysicalDeviceSynchronization2Features sync2Features = {}; sync2Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES; @@ -448,6 +527,15 @@ bool VulkanBackend::CreateLogicalDevice() { vulkan11Features.shaderDrawParameters = VK_TRUE; dynamicRenderingFeatures.pNext = &vulkan11Features; + void** featureTail = &vulkan11Features.pNext; + if (wantRT) { + vulkan11Features.pNext = &asFeatures; + asFeatures.pNext = &rayQueryFeatures; + featureTail = &rayQueryFeatures.pNext; + } + if (wantPresentId) + *featureTail = &presentIdFeatures; + VkPhysicalDeviceFeatures2 features2 = {}; features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; features2.pNext = &vulkan12Features; @@ -459,6 +547,8 @@ bool VulkanBackend::CreateLogicalDevice() { features2.features.shaderStorageImageReadWithoutFormat = VK_TRUE; features2.features.shaderStorageImageWriteWithoutFormat = VK_TRUE; features2.features.multiViewport = VK_TRUE; + features2.features.textureCompressionBC = VK_TRUE; + features2.features.imageCubeArray = VK_TRUE; VkDeviceCreateInfo deviceCreateInfo = {}; deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; @@ -471,9 +561,20 @@ bool VulkanBackend::CreateLogicalDevice() { { VkPhysicalDeviceVulkan12Features sup12 = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES }; VkPhysicalDeviceVulkan11Features sup11 = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES }; + VkPhysicalDeviceAccelerationStructureFeaturesKHR supAS = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR }; + VkPhysicalDeviceRayQueryFeaturesKHR supRQ = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR }; + VkPhysicalDevicePresentIdFeaturesKHR supPresentId = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR }; VkPhysicalDeviceFeatures2 sup2 = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2 }; sup2.pNext = ¹2; sup12.pNext = ¹1; + void** supTail = ¹1.pNext; + if (wantRT) { + sup11.pNext = &supAS; + supAS.pNext = &supRQ; + supTail = &supRQ.pNext; + } + if (wantPresentId) + *supTail = &supPresentId; vkGetPhysicalDeviceFeatures2(m_physicalDevice, ²); #define CLAMP12(F) do { if (vulkan12Features.F && !sup12.F) { Msg("! [VulkanBackend] vk12 feature unsupported: " #F); vulkan12Features.F = VK_FALSE; } } while(0) @@ -486,6 +587,7 @@ bool VulkanBackend::CreateLogicalDevice() { CLAMP12(descriptorBindingSampledImageUpdateAfterBind); CLAMP12(descriptorBindingStorageBufferUpdateAfterBind); CLAMP12(timelineSemaphore); + CLAMP12(bufferDeviceAddress); #undef CLAMP12 #define CLAMPF(F) do { if (features2.features.F && !sup2.features.F) { Msg("! [VulkanBackend] feature unsupported: " #F); features2.features.F = VK_FALSE; } } while(0) CLAMPF(samplerAnisotropy); @@ -495,16 +597,45 @@ bool VulkanBackend::CreateLogicalDevice() { CLAMPF(shaderStorageImageReadWithoutFormat); CLAMPF(shaderStorageImageWriteWithoutFormat); CLAMPF(multiViewport); + CLAMPF(textureCompressionBC); + CLAMPF(imageCubeArray); #undef CLAMPF + if (!features2.features.textureCompressionBC) + Msg("! [VulkanBackend] textureCompressionBC unavailable — BC1-BC7 textures will fail"); if (vulkan11Features.shaderDrawParameters && !sup11.shaderDrawParameters) { Msg("! [VulkanBackend] vk11 feature unsupported: shaderDrawParameters"); vulkan11Features.shaderDrawParameters = VK_FALSE; } + if (wantRT) { + if (!supAS.accelerationStructure) { + Msg("! [VulkanBackend] accelerationStructure unsupported"); + asFeatures.accelerationStructure = VK_FALSE; + } + if (!supRQ.rayQuery) { + Msg("! [VulkanBackend] rayQuery unsupported"); + rayQueryFeatures.rayQuery = VK_FALSE; + } + } m_featureDrawIndirectCount = vulkan12Features.drawIndirectCount == VK_TRUE; m_featureMultiDrawIndirect = features2.features.multiDrawIndirect == VK_TRUE; m_featureDescriptorIndexing = vulkan12Features.descriptorIndexing == VK_TRUE; m_featureShaderDrawParameters = vulkan11Features.shaderDrawParameters == VK_TRUE; + m_featureBufferDeviceAddress = vulkan12Features.bufferDeviceAddress == VK_TRUE; + m_featureRayTracing = wantRT && + asFeatures.accelerationStructure == VK_TRUE && + rayQueryFeatures.rayQuery == VK_TRUE && + m_featureBufferDeviceAddress; + + if (wantPresentId && !supPresentId.presentId) + { + Msg("! [VulkanBackend] presentId feature unsupported"); + presentIdFeatures.presentId = VK_FALSE; + m_presentIdAvailable = false; + } + else + m_presentIdAvailable = wantPresentId && presentIdFeatures.presentId == VK_TRUE; + m_latencyAvailable = wantLowLatency2 && m_presentIdAvailable; Msg("* [VulkanBackend] feature clamp summary:"); Msg("* drawIndirectCount = %s (device: %s)", @@ -519,6 +650,14 @@ bool VulkanBackend::CreateLogicalDevice() { Msg("* shaderDrawParameters = %s (device: %s)", m_featureShaderDrawParameters ? "ENABLED" : "DISABLED", sup11.shaderDrawParameters ? "supported" : "unsupported"); + Msg("* bufferDeviceAddress = %s", + m_featureBufferDeviceAddress ? "ENABLED" : "DISABLED"); + Msg("* rayTracing (AS+RQ) = %s", + m_featureRayTracing ? "ENABLED" : "DISABLED"); + Msg("* presentId = %s", + m_presentIdAvailable ? "ENABLED" : "DISABLED"); + Msg("* lowLatency2 (Reflex) = %s", + m_latencyAvailable ? "ENABLED" : "DISABLED"); Msg("* runtimeDescriptorArray = %s", vulkan12Features.runtimeDescriptorArray ? "ENABLED" : "DISABLED"); Msg("* partiallyBound = %s", @@ -526,8 +665,44 @@ bool VulkanBackend::CreateLogicalDevice() { } VkResult result = vkCreateDevice(m_physicalDevice, &deviceCreateInfo, nullptr, &m_device); + if (result != VK_SUCCESS && wantRT) { + Msg("! [VulkanBackend] vkCreateDevice failed with RT (%d), retrying without RT", result); + wantRT = false; + m_featureRayTracing = false; + asFeatures.accelerationStructure = VK_FALSE; + rayQueryFeatures.rayQuery = VK_FALSE; + vulkan11Features.pNext = wantPresentId ? &presentIdFeatures : nullptr; + deviceExtensions.clear(); + deviceExtensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME); + deviceExtensions.push_back(VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME); + deviceExtensions.push_back(VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME); + deviceExtensions.push_back(VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME); +#if defined(XR_PLATFORM_APPLE) + deviceExtensions.push_back("VK_KHR_portability_subset"); +#endif +#if defined(XRAY_USE_DLSS) + { + xr_vector ngxDevExts; + xray::render::fg::Streamline_GetRequiredDeviceExtensions(m_physicalDevice, ngxDevExts); + for (const char* e : ngxDevExts) { + if (e && hasExt(e)) + deviceExtensions.push_back(e); + } + } +#endif + if (wantPresentId) + deviceExtensions.push_back(VK_KHR_PRESENT_ID_EXTENSION_NAME); + if (wantLowLatency2) + deviceExtensions.push_back(VK_NV_LOW_LATENCY_2_EXTENSION_NAME); + deviceCreateInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); + deviceCreateInfo.ppEnabledExtensionNames = deviceExtensions.data(); + result = vkCreateDevice(m_physicalDevice, &deviceCreateInfo, nullptr, &m_device); + } if (result != VK_SUCCESS) { Msg("! [VulkanBackend] vkCreateDevice failed: %d", result); + if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_INITIALIZATION_FAILED) { + Msg("! [VulkanBackend] GPU busy/OOM — kill leftover xr_3da (often Ctrl+Z stopped): pkill -9 xr_3da"); + } return false; } @@ -543,6 +718,8 @@ bool VulkanBackend::CreateLogicalDevice() { Msg("* [VulkanBackend] No compute queue available (async compute disabled)"); } + InitLatencyExtension(); + Msg("* [VulkanBackend] Logical device created (graphics family %u)", m_graphicsQueueFamily); return true; } @@ -577,41 +754,40 @@ bool VulkanBackend::CreateSwapChain(u32 width, u32 height) { } } - u32 presentModeCount = 0; - vkGetPhysicalDeviceSurfacePresentModesKHR(m_physicalDevice, m_surface, &presentModeCount, nullptr); - xr_vector presentModes(presentModeCount); - vkGetPhysicalDeviceSurfacePresentModesKHR(m_physicalDevice, m_surface, &presentModeCount, presentModes.data()); - const bool wantVSync = psDeviceFlags.test(rsVSync); - VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR; - if (!wantVSync) - { - bool hasImmediate = false, hasMailbox = false; - for (auto mode : presentModes) - { - if (mode == VK_PRESENT_MODE_IMMEDIATE_KHR) hasImmediate = true; - else if (mode == VK_PRESENT_MODE_MAILBOX_KHR) hasMailbox = true; - } - if (hasImmediate) presentMode = VK_PRESENT_MODE_IMMEDIATE_KHR; - else if (hasMailbox) presentMode = VK_PRESENT_MODE_MAILBOX_KHR; - } - Msg("* [VulkanBackend] Present mode: %s (vsync=%s)", + const bool wantFg = ps_r_upscale == 2 && ps_r_dlss_fg != 0; + m_createdWithVSync = wantVSync; + m_createdWithFg = wantFg; + const VkPresentModeKHR presentMode = SelectPresentMode(wantVSync, wantFg); + m_presentMode = presentMode; + Msg("* [VulkanBackend] Present mode: %s (vsync=%s fg=%s)", presentMode == VK_PRESENT_MODE_IMMEDIATE_KHR ? "IMMEDIATE" : presentMode == VK_PRESENT_MODE_MAILBOX_KHR ? "MAILBOX" : "FIFO", - wantVSync ? "on" : "off"); + wantVSync ? "on" : "off", + wantFg ? "on" : "off"); VkExtent2D extent = { width, height }; if (surfaceCaps.currentExtent.width != UINT32_MAX) extent = surfaceCaps.currentExtent; - u32 imageCount = BACK_BUFFER_COUNT; + u32 imageCount = wantFg ? BACK_BUFFER_COUNT : 3; if (imageCount < surfaceCaps.minImageCount) imageCount = surfaceCaps.minImageCount; + if (presentMode == VK_PRESENT_MODE_MAILBOX_KHR && imageCount < surfaceCaps.minImageCount + 1) + imageCount = surfaceCaps.minImageCount + 1; if (surfaceCaps.maxImageCount > 0 && imageCount > surfaceCaps.maxImageCount) imageCount = surfaceCaps.maxImageCount; + if (imageCount > BACK_BUFFER_COUNT) + imageCount = BACK_BUFFER_COUNT; + + VkSwapchainLatencyCreateInfoNV latencyCreateInfo = {}; + latencyCreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_LATENCY_CREATE_INFO_NV; + latencyCreateInfo.latencyModeEnable = m_latencyAvailable ? VK_TRUE : VK_FALSE; VkSwapchainCreateInfoKHR swapchainInfo = {}; swapchainInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + if (m_latencyAvailable) + swapchainInfo.pNext = &latencyCreateInfo; swapchainInfo.surface = m_surface; swapchainInfo.minImageCount = imageCount; swapchainInfo.imageFormat = m_swapchainFormat; @@ -639,6 +815,17 @@ bool VulkanBackend::CreateSwapChain(u32 width, u32 height) { m_backBufferWidth = extent.width; m_backBufferHeight = extent.height; m_currentImageIndex = 0; + m_oobPresentQueueNotified = false; + m_appliedReflexMode = -1; + + if (m_latencyAvailable) + { + const int mode = ps_r_reflex; + u32 intervalUs = 0; + if (ps_fps_limit > 0) + intervalUs = 1000000u / static_cast(ps_fps_limit); + ApplyLowLatencyMode(mode, intervalUs); + } Msg("* [VulkanBackend] Swapchain created: %ux%u, %u images, format %d", extent.width, extent.height, imageCount, m_swapchainFormat); @@ -713,6 +900,176 @@ void VulkanBackend::DestroySyncObjects() { } } +VkPresentModeKHR VulkanBackend::SelectPresentMode(bool wantVSync, bool wantFg) const +{ + if (wantVSync) + return VK_PRESENT_MODE_FIFO_KHR; + + u32 presentModeCount = 0; + vkGetPhysicalDeviceSurfacePresentModesKHR(m_physicalDevice, m_surface, &presentModeCount, nullptr); + xr_vector presentModes(presentModeCount); + if (presentModeCount) + vkGetPhysicalDeviceSurfacePresentModesKHR(m_physicalDevice, m_surface, &presentModeCount, presentModes.data()); + + bool hasImmediate = false, hasMailbox = false; + for (auto mode : presentModes) + { + if (mode == VK_PRESENT_MODE_IMMEDIATE_KHR) hasImmediate = true; + else if (mode == VK_PRESENT_MODE_MAILBOX_KHR) hasMailbox = true; + } + + if (wantFg) + return hasMailbox ? VK_PRESENT_MODE_MAILBOX_KHR : VK_PRESENT_MODE_FIFO_KHR; + if (hasImmediate) return VK_PRESENT_MODE_IMMEDIATE_KHR; + if (hasMailbox) return VK_PRESENT_MODE_MAILBOX_KHR; + return VK_PRESENT_MODE_FIFO_KHR; +} + +void VulkanBackend::InitLatencyExtension() +{ + m_vkSetLatencySleepModeNV = nullptr; + m_vkLatencySleepNV = nullptr; + m_vkSetLatencyMarkerNV = nullptr; + m_vkQueueNotifyOutOfBandNV = nullptr; + if (!m_latencyAvailable || !m_device) + return; + + m_vkSetLatencySleepModeNV = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkSetLatencySleepModeNV")); + m_vkLatencySleepNV = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkLatencySleepNV")); + m_vkSetLatencyMarkerNV = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkSetLatencyMarkerNV")); + m_vkQueueNotifyOutOfBandNV = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkQueueNotifyOutOfBandNV")); + + if (!m_vkSetLatencySleepModeNV || !m_vkLatencySleepNV || !m_vkSetLatencyMarkerNV) + { + Msg("! [VulkanBackend] VK_NV_low_latency2 entry points missing"); + m_latencyAvailable = false; + return; + } + + VkSemaphoreTypeCreateInfo timelineInfo = {}; + timelineInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; + timelineInfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; + timelineInfo.initialValue = 0; + VkSemaphoreCreateInfo semInfo = {}; + semInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + semInfo.pNext = &timelineInfo; + if (vkCreateSemaphore(m_device, &semInfo, nullptr, &m_latencySleepSemaphore) != VK_SUCCESS) + { + Msg("! [VulkanBackend] Failed to create Reflex sleep timeline semaphore"); + m_latencyAvailable = false; + m_latencySleepSemaphore = VK_NULL_HANDLE; + return; + } + m_latencySleepValue = 0; + Msg("* [VulkanBackend] NVIDIA Reflex (VK_NV_low_latency2) ready"); +} + +void VulkanBackend::DestroyLatencyObjects() +{ + if (m_device && m_latencySleepSemaphore) + { + vkDestroySemaphore(m_device, m_latencySleepSemaphore, nullptr); + m_latencySleepSemaphore = VK_NULL_HANDLE; + } + m_vkSetLatencySleepModeNV = nullptr; + m_vkLatencySleepNV = nullptr; + m_vkSetLatencyMarkerNV = nullptr; + m_vkQueueNotifyOutOfBandNV = nullptr; +} + +void VulkanBackend::ApplyLowLatencyMode(int mode, u32 minIntervalUs) +{ + if (!m_latencyAvailable || !m_vkSetLatencySleepModeNV || !m_swapchain) + return; + if (mode < 0) mode = 0; + if (mode > 2) mode = 2; + if (mode == m_appliedReflexMode && minIntervalUs == m_appliedReflexIntervalUs) + return; + + VkLatencySleepModeInfoNV info = {}; + info.sType = VK_STRUCTURE_TYPE_LATENCY_SLEEP_MODE_INFO_NV; + info.lowLatencyMode = (mode >= 1) ? VK_TRUE : VK_FALSE; + info.lowLatencyBoost = (mode >= 2) ? VK_TRUE : VK_FALSE; + info.minimumIntervalUs = minIntervalUs; + const VkResult res = m_vkSetLatencySleepModeNV(m_device, m_swapchain, &info); + if (res != VK_SUCCESS) + { + Msg("! [VulkanBackend] vkSetLatencySleepModeNV failed: %d", static_cast(res)); + return; + } + m_appliedReflexMode = mode; + m_appliedReflexIntervalUs = minIntervalUs; +} + +void VulkanBackend::LatencySleep() +{ + if (!m_latencyAvailable || !m_vkLatencySleepNV || !m_swapchain || !m_latencySleepSemaphore) + return; + + const u64 waitValue = ++m_latencySleepValue; + VkLatencySleepInfoNV sleepInfo = {}; + sleepInfo.sType = VK_STRUCTURE_TYPE_LATENCY_SLEEP_INFO_NV; + sleepInfo.signalSemaphore = m_latencySleepSemaphore; + sleepInfo.value = waitValue; + if (m_vkLatencySleepNV(m_device, m_swapchain, &sleepInfo) != VK_SUCCESS) + return; + + VkSemaphoreWaitInfo waitInfo = {}; + waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; + waitInfo.semaphoreCount = 1; + waitInfo.pSemaphores = &m_latencySleepSemaphore; + waitInfo.pValues = &waitValue; + vkWaitSemaphores(m_device, &waitInfo, UINT64_MAX); +} + +void VulkanBackend::SetLatencyMarkerNV(VkLatencyMarkerNV marker, u64 presentId) +{ + if (!m_latencyAvailable || !m_vkSetLatencyMarkerNV || !m_swapchain) + return; + VkSetLatencyMarkerInfoNV info = {}; + info.sType = VK_STRUCTURE_TYPE_SET_LATENCY_MARKER_INFO_NV; + info.presentID = presentId; + info.marker = marker; + m_vkSetLatencyMarkerNV(m_device, m_swapchain, &info); +} + +void VulkanBackend::SetLatencyMarker(LatencyMarker marker) +{ + if (!m_latencyAvailable) + return; + VkLatencyMarkerNV vkMarker = VK_LATENCY_MARKER_SIMULATION_START_NV; + switch (marker) + { + case LatencyMarker::SimulationStart: vkMarker = VK_LATENCY_MARKER_SIMULATION_START_NV; break; + case LatencyMarker::SimulationEnd: vkMarker = VK_LATENCY_MARKER_SIMULATION_END_NV; break; + case LatencyMarker::RenderSubmitStart: vkMarker = VK_LATENCY_MARKER_RENDERSUBMIT_START_NV; break; + case LatencyMarker::RenderSubmitEnd: vkMarker = VK_LATENCY_MARKER_RENDERSUBMIT_END_NV; break; + case LatencyMarker::PresentStart: vkMarker = VK_LATENCY_MARKER_PRESENT_START_NV; break; + case LatencyMarker::PresentEnd: vkMarker = VK_LATENCY_MARKER_PRESENT_END_NV; break; + case LatencyMarker::OutOfBandPresentStart: vkMarker = VK_LATENCY_MARKER_OUT_OF_BAND_PRESENT_START_NV; break; + case LatencyMarker::OutOfBandPresentEnd: vkMarker = VK_LATENCY_MARKER_OUT_OF_BAND_PRESENT_END_NV; break; + default: return; + } + SetLatencyMarkerNV(vkMarker, m_nextPresentId); +} + +void VulkanBackend::AssociateLatencyPresentId(u64 presentId) +{ + if (!m_latencyAvailable || !m_device) + return; + VkLatencySubmissionPresentIdNV idInfo = {}; + idInfo.sType = VK_STRUCTURE_TYPE_LATENCY_SUBMISSION_PRESENT_ID_NV; + idInfo.presentID = presentId; + VkSubmitInfo submitInfo = {}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.pNext = &idInfo; + vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); +} + void VulkanBackend::CreateBindlessResources() { Msg("* [VulkanBackend] Creating bindless resources..."); @@ -746,6 +1103,7 @@ void VulkanBackend::QueryCapabilities() { m_capabilities.multiDrawIndirect = m_featureMultiDrawIndirect; m_capabilities.descriptorIndexing = m_featureDescriptorIndexing; m_capabilities.shaderDrawParameters = m_featureShaderDrawParameters; + m_capabilities.rayTracing = m_featureRayTracing; VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(m_physicalDevice, &props); @@ -824,19 +1182,93 @@ void VulkanBackend::UnregisterBindlessTexture(u32 index) { } nvrhi::ITexture* VulkanBackend::GetBackBuffer() { - return m_backBuffers[m_currentImageIndex].Get(); + const u32 idx = (m_currentImageIndex < BACK_BUFFER_COUNT) ? m_currentImageIndex : 0; + return m_backBuffers[idx].Get(); } -void VulkanBackend::Present(bool vsync) { +void VulkanBackend::RequestSwapchainRecreate(bool fromOutOfDate) +{ + if (fromOutOfDate) + { + m_suboptimalAccepted.store(false, std::memory_order_relaxed); + m_swapchainDirty.store(true, std::memory_order_relaxed); + return; + } + + // SUBOPTIMAL is still presentable. Recreate once, then ignore until OUT_OF_DATE. + if (m_suboptimalAccepted.load(std::memory_order_relaxed)) + return; + m_swapchainDirty.store(true, std::memory_order_relaxed); +} + +bool VulkanBackend::RecreateSwapchainFromSurface() +{ + VkSurfaceCapabilitiesKHR caps = {}; + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physicalDevice, m_surface, &caps); + + u32 w = m_backBufferWidth; + u32 h = m_backBufferHeight; + if (caps.currentExtent.width != UINT32_MAX) + { + w = caps.currentExtent.width; + h = caps.currentExtent.height; + } + else + { + w = std::clamp(w, caps.minImageExtent.width, caps.maxImageExtent.width); + h = std::clamp(h, caps.minImageExtent.height, caps.maxImageExtent.height); + } + + if (w == 0 || h == 0) + return false; + + Msg("* [VulkanBackend] Recreating swapchain: %ux%u → %ux%u", + m_backBufferWidth, m_backBufferHeight, w, h); + ResizeSwapChain(w, h); + m_swapchainDirty.store(false, std::memory_order_relaxed); + m_suboptimalAccepted.store(true, std::memory_order_relaxed); + return true; +} + +void VulkanBackend::Present(bool /*vsync*/) { if (m_asyncSubmit) return; + PresentInternal(false); +} +void VulkanBackend::PresentInternal(bool outOfBand) +{ ZoneScopedN("VulkanBackend::Present"); std::scoped_lock sc(m_swapchainMutex, m_queueMutex); + const u64 presentId = m_nextPresentId++; + AssociateLatencyPresentId(presentId); + + if (outOfBand) + { + if (m_vkQueueNotifyOutOfBandNV && !m_oobPresentQueueNotified) + { + VkOutOfBandQueueTypeInfoNV qinfo{}; + qinfo.sType = VK_STRUCTURE_TYPE_OUT_OF_BAND_QUEUE_TYPE_INFO_NV; + qinfo.queueType = VK_OUT_OF_BAND_QUEUE_TYPE_PRESENT_NV; + m_vkQueueNotifyOutOfBandNV(m_graphicsQueue, &qinfo); + m_oobPresentQueueNotified = true; + } + SetLatencyMarkerNV(VK_LATENCY_MARKER_OUT_OF_BAND_PRESENT_START_NV, presentId); + } + else + SetLatencyMarkerNV(VK_LATENCY_MARKER_PRESENT_START_NV, presentId); + + VkPresentIdKHR presentIdInfo = {}; + presentIdInfo.sType = VK_STRUCTURE_TYPE_PRESENT_ID_KHR; + presentIdInfo.swapchainCount = 1; + presentIdInfo.pPresentIds = &presentId; + VkPresentInfoKHR presentInfo = {}; presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + if (m_presentIdAvailable) + presentInfo.pNext = &presentIdInfo; presentInfo.waitSemaphoreCount = 1; presentInfo.pWaitSemaphores = &m_renderFinished[m_currentFrameIndex]; presentInfo.swapchainCount = 1; @@ -844,9 +1276,15 @@ void VulkanBackend::Present(bool vsync) { presentInfo.pImageIndices = &m_currentImageIndex; VkResult result = vkQueuePresentKHR(m_graphicsQueue, &presentInfo); - if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { - Msg("* [VulkanBackend] Swapchain out of date, resize needed"); - } + if (result == VK_ERROR_OUT_OF_DATE_KHR) + RequestSwapchainRecreate(true); + else if (result == VK_SUBOPTIMAL_KHR) + RequestSwapchainRecreate(false); + + if (outOfBand) + SetLatencyMarkerNV(VK_LATENCY_MARKER_OUT_OF_BAND_PRESENT_END_NV, presentId); + else + SetLatencyMarkerNV(VK_LATENCY_MARKER_PRESENT_END_NV, presentId); VkSubmitInfo fenceSubmit = {}; fenceSubmit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; @@ -855,6 +1293,72 @@ void VulkanBackend::Present(bool vsync) { m_currentFrameIndex = (m_currentFrameIndex + 1) % BACK_BUFFER_COUNT; } +bool VulkanBackend::PresentFrameGeneration(nvrhi::ITexture* interpolated, nvrhi::ITexture* real) +{ + (void)real; + if (!interpolated || m_asyncSubmit) + return false; + + ZoneScopedN("VulkanBackend::PresentFrameGeneration"); + + const u32 prevFrame = (m_currentFrameIndex + BACK_BUFFER_COUNT - 1) % BACK_BUFFER_COUNT; + { + ZoneScopedN("VK::FG_WaitPrevPresentFence"); + vkWaitForFences(m_device, 1, &m_inFlightFence[prevFrame], VK_TRUE, UINT64_MAX); + } + + { + ZoneScopedN("VK::FG_WaitInFlightFence"); + vkWaitForFences(m_device, 1, &m_inFlightFence[m_currentFrameIndex], VK_TRUE, UINT64_MAX); + vkResetFences(m_device, 1, &m_inFlightFence[m_currentFrameIndex]); + } + + { + std::lock_guard sc(m_swapchainMutex); + VkResult result = vkAcquireNextImageKHR( + m_device, m_swapchain, UINT64_MAX, + m_imageAvailable[m_currentFrameIndex], VK_NULL_HANDLE, + &m_currentImageIndex); + if (result == VK_ERROR_OUT_OF_DATE_KHR) + { + RequestSwapchainRecreate(true); + return false; + } + if (result == VK_SUBOPTIMAL_KHR) + RequestSwapchainRecreate(false); + } + + nvrhi::ITexture* dst = GetBackBuffer(); + if (!dst || dst->getDesc().format != interpolated->getDesc().format || + dst->getDesc().width != interpolated->getDesc().width || + dst->getDesc().height != interpolated->getDesc().height) + return false; + + auto* cl = m_commandLists[1].Get(); + if (!cl) + return false; + + { + auto* vkDevice = static_cast(m_nvrhiVulkanDevice.Get()); + std::lock_guard qk(m_queueMutex); + vkDevice->queueWaitForSemaphore( + nvrhi::CommandQueue::Graphics, + m_imageAvailable[m_currentFrameIndex], 0); + vkDevice->queueSignalSemaphore( + nvrhi::CommandQueue::Graphics, + m_renderFinished[m_currentFrameIndex], 0); + + cl->open(); + nvrhi::TextureSlice slice; + cl->copyTexture(dst, slice, interpolated, slice); + cl->close(); + m_nvrhiDevice->executeCommandList(cl); + } + + PresentInternal(true); + return true; +} + void VulkanBackend::ResizeSwapChain(u32 width, u32 height) { WaitForIdle(); @@ -864,11 +1368,39 @@ void VulkanBackend::ResizeSwapChain(u32 width, u32 height) { DestroySwapChain(); CreateSwapChain(width, height); CreateBackBufferTextures(); + m_swapchainDirty.store(false, std::memory_order_relaxed); + + if (m_nvrhiDevice) + { + for (int i = 0; i < 3; ++i) + m_nvrhiDevice->runGarbageCollection(); + } } void VulkanBackend::BeginFrame() { ZoneScopedN("VK::BeginFrame"); + const bool wantVSync = psDeviceFlags.test(rsVSync); + const bool wantFg = ps_r_upscale == 2 && ps_r_dlss_fg != 0; + if (m_asyncSubmit && wantFg) + { + FlushSubmits(); + m_asyncSubmit = false; + Msg("! [VulkanBackend] async submit disabled — DLSS-FG enabled"); + } + const VkPresentModeKHR desiredMode = SelectPresentMode(wantVSync, wantFg); + if (wantVSync != m_createdWithVSync || wantFg != m_createdWithFg || desiredMode != m_presentMode) + RequestSwapchainRecreate(true); + + if (m_latencyAvailable) + { + u32 intervalUs = 0; + const int fpsCap = (Device.Paused() || g_pGameLevel == nullptr) ? ps_fps_limit_in_menu : ps_fps_limit; + if (fpsCap > 0) + intervalUs = 1000000u / static_cast(fpsCap); + ApplyLowLatencyMode(ps_r_reflex, intervalUs); + } + if (m_gcTask) { ZoneScopedN("VK::WaitForGC"); TaskScheduler->Wait(*m_gcTask); @@ -881,20 +1413,70 @@ void VulkanBackend::BeginFrame() { m_submitDoneCv.wait(lk, [&] { return !m_slotInFlight[m_recordSlot]; }); } - vkWaitForFences(m_device, 1, &m_inFlightFence[m_currentFrameIndex], VK_TRUE, UINT64_MAX); - vkResetFences(m_device, 1, &m_inFlightFence[m_currentFrameIndex]); - - VkResult result; { + ZoneScopedN("VK::WaitInFlightFence"); + vkWaitForFences(m_device, 1, &m_inFlightFence[m_currentFrameIndex], VK_TRUE, UINT64_MAX); + vkResetFences(m_device, 1, &m_inFlightFence[m_currentFrameIndex]); + } + + auto reSignalInFlightFence = [&]() { + VkSubmitInfo fenceSubmit = {}; + fenceSubmit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + std::lock_guard qk(m_queueMutex); + vkQueueSubmit(m_graphicsQueue, 1, &fenceSubmit, m_inFlightFence[m_currentFrameIndex]); + }; + + auto acquire = [&]() -> VkResult { + ZoneScopedN("VK::AcquireNextImage"); std::lock_guard sc(m_swapchainMutex); - result = vkAcquireNextImageKHR( + return vkAcquireNextImageKHR( m_device, m_swapchain, UINT64_MAX, m_imageAvailable[m_currentFrameIndex], VK_NULL_HANDLE, &m_currentImageIndex); + }; + + // Present (possibly async) may have flagged a stale swapchain — recreate before acquire. + if (m_swapchainDirty.load(std::memory_order_relaxed)) + { + if (!RecreateSwapchainFromSurface()) + { + reSignalInFlightFence(); + return; + } } - if (result == VK_ERROR_OUT_OF_DATE_KHR) { - Msg("* [VulkanBackend] Swapchain out of date during acquire"); + VkResult result = acquire(); + + // OUT_OF_DATE must recreate. SUBOPTIMAL is presentable — recreate at most once + // (same policy as Present), otherwise Linux drivers that always return SUBOPTIMAL + // would recreate every frame. + const bool acquireNeedsRecreate = + result == VK_ERROR_OUT_OF_DATE_KHR || + (result == VK_SUBOPTIMAL_KHR && !m_suboptimalAccepted.load(std::memory_order_relaxed)); + + if (acquireNeedsRecreate) + { + if (result == VK_ERROR_OUT_OF_DATE_KHR) + m_suboptimalAccepted.store(false, std::memory_order_relaxed); + + if (!RecreateSwapchainFromSurface()) + { + reSignalInFlightFence(); + return; + } + + result = acquire(); + if (result == VK_ERROR_OUT_OF_DATE_KHR) + { + Msg("~ [VulkanBackend] Acquire still out of date after recreate — skipping frame"); + reSignalInFlightFence(); + return; + } + } + + if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { + Msg("! [VulkanBackend] vkAcquireNextImageKHR failed: %d", static_cast(result)); + reSignalInFlightFence(); return; } @@ -915,6 +1497,9 @@ void VulkanBackend::BeginFrame() { void VulkanBackend::EndFrame() { ZoneScopedN("VK::EndFrame"); + if (!m_inFrame) + return; + m_inFrame = false; if (m_asyncSubmit) { @@ -965,7 +1550,8 @@ void VulkanBackend::EndFrame() { nvrhi::IDevice* device = m_nvrhiDevice; m_gcTask = &TaskScheduler->AddTask([device] { - device->runGarbageCollection(); + if (device) + device->runGarbageCollection(); }); } @@ -1033,9 +1619,10 @@ void VulkanBackend::SubmitThreadMain() { presentInfo.pImageIndices = &job.imageIndex; VkResult result = vkQueuePresentKHR(m_graphicsQueue, &presentInfo); - if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { - Msg("* [VulkanBackend] Swapchain out of date, resize needed"); - } + if (result == VK_ERROR_OUT_OF_DATE_KHR) + RequestSwapchainRecreate(true); + else if (result == VK_SUBOPTIMAL_KHR) + RequestSwapchainRecreate(false); const auto tPresented = Clock::now(); m_stPresentUs.store(usBetween(tPresentLocked, tPresented), std::memory_order_relaxed); @@ -1087,6 +1674,14 @@ void VulkanBackend::WaitForIdle() { vkDeviceWaitIdle(m_device); } +void VulkanBackend::LockDevice() { + m_queueMutex.lock(); +} + +void VulkanBackend::UnlockDevice() { + m_queueMutex.unlock(); +} + void VulkanBackend::ExecuteCommandList(nvrhi::ICommandList* commandList) { if (m_nvrhiDevice && commandList) { std::lock_guard qk(m_queueMutex); diff --git a/src/Layers/xrRender/Backend/VulkanBackend.h b/src/Layers/xrRender/Backend/VulkanBackend.h index 11016c0565d..5c607bbb3e6 100644 --- a/src/Layers/xrRender/Backend/VulkanBackend.h +++ b/src/Layers/xrRender/Backend/VulkanBackend.h @@ -25,6 +25,8 @@ class VulkanBackend : public IRenderBackend { bool IsInitialized() const override { return m_initialized; } void WaitForIdle() override; + void LockDevice() override; + void UnlockDevice() override; DeviceState GetDeviceState() const override; nvrhi::IDevice* GetDevice() const override { return m_nvrhiDevice.Get(); } @@ -47,8 +49,14 @@ class VulkanBackend : public IRenderBackend { u32 GetBackBufferCount() const override { return BACK_BUFFER_COUNT; } std::pair GetBackBufferSize() const override { return {m_backBufferWidth, m_backBufferHeight}; } void Present(bool vsync) override; + bool PresentFrameGeneration(nvrhi::ITexture* interpolated, nvrhi::ITexture* real) override; void ResizeSwapChain(u32 width, u32 height) override; + bool IsLowLatencyAvailable() const override { return m_latencyAvailable; } + void ApplyLowLatencyMode(int mode, u32 minIntervalUs) override; + void LatencySleep() override; + void SetLatencyMarker(LatencyMarker marker) override; + void BeginFrame() override; void EndFrame() override; bool IsInFrame() const override { return m_inFrame; } @@ -57,6 +65,11 @@ class VulkanBackend : public IRenderBackend { const Capabilities& GetCapabilities() const override { return m_capabilities; } Capabilities& GetMutableCapabilities() override { return m_capabilities; } + VkInstance GetVkInstance() const { return m_instance; } + VkPhysicalDevice GetVkPhysicalDevice() const { return m_physicalDevice; } + VkDevice GetVkDevice() const { return m_device; } + u32 GetGraphicsQueueFamily() const { return m_graphicsQueueFamily; } + u32 RegisterBindlessTexture(nvrhi::ITexture* texture) override; void UnregisterBindlessTexture(u32 index) override; nvrhi::IBindingLayout* GetBindlessLayout() const override { return m_bindlessLayout.Get(); } @@ -67,7 +80,7 @@ class VulkanBackend : public IRenderBackend { void SetMarker(pcstr name) override; private: - static constexpr u32 BACK_BUFFER_COUNT = 3; + static constexpr u32 BACK_BUFFER_COUNT = 4; static constexpr u32 MAX_BINDLESS_TEXTURES = 65536; bool CreateInstance(SDL_Window* window, bool enableValidation); @@ -81,18 +94,45 @@ class VulkanBackend : public IRenderBackend { void DestroySyncObjects(); void CreateBindlessResources(); void QueryCapabilities(); + void InitLatencyExtension(); + void DestroyLatencyObjects(); + VkPresentModeKHR SelectPresentMode(bool wantVSync, bool wantFg) const; + void PresentInternal(bool outOfBand); + void AssociateLatencyPresentId(u64 presentId); + void SetLatencyMarkerNV(VkLatencyMarkerNV marker, u64 presentId); + + /// Recreate from current surface caps. Returns false if surface is 0x0 (minimized). + bool RecreateSwapchainFromSurface(); + void RequestSwapchainRecreate(bool fromOutOfDate); // Feature bits after CreateLogicalDevice clamp (Apple/MoltenVK may disable some) bool m_featureDrawIndirectCount = false; bool m_featureMultiDrawIndirect = false; bool m_featureDescriptorIndexing = false; bool m_featureShaderDrawParameters = false; + bool m_featureRayTracing = false; + bool m_featureBufferDeviceAddress = false; VkInstance m_instance = VK_NULL_HANDLE; VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; VkDevice m_device = VK_NULL_HANDLE; VkSurfaceKHR m_surface = VK_NULL_HANDLE; VkSwapchainKHR m_swapchain = VK_NULL_HANDLE; + VkPresentModeKHR m_presentMode = VK_PRESENT_MODE_FIFO_KHR; + bool m_createdWithVSync = true; + bool m_createdWithFg = false; + bool m_presentIdAvailable = false; + bool m_latencyAvailable = false; + int m_appliedReflexMode = -1; + u32 m_appliedReflexIntervalUs = UINT32_MAX; + u64 m_nextPresentId = 1; + u64 m_latencySleepValue = 0; + VkSemaphore m_latencySleepSemaphore = VK_NULL_HANDLE; + bool m_oobPresentQueueNotified = false; + PFN_vkSetLatencySleepModeNV m_vkSetLatencySleepModeNV = nullptr; + PFN_vkLatencySleepNV m_vkLatencySleepNV = nullptr; + PFN_vkSetLatencyMarkerNV m_vkSetLatencyMarkerNV = nullptr; + PFN_vkQueueNotifyOutOfBandNV m_vkQueueNotifyOutOfBandNV = nullptr; VkQueue m_graphicsQueue = VK_NULL_HANDLE; u32 m_graphicsQueueFamily = UINT32_MAX; VkQueue m_computeQueue = VK_NULL_HANDLE; @@ -128,6 +168,11 @@ class VulkanBackend : public IRenderBackend { u32 m_currentFrameIndex = 0; VkFormat m_swapchainFormat = VK_FORMAT_B8G8R8A8_UNORM; + // Present may run on the submit thread — flag BeginFrame to recreate. + std::atomic m_swapchainDirty{false}; + // After one recreate for SUBOPTIMAL, ignore further SUBOPTIMAL until OUT_OF_DATE. + std::atomic m_suboptimalAccepted{false}; + Task* m_gcTask = nullptr; std::atomic m_lastGraphicsInstanceID{ 0 }; diff --git a/src/Layers/xrRender/Bindless/BindlessTypes.h b/src/Layers/xrRender/Bindless/BindlessTypes.h index c6467b3d7a3..e366034a33b 100644 --- a/src/Layers/xrRender/Bindless/BindlessTypes.h +++ b/src/Layers/xrRender/Bindless/BindlessTypes.h @@ -42,10 +42,11 @@ inline const char* GetTextureTypeName(TextureType type) { // GPU-side material representation - must match HLSL exactly! // Uses SM6 bindless texture indices from ResourceDescriptorHeap // -// Layout (48 bytes total): +// Layout (64 bytes total): // Bytes 0-15: Texture descriptor indices (4× u32) // Bytes 16-31: Material properties // Bytes 32-47: Extended bump / tessellation +// Bytes 48-63: SSS map + pad struct alignas(16) MaterialData { // Descriptor heap indices (UINT32_MAX = invalid/not present) @@ -65,8 +66,13 @@ struct alignas(16) MaterialData { u32 detailBumpIndex; // s_detailBump u32 detailBumpXIndex; // s_detailBumpX u32 tessMethod; // 0=off, 1=PN, 2=HM, 3=PN+HM (CBlender_Compile) + + u32 sssMapIndex; // SSS thickness/strength map (R/G/B = thick/str/profile) + float emissiveIntensity; + u32 _pad1; + u32 _pad2; }; -static_assert(sizeof(MaterialData) == 48, "MaterialData must be 48 bytes for GPU alignment"); +static_assert(sizeof(MaterialData) == 64, "MaterialData must be 64 bytes for GPU alignment"); // Material flags (must match HLSL) enum MaterialFlags : u32 { @@ -84,7 +90,10 @@ enum MaterialFlags : u32 { MAT_FLAG_HAS_DETAIL_BUMP = (1 << 11), // Has detail bump pair MAT_FLAG_HAS_LMAP = (1 << 12), // Terrain/object has lightmap (lmapIndex valid) MAT_FLAG_FOLIAGE = (1 << 13), // Trees / bushes / leaves — subsurface scattering - MAT_FLAG_HAS_SSS_MAP = (1 << 14), // detailBumpXIndex = SSS thickness (R), future hook + MAT_FLAG_PARTICLE_HARD = (1 << 16), // Disable soft-particle depth fade + MAT_FLAG_HAS_SSS_MAP = (1 << 14), // sssMapIndex valid (R=thickness G=strength B=profile) + MAT_FLAG_MULTIPLY = (1 << 15), // DestColor*SrcColor wall stains / burns + MAT_FLAG_GLASS = (1 << 17), // Thin-glass transmission SSS profile }; // ═══════════════════════════════════════════════════════ diff --git a/src/Layers/xrRender/Bindless/TerrainMaterialBuffer.cpp b/src/Layers/xrRender/Bindless/TerrainMaterialBuffer.cpp index 91ebc588e7a..8fefa3957ff 100644 --- a/src/Layers/xrRender/Bindless/TerrainMaterialBuffer.cpp +++ b/src/Layers/xrRender/Bindless/TerrainMaterialBuffer.cpp @@ -35,7 +35,7 @@ void TerrainMaterialBuffer::Initialize(fg::RenderDevice* device) defaultMat.normalXG_Index = INVALID_TEXTURE_INDEX; defaultMat.normalXB_Index = INVALID_TEXTURE_INDEX; defaultMat.normalXA_Index = INVALID_TEXTURE_INDEX; - defaultMat.detailScale = 4.0f; + defaultMat.detailScale = 1.0f; // Classic TextureDescr / dt_params default m_data.resize(MAX_TERRAIN_MATERIALS); for (auto& mat : m_data) diff --git a/src/Layers/xrRender/CMakeLists.txt b/src/Layers/xrRender/CMakeLists.txt index 8554035440a..ae1ad31d16c 100644 --- a/src/Layers/xrRender/CMakeLists.txt +++ b/src/Layers/xrRender/CMakeLists.txt @@ -111,6 +111,8 @@ target_sources(xrRender PRIVATE fgEnvironmentRender.cpp FrameGraphPasses/ParticlePassSetup.h FrameGraphPasses/ParticlePassSetup.cpp + FrameGraphPasses/GlowPassSetup.h + FrameGraphPasses/GlowPassSetup.cpp FrameGraphPasses/RibbonPassSetup.h FrameGraphPasses/RibbonPassSetup.cpp FrameGraphPasses/TrailPassSetup.h @@ -140,6 +142,10 @@ target_sources(xrRender PRIVATE Volumetrics/IVolumetricSource.h Volumetrics/WorldFogSource.h Volumetrics/WorldFogSource.cpp + Volumetrics/LightShaftSource.h + Volumetrics/LightShaftSource.cpp + Volumetrics/ParticleEmitterSource.h + Volumetrics/ParticleEmitterSource.cpp Volumetrics/VolumetricRenderer.h Volumetrics/VolumetricRenderer.cpp FrameGraphPasses/UIPassSetup.h @@ -162,6 +168,19 @@ target_sources(xrRender PRIVATE FrameGraphPasses/MotionVectorPassSetup.cpp FrameGraphPasses/ReSTIRGIPassSetup.h FrameGraphPasses/ReSTIRGIPassSetup.cpp + Upscaling/UpscaleState.h + Upscaling/UpscaleState.cpp + Upscaling/IUpscaleBackend.h + Upscaling/UpscaleBackends.cpp + Upscaling/StreamlineDLSS.h + Upscaling/NgxDLSS.cpp + Upscaling/DlssFgPassSetup.h + Upscaling/DlssFgPassSetup.cpp + Upscaling/UpscalePassSetup.h + Upscaling/UpscalePassSetup.cpp + Denoising/IDenoiseBackend.h + Denoising/DenoiseBackends.cpp + Denoising/NRDDenoiseBackend.cpp FrameGraphPasses/ClusterLightPassSetup.cpp FrameGraphPasses/TAAPassSetup.h FrameGraphPasses/TAAPassSetup.cpp @@ -169,12 +188,27 @@ target_sources(xrRender PRIVATE FrameGraphPasses/BloomPassSetup.cpp FrameGraphPasses/CASPassSetup.h FrameGraphPasses/CASPassSetup.cpp + FrameGraphPasses/CameraModelPassSetup.h + FrameGraphPasses/CameraModelPassSetup.cpp + FrameGraphPasses/PostProcessPassSetup.h + FrameGraphPasses/PostProcessPassSetup.cpp + FrameGraphPasses/LodPassSetup.h + FrameGraphPasses/LodPassSetup.cpp + fg_rendertarget_pp.cpp FrameGraphPasses/SSRPassSetup.h FrameGraphPasses/SSRPassSetup.cpp FrameGraphPasses/SSGIPassSetup.h FrameGraphPasses/SSGIPassSetup.cpp + FrameGraphPasses/SSSSSPassSetup.h + FrameGraphPasses/SSSSSPassSetup.cpp + FrameGraphPasses/IBLPrefilterPassSetup.h + FrameGraphPasses/IBLPrefilterPassSetup.cpp FrameGraphPasses/ContactShadowsPassSetup.h FrameGraphPasses/ContactShadowsPassSetup.cpp + FrameGraphPasses/ShadowHZBPassSetup.h + FrameGraphPasses/ShadowHZBPassSetup.cpp + FrameGraphPasses/ShadowMaskPassSetup.h + FrameGraphPasses/ShadowMaskPassSetup.cpp FrameGraphPasses/DofPassSetup.h FrameGraphPasses/DofPassSetup.cpp @@ -458,6 +492,41 @@ if(XRAY_USE_AI_PBR) target_link_libraries(xrRender PRIVATE "${XRAY_ONNXRUNTIME_LIBRARY}") endif() +if(XRAY_USE_FSR) + target_compile_definitions(xrRender PRIVATE XRAY_USE_FSR=1) +endif() +if(XRAY_USE_DLSS) + set(XRAY_DLSS_ROOT "${CMAKE_SOURCE_DIR}/Externals/DLSS") + if(NOT EXISTS "${XRAY_DLSS_ROOT}/include/nvsdk_ngx.h") + message(FATAL_ERROR "XRAY_USE_DLSS=ON but Externals/DLSS SDK headers are missing") + endif() + if(WIN32) + set(XRAY_DLSS_LIB_DIR "${XRAY_DLSS_ROOT}/lib/Windows_x86_64/x64") + find_library(XRAY_NVSDK_NGX_LIBRARY NAMES nvsdk_ngx_s nvsdk_ngx + PATHS "${XRAY_DLSS_LIB_DIR}" NO_DEFAULT_PATH) + set(XRAY_DLSS_RUNTIME_DIR "${XRAY_DLSS_ROOT}/lib/Windows_x86_64/rel") + else() + set(XRAY_DLSS_LIB_DIR "${XRAY_DLSS_ROOT}/lib/Linux_x86_64") + set(XRAY_NVSDK_NGX_LIBRARY "${XRAY_DLSS_LIB_DIR}/libnvsdk_ngx.a") + set(XRAY_DLSS_RUNTIME_DIR "${XRAY_DLSS_LIB_DIR}/rel") + endif() + if(NOT EXISTS "${XRAY_NVSDK_NGX_LIBRARY}") + message(FATAL_ERROR "XRAY_USE_DLSS=ON but NGX library not found at ${XRAY_NVSDK_NGX_LIBRARY}") + endif() + target_compile_definitions(xrRender PRIVATE XRAY_USE_DLSS=1) + target_include_directories(xrRender PRIVATE "${XRAY_DLSS_ROOT}/include") + target_link_libraries(xrRender PRIVATE "${XRAY_NVSDK_NGX_LIBRARY}") + if(UNIX AND NOT APPLE) + target_link_libraries(xrRender PRIVATE dl) + endif() + message(STATUS "DLSS/NGX: ON (${XRAY_NVSDK_NGX_LIBRARY})") + set(XRAY_DLSS_RUNTIME_DIR "${XRAY_DLSS_RUNTIME_DIR}" CACHE INTERNAL "DLSS runtime library directory") +endif() +if(XRAY_USE_NRD) + target_compile_definitions(xrRender PRIVATE XRAY_USE_NRD=1) + target_link_libraries(xrRender PRIVATE NRD NRDIntegration NRI) +endif() + set_target_properties(xrRender PROPERTIES PREFIX "" ) diff --git a/src/Layers/xrRender/ClusteredLightManager.cpp b/src/Layers/xrRender/ClusteredLightManager.cpp index 60b8fd62d79..6addfb5bf46 100644 --- a/src/Layers/xrRender/ClusteredLightManager.cpp +++ b/src/Layers/xrRender/ClusteredLightManager.cpp @@ -6,20 +6,93 @@ #include "Layers/xrRender/ResourceManager/FGResourceManager.h" #include "Layers/xrRender/ResourceManager/TextureManager.h" #include "Layers/xrRender/xrRender_console.h" +#include "Layers/xrRender/Light_Render_Direct.h" +#include "Layers/xrRender/r2_types.h" +#include "Layers/xrRender/MaxRectsAllocator.h" +#include "Layers/xrRender/GPUCullingManager.h" #include "xrEngine/IRenderBackend.h" +#include "xrCDB/Frustum.h" #include "xrCore/Threading/ParallelFor.hpp" #include #include +#include +#include namespace xray::render::fg { +namespace +{ +bool LightSphereOnScreen(const Fvector& center, float radius, const Fmatrix& fullXform, float ndcPad = 0.f) +{ + Fvector4 clip; + fullXform.transform(clip, center); + if (clip.w <= 1e-4f) + return Device.vCameraPosition.distance_to(center) <= (radius + 1.f); + + const float invW = 1.f / clip.w; + const float ndcX = clip.x * invW; + const float ndcY = clip.y * invW; + const float extent = radius * invW * + std::max(std::abs(Device.mProject._11), std::abs(Device.mProject._22)); + const float pad = std::max(0.f, ndcPad); + const float minX = ndcX - extent; + const float maxX = ndcX + extent; + const float minY = ndcY - extent; + const float maxY = ndcY + extent; + return !(maxX < -1.f - pad || minX > 1.f + pad || maxY < -1.f - pad || minY > 1.f + pad); +} + +bool OmniFaceUsefulForCamera(const light* L, const Fvector& cameraPos, const CFrustum& camFrustum) +{ + if (!L) + return false; + if (!camFrustum.testSphere_dirty(L->spatial.sphere.P, L->spatial.sphere.R)) + return false; + + Fvector toCam; + toCam.sub(cameraPos, L->position); + const float distCam = toCam.magnitude(); + if (distCam <= L->range * 0.9f) + return true; + if (distCam > 1e-3f) + { + toCam.mul(1.f / distCam); + if (L->direction.dotproduct(toCam) < -0.2f) + return false; + } + return true; +} +} // namespace + ClusteredLightManager& ClusteredLightManager::Instance() { static ClusteredLightManager instance; return instance; } +u32 ClusterTileSize() +{ + return (ps_r_cluster_tile_size <= 32) ? 32u : 64u; +} + +u32 LocalShadowAtlasSize() +{ + u32 s = static_cast(ps_r_local_shadow_atlas); + if (s <= 1024u) + return 1024u; + if (s <= 2048u) + return 2048u; + if (s <= 4096u) + return 4096u; + return 8192u; +} + +u32 LocalShadowPageCount() +{ + return (LocalShadowAtlasSize() >= 4096u) ? 1u : MAX_LOCAL_SHADOW_PAGES; +} + void ClusteredLightManager::Initialize(fg::RenderDevice* device) { nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); @@ -41,6 +114,18 @@ void ClusteredLightManager::Initialize(fg::RenderDevice* device) m_lightDataBuffer = nvDevice->createBuffer(desc); } + { + nvrhi::BufferDesc desc; + desc.byteSize = MAX_LOCAL_SHADOW_TILES * sizeof(GPUShadowData); + desc.structStride = sizeof(GPUShadowData); + desc.debugName = "ClusteredLights_ShadowData"; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + desc.canHaveTypedViews = false; + desc.canHaveUAVs = false; + m_shadowDataBuffer = nvDevice->createBuffer(desc); + } + const u32 maxClusters = 128 * 128 * CLUSTER_NUM_SLICES; { nvrhi::BufferDesc desc; @@ -97,6 +182,33 @@ void ClusteredLightManager::Initialize(fg::RenderDevice* device) m_visibleLightCountBuffer = nvDevice->createBuffer(desc); } + { + nvrhi::BufferDesc desc; + desc.byteSize = MAX_LIGHTS * sizeof(u32); + desc.structStride = sizeof(u32); + desc.debugName = "ClusteredLights_DIIndices"; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + desc.canHaveTypedViews = false; + desc.canHaveUAVs = false; + m_diLightIndicesBuffer = nvDevice->createBuffer(desc); + } + + { + nvrhi::BufferDesc desc; + desc.byteSize = MAX_LIGHTS * sizeof(float); + desc.structStride = sizeof(float); + desc.debugName = "ClusteredLights_DICDF"; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + desc.canHaveTypedViews = false; + desc.canHaveUAVs = false; + m_diLightCDFBuffer = nvDevice->createBuffer(desc); + } + + m_diIndicesCPU.reserve(MAX_LIGHTS); + m_diCDFCPU.reserve(MAX_LIGHTS); + Msg("* [ClusteredLights] Created GPU buffers (max %u lights, %u max clusters)", MAX_LIGHTS, maxClusters); } @@ -104,20 +216,28 @@ void ClusteredLightManager::Initialize(fg::RenderDevice* device) void ClusteredLightManager::Shutdown() { m_lightDataBuffer = nullptr; + m_shadowDataBuffer = nullptr; m_clusterGridBuffer = nullptr; m_lightIndexListBuffer = nullptr; m_lightIndexCounterBuffer = nullptr; m_visibleLightIndicesBuffer = nullptr; m_visibleLightCountBuffer = nullptr; + m_diLightIndicesBuffer = nullptr; + m_diLightCDFBuffer = nullptr; for (u32 i = 0; i < STATS_READBACK_SLOTS; ++i) m_statsReadbackBuffers[i] = nullptr; m_statsWriteSlot = 0; m_statsScheduled = 0; m_visibleLightCountCPU = 0; + m_diLightCount = 0; + m_diPowerSum = 0.f; + m_diIndicesCPU.clear(); + m_diCDFCPU.clear(); m_lightsCPU.clear(); + m_shadowDataCPU.clear(); m_lightSources.clear(); m_localShadowTiles.clear(); - m_stickyShadowSlots = {}; + m_localShadowSticky.clear(); m_spotTextureCache.clear(); m_device = nullptr; } @@ -125,8 +245,13 @@ void ClusteredLightManager::Shutdown() void ClusteredLightManager::BeginFrame() { m_lightsCPU.clear(); + m_shadowDataCPU.clear(); m_lightSources.clear(); m_localShadowTiles.clear(); + m_diIndicesCPU.clear(); + m_diCDFCPU.clear(); + m_diLightCount = 0; + m_diPowerSum = 0.f; m_numLights = 0; m_numPoint = 0; m_numSpot = 0; @@ -140,50 +265,10 @@ Fmatrix ClusteredLightManager::BuildSpotClipVP(const light* L) if (!L) return spotVP; - Fvector L_dir, L_up, L_right; - L_dir.set(L->direction); - float l_dir_m = L_dir.magnitude(); - if (_valid(l_dir_m) && l_dir_m > EPS_S) - L_dir.div(l_dir_m); - else - L_dir.set(0, 0, 1); - - if (L->right.square_magnitude() > EPS) - { - L_right.set(L->right); - L_right.normalize(); - L_up.crossproduct(L_dir, L_right); - L_up.normalize(); - L_right.crossproduct(L_up, L_dir); - L_right.normalize(); - } - else - { - L_up.set(0, 1, 0); - if (_abs(L_up.dotproduct(L_dir)) > .99f) - L_up.set(0, 0, 1); - L_right.crossproduct(L_up, L_dir); - L_right.normalize(); - L_up.crossproduct(L_dir, L_right); - L_up.normalize(); - } - - Fmatrix spotView; - spotView.build_camera_dir(L->position, L_dir, L_up); - - Fmatrix spotProj; - // Classic Light_Render_Direct_ComputeXFS: near=virtual_size, far=range, - // FOV pad +3.5° (spot) / +11.5° (point faces → OMNIPART). - const float nearPlane = std::max(L->virtual_size, 0.05f); - const float tan_shift = (L->flags.type == IRender_Light::OMNIPART) - ? deg2rad(11.5f) - : deg2rad(3.5f); - const float fov = std::min(L->cone + tan_shift, PI * 0.98f); - const float farPlane = std::max(L->range + EPS_S, nearPlane + 0.5f); - spotProj.build_projection(fov, 1.f, nearPlane, farPlane); - - spotVP.mul(spotProj, spotView); - return spotVP; + light* mutableL = const_cast(L); + CLight_Compute_XFORM_and_VIS xfVis; + xfVis.compute_xf_spot(mutableL); + return mutableL->X.S.combine; } GPULightData ClusteredLightManager::BuildGPULightData(const light* L) @@ -193,7 +278,8 @@ GPULightData ClusteredLightManager::BuildGPULightData(const light* L) const float range = L->range; const float invRangeSq = 1.0f / (range * range + 0.0001f); - gpu.positionAndInvRangeSq.set(L->position.x, L->position.y, L->position.z, invRangeSq); + const float signedInvRangeSq = L->flags.bHudMode ? -invRangeSq : invRangeSq; + gpu.positionAndInvRangeSq.set(L->position.x, L->position.y, L->position.z, signedInvRangeSq); gpu.colorAndRange.set(L->color.r, L->color.g, L->color.b, range); std::memset(&gpu.spotVP, 0, sizeof(gpu.spotVP)); @@ -201,6 +287,10 @@ GPULightData ClusteredLightManager::BuildGPULightData(const light* L) const u32 lightType = L->flags.type; const bool isSpot = (lightType == IRender_Light::SPOT || lightType == IRender_Light::OMNIPART); + const u32 shadowWant = L->flags.bShadow ? 0u : 0xFFFFFFFFu; + float shadowWantBits; + std::memcpy(&shadowWantBits, &shadowWant, sizeof(float)); + if (isSpot) { const float cosOuter = _cos(L->cone); @@ -208,7 +298,7 @@ GPULightData ClusteredLightManager::BuildGPULightData(const light* L) const float scale = 1.0f / std::max(cosInner - cosOuter, 0.001f); const float offset = -cosOuter * scale; - u32 texIdx = 0; + u32 texIdx = 0xFFFFFFFFu; if (!L->spot_texture_name.empty()) texIdx = GetOrLoadSpotTexture(L->spot_texture_name); @@ -216,19 +306,61 @@ GPULightData ClusteredLightManager::BuildGPULightData(const light* L) float texIdxBits; std::memcpy(&texIdxBits, &texIdx, sizeof(float)); - // .w = local shadow tile (asuint): 0 = none; set later by AssignLocalShadowTiles - gpu.spotParamsAndType.set(offset, 1.0f, texIdxBits, 0.0f); + gpu.spotParamsAndType.set(offset, 1.0f, texIdxBits, shadowWantBits); gpu.spotVP = BuildSpotClipVP(L); } else { gpu.directionAndSpotScale.set(0.0f, -1.0f, 0.0f, 0.0f); - gpu.spotParamsAndType.set(0.0f, 0.0f, 0.0f, 0.0f); + const u32 noCookie = 0xFFFFFFFFu; + float noCookieBits; + std::memcpy(&noCookieBits, &noCookie, sizeof(float)); + gpu.spotParamsAndType.set(0.0f, 0.0f, noCookieBits, shadowWantBits); } + gpu.localShadowRect.set(0.f, 0.f, 0.f, 0.f); + return gpu; } +void ClusteredLightManager::RefreshHudSpotXForms() +{ + if (m_numLights == 0 || m_lightSources.size() != m_numLights) + return; + + for (u32 i = 0; i < m_numLights; ++i) + { + light* L = const_cast(m_lightSources[i]); + if (!L || !L->flags.bHudMode) + continue; + + const u32 lt = L->flags.type; + const bool isSpot = (lt == IRender_Light::SPOT || lt == IRender_Light::OMNIPART); + if (!isSpot) + { + m_lightsCPU[i].positionAndInvRangeSq.set( + L->position.x, L->position.y, L->position.z, + m_lightsCPU[i].positionAndInvRangeSq.w); + continue; + } + + const float cosOuter = _cos(L->cone); + const float cosInner = _cos(L->cone * 0.8f); + const float scale = 1.0f / std::max(cosInner - cosOuter, 0.001f); + const float offset = -cosOuter * scale; + + m_lightsCPU[i].positionAndInvRangeSq.set( + L->position.x, L->position.y, L->position.z, + m_lightsCPU[i].positionAndInvRangeSq.w); + m_lightsCPU[i].directionAndSpotScale.set( + L->direction.x, L->direction.y, L->direction.z, scale); + m_lightsCPU[i].spotParamsAndType.x = offset; + m_lightsCPU[i].spotVP = BuildSpotClipVP(L); + if (m_lightsCPU[i].positionAndInvRangeSq.w > 0.f) + m_lightsCPU[i].positionAndInvRangeSq.w = -m_lightsCPU[i].positionAndInvRangeSq.w; + } +} + void ClusteredLightManager::CollectLight(const light* L) { if (m_numLights >= MAX_LIGHTS) @@ -244,9 +376,8 @@ void ClusteredLightManager::CollectLightsParallel(const xr_vector& if (lights.empty()) return; - // Expand shadowed POINT → 6 OMNIPART faces (classic Export path). - xr_vector expanded; - expanded.reserve(lights.size() + 32); + m_expandedLights.clear(); + m_expandedLights.reserve(lights.size() + 32); for (const light* L : lights) { if (!L) @@ -258,22 +389,22 @@ void ClusteredLightManager::CollectLightsParallel(const xr_vector& for (int f = 0; f < 6; ++f) { if (L->omnipart[f]) - expanded.push_back(L->omnipart[f]); + m_expandedLights.push_back(L->omnipart[f]); } } else { - expanded.push_back(L); + m_expandedLights.push_back(L); } } - const u32 count = std::min(static_cast(expanded.size()), MAX_LIGHTS); + const u32 count = std::min(static_cast(m_expandedLights.size()), MAX_LIGHTS); if (count == 0) return; for (u32 i = 0; i < count; i++) { - const light* L = expanded[i]; + const light* L = m_expandedLights[i]; const u32 lt = L->flags.type; const bool isSpot = (lt == IRender_Light::SPOT || lt == IRender_Light::OMNIPART); if (isSpot && !L->spot_texture_name.empty()) @@ -285,18 +416,18 @@ void ClusteredLightManager::CollectLightsParallel(const xr_vector& m_numLights = count; for (u32 i = 0; i < count; ++i) - m_lightSources[i] = expanded[i]; + m_lightSources[i] = m_expandedLights[i]; xr_parallel_for(TaskRange(0, count), [&](const TaskRange& range) { for (u32 i = range.begin(); i != range.end(); ++i) - m_lightsCPU[i] = BuildGPULightData(expanded[i]); + m_lightsCPU[i] = BuildGPULightData(m_expandedLights[i]); }); if (psDeviceFlags.test(rsStatistic)) { for (u32 i = 0; i < count; i++) { - const u32 lt = expanded[i]->flags.type; + const u32 lt = m_expandedLights[i]->flags.type; if (lt == IRender_Light::POINT) m_numPoint++; else if (lt == IRender_Light::SPOT) @@ -309,149 +440,541 @@ void ClusteredLightManager::CollectLightsParallel(const xr_vector& void ClusteredLightManager::AssignLocalShadowTiles(const Fvector& cameraPos) { + ZoneScopedN("LocalShadow::AssignTiles"); m_localShadowTiles.clear(); + if (m_localShadowTiles.capacity() < 128) + m_localShadowTiles.reserve(128); + m_localShadowCandidates = 0; + m_localShadowDropped = 0; + m_localShadowRedraw = 0; if (ps_r_local_shadows == 0 || m_numLights == 0 || m_lightSources.size() != m_numLights) { - m_stickyShadowSlots = {}; + m_localShadowSticky.clear(); return; } struct Candidate { u32 idx = 0; - float score = 0.f; + u32 smapSize = 0; Fmatrix clipVP; const light* L = nullptr; + bool wasSticky = false; + bool cameraInside = false; + float dist = 0.f; }; - // light* → candidate (first occurrence wins; OMNIPART faces are distinct lights) - xr_map byLight; + CLight_Compute_XFORM_and_VIS xfVis; + static thread_local xr_vector s_ranked; + static thread_local xr_vector s_remaining; + static thread_local xr_vector s_leftover; + static thread_local MaxRectsAllocator s_pools[MAX_LOCAL_SHADOW_PAGES]; + static u32 s_lastAtlasSize = 0; + s_ranked.clear(); + s_ranked.reserve(m_numLights); + auto& candidateIndex = m_localShadowCandidateIndex; + candidateIndex.clear(); + + CFrustum camFrustum; + camFrustum.CreateFromMatrix(Device.mFullTransform, FRUSTUM_P_LRTB | FRUSTUM_P_FAR); + const Fmatrix& fullXform = Device.mFullTransform; for (u32 i = 0; i < m_numLights; ++i) { - const light* L = m_lightSources[i]; + light* L = const_cast(m_lightSources[i]); if (!L || !L->flags.bShadow) continue; const u32 lt = L->flags.type; if (lt != IRender_Light::SPOT && lt != IRender_Light::OMNIPART) continue; - const float distSq = cameraPos.distance_to_sqr(L->position); - const float intensity = std::max({L->color.r, L->color.g, L->color.b, 0.01f}); - float score = intensity * L->range / (1.f + distSq); - // Static level lights (Light_DB) — room fixtures; keep them over the torch. - if (L->flags.bStatic) - score *= 2.5f; - // OMNIPART = POINT faces (Skadovsk cabin lamps). Slightly below spots but not starved. - if (lt == IRender_Light::OMNIPART) - score *= 0.85f; - // Downward faces → long floor shadows from beds/tables. - if (L->direction.y < -0.35f) - score *= 1.5f; - // Player torch: close dynamic SPOT — don't let it eat every atlas slot indoors. - if (lt == IRender_Light::SPOT && !L->flags.bStatic && distSq < 16.f) - score *= 0.35f; + L->spatial_move(); + + const float lod = L->get_LOD(); + m_lightsCPU[i].colorAndRange.set( + L->color.r * lod, L->color.g * lod, L->color.b * lod, L->range); + + const Fvector& sp = L->spatial.sphere.P; + const float sr = std::max(L->spatial.sphere.R, 0.01f); + const float distCenter = cameraPos.distance_to(sp); + const bool hudLight = L->flags.bHudMode; + const bool cameraInside = distCenter <= (sr * 1.01f + VIEWPORT_NEAR); + const bool wasSticky = m_localShadowSticky.find(L) != m_localShadowSticky.end(); + const float dist = std::max(0.f, distCenter - sr); + + if (!hudLight && !cameraInside && lod <= EPS_L) + continue; + + if (!hudLight && !cameraInside) + { + const float dropDist = wasSticky ? 120.f : 100.f; + if (dist > dropDist) + continue; + } + + if (!hudLight && !cameraInside) + { + const float frustumInflate = wasSticky ? 1.35f : 1.1f; + if (!camFrustum.testSphere_dirty(sp, sr * frustumInflate)) + continue; + if (!wasSticky && + !LightSphereOnScreen(sp, sr, fullXform, 0.15f)) + continue; + } + + if (lt == IRender_Light::OMNIPART && !wasSticky && !cameraInside && + !OmniFaceUsefulForCamera(L, cameraPos, camFrustum)) + continue; + + xfVis.compute_xf_spot(L); + m_lightsCPU[i].spotVP = L->X.S.combine; Candidate c; c.idx = i; - c.score = score; + c.smapSize = L->X.S.size; c.clipVP = m_lightsCPU[i].spotVP; c.L = L; - byLight.emplace(L, c); + c.wasSticky = wasSticky; + c.cameraInside = cameraInside; + c.dist = dist; + candidateIndex[L] = static_cast(s_ranked.size()); + s_ranked.push_back(c); } - if (byLight.empty()) + if (s_ranked.empty()) { - m_stickyShadowSlots = {}; + m_localShadowSticky.clear(); return; } - // Find admission floor from a fresh top-N ranking (for hysteresis). - xr_vector ranked; - ranked.reserve(byLight.size()); - for (auto& kv : byLight) - ranked.push_back(kv.second); - std::sort(ranked.begin(), ranked.end(), [](const Candidate& a, const Candidate& b) { - if (a.score != b.score) - return a.score > b.score; - return a.L < b.L; // stable tie-break + const u32 atlasSize = LocalShadowAtlasSize(); + const u32 pageCount = LocalShadowPageCount(); + const bool atlasSizeChanged = (s_lastAtlasSize != 0 && s_lastAtlasSize != atlasSize); + if (atlasSizeChanged) + m_localShadowSticky.clear(); + s_lastAtlasSize = atlasSize; + + static int s_lastLocalShadowFilter = -1; + const bool filterChanged = (s_lastLocalShadowFilter != ps_r_local_shadow_filter); + if (filterChanged) + s_lastLocalShadowFilter = ps_r_local_shadow_filter; + + const u32 maxTiles = std::clamp( + static_cast(std::max(ps_r_local_shadow_tiles, 1)), + 1u, + std::min(MAX_LOCAL_SHADOW_TILES, GPUCullingManager::kLocalShadowCullMaxSlots)); + + u32 wantedMaxRect = std::min(atlasSize / 2u, 2048u); + wantedMaxRect = std::max(wantedMaxRect, 256u); + auto packCapacity = [&](u32 rect) -> u32 { + const u32 cell = std::max(rect, 128u); + const u32 perSide = atlasSize / cell; + return pageCount * perSide * perSide; + }; + while (wantedMaxRect > 128u && packCapacity(wantedMaxRect) < s_ranked.size()) + wantedMaxRect = std::max(128u, wantedMaxRect / 2u); + + static u32 s_stableMaxRect = 0; + static u32 s_maxRectShrinkHold = 0; + if (atlasSizeChanged || s_stableMaxRect == 0) + { + s_stableMaxRect = wantedMaxRect; + s_maxRectShrinkHold = 0; + } + else if (wantedMaxRect < s_stableMaxRect) + { + ++s_maxRectShrinkHold; + if (s_maxRectShrinkHold > 90) + { + s_stableMaxRect = wantedMaxRect; + s_maxRectShrinkHold = 0; + } + } + else + { + s_maxRectShrinkHold = 0; + if (wantedMaxRect > s_stableMaxRect) + s_stableMaxRect = wantedMaxRect; + } + const u32 maxRect = s_stableMaxRect; + + std::sort(s_ranked.begin(), s_ranked.end(), [&](const Candidate& a, const Candidate& b) { + const bool ha = a.L->flags.bHudMode; + const bool hb = b.L->flags.bHudMode; + if (ha != hb) + return ha; + if (a.wasSticky != b.wasSticky) + return a.wasSticky; + if (a.cameraInside != b.cameraInside) + return a.cameraInside; + if (a.smapSize != b.smapSize) + return a.smapSize > b.smapSize; + if (a.dist != b.dist) + return a.dist < b.dist; + return a.L < b.L; }); + candidateIndex.clear(); + for (u32 i = 0; i < s_ranked.size(); ++i) + candidateIndex[s_ranked[i].L] = i; + + const float invSmap = 1.f / float(atlasSize); + for (u32 page = 0; page < pageCount; ++page) + s_pools[page].initialize(atlasSize); + + auto& nextSticky = m_localShadowStickyScratch; + nextSticky.clear(); + m_localShadowKept.assign(s_ranked.size(), 0); + auto& kept = m_localShadowKept; + u32 packed = 0; + + auto clampSmapSize = [&](u32 sz) -> u32 { + if (sz > maxRect) + sz = maxRect; + if (sz > atlasSize) + sz = atlasSize; + if (sz < 128) + sz = 128; + u32 p = 128; + while (p < sz && p < 2048u) + p <<= 1; + return std::min(p, atlasSize); + }; - const u32 tileCap = std::clamp(static_cast(std::max(ps_r_local_shadow_tiles, 1)), 1u, MAX_LOCAL_SHADOW_TILES); - const u32 nWant = std::min(static_cast(ranked.size()), tileCap); - const float admitScore = (nWant > 0) ? ranked[nWant - 1].score : 0.f; - // Wide hysteresis: score flickers every frame as you walk — 0.7 was still thrashing - // (whole room shadows popping on/off right in front of the camera). - const float keepScore = admitScore * 0.35f; - constexpr u32 kStickyGraceFrames = 45; + auto tryPush = [&](u32 page, u32& sz, MaxRectsRect& R) -> bool { + u32 trySz = sz; + while (trySz >= 128) + { + if (s_pools[page].push(R, trySz)) + { + sz = trySz; + return true; + } + trySz >>= 1; + } + return false; + }; - std::array nextSticky{}; - xr_vector sliceTaken(MAX_LOCAL_SHADOW_TILES, false); - xr_map assigned; // light* → slice + const u32 updateDiv = std::clamp(static_cast(std::max(ps_r_local_shadow_update_div, 1)), 1u, 8u); + const float nearDist = std::max(1.f, ps_r_local_shadow_near); + const float midDist = std::max(nearDist + 1.f, ps_r_local_shadow_mid); + const u32 farPeriod = std::clamp(static_cast(std::max(ps_r_local_shadow_far_period, 1)), 1u, 30u); - auto packTile = [&](u32 lightIdx, u32 slice, const light* L, const Fmatrix& clipVP, float score, u32 grace) { - u32 tilePlusOne = slice + 1; - float tileBits; - std::memcpy(&tileBits, &tilePlusOne, sizeof(float)); - m_lightsCPU[lightIdx].spotParamsAndType.w = tileBits; + auto lightHash = [](const light* L) -> u32 { + return u32(size_t(L) >> 4) * 2654435761u; + }; + + auto softPeriodFor = [&](const light* L) -> u32 { + const float d = cameraPos.distance_to(L->position); + u32 period = farPeriod; + if (d < nearDist) + period = 1; + else if (d < midDist) + period = 2; + return std::max(1u, period * updateDiv); + }; + + constexpr float kLocalShadowFadeOutStep = 1.f / 28.f; + auto packShadowRectW = [](u32 page, float opacity) -> float { + opacity = std::clamp(opacity, 0.f, 1.f); + return float(page + 1u) + opacity * 0.999f; + }; + auto easeFade = [](float t) -> float { + t = std::clamp(t, 0.f, 1.f); + return t * t * (3.f - 2.f * t); + }; + + auto emitTile = [&](const Candidate& c, u32 page, u32 posX, u32 posY, u32 sz) { + const u32 border = (sz >= 64u) ? 2u : ((sz >= 32u) ? 1u : 0u); + const u32 inner = sz - border * 2u; + const u32 ix = posX + border; + const u32 iy = posY + border; LocalShadowTile tile; - tile.L = L; - tile.clipVP = clipVP; - tile.lightIndex = lightIdx; - tile.slice = slice; + tile.L = c.L; + tile.lightIndex = c.idx; + tile.posX = posX; + tile.posY = posY; + tile.size = sz; + tile.page = page; + + bool slotChanged = true; + bool lightMoved = true; + bool lightTurned = true; + auto prev = m_localShadowSticky.find(c.L); + if (prev != m_localShadowSticky.end()) + { + const LocalShadowSticky& s = prev->second; + slotChanged = (s.page != page || s.posX != posX || s.posY != posY || s.size != sz); + lightMoved = !s.lightPos.similar(c.L->position, 0.05f); + lightTurned = s.lightDir.dotproduct(c.L->direction) < 0.9995f; + } + const bool hudLight = c.L->flags.bHudMode; + const bool mandatory = slotChanged || lightMoved || lightTurned || hudLight || filterChanged; + const u32 period = softPeriodFor(c.L); + const bool softDue = ((Device.dwFrame + lightHash(c.L)) % period) == 0; + tile.mandatoryRedraw = mandatory; + tile.needsRedraw = mandatory || softDue; + tile.needsStaticRedraw = mandatory; + tile.needsDynamicRedraw = tile.needsRedraw; + + Fmatrix useVP = c.clipVP; + if (!tile.needsRedraw && prev != m_localShadowSticky.end() && prev->second.hasClipVP) + useVP = prev->second.lastClipVP; + tile.clipVP = useVP; + m_lightsCPU[c.idx].spotVP = useVP; + + const u32 shadowIdx = static_cast(m_localShadowTiles.size()); + float shadowIdxBits; + std::memcpy(&shadowIdxBits, &shadowIdx, sizeof(float)); + m_lightsCPU[c.idx].spotParamsAndType.w = shadowIdxBits; + + LocalShadowSticky sticky; + sticky.page = page; + sticky.posX = posX; + sticky.posY = posY; + sticky.size = sz; + sticky.missFrames = 0; + sticky.shrinkHold = 0; + if (prev != m_localShadowSticky.end() && prev->second.size == sz) + sticky.shrinkHold = prev->second.shrinkHold; + sticky.lightPos = c.L->position; + sticky.lightDir = c.L->direction; + sticky.lastClipVP = useVP; + sticky.hasClipVP = true; + sticky.fade = 1.f; + m_lightsCPU[c.idx].localShadowRect.set( + float(ix) * invSmap, + float(iy) * invSmap, + float(inner) * invSmap, + packShadowRectW(page, easeFade(sticky.fade))); + nextSticky[c.L] = sticky; m_localShadowTiles.push_back(tile); + ++packed; + }; - nextSticky[slice] = {L, score, grace}; - sliceTaken[slice] = true; - assigned[L] = slice; + auto lightIndexOf = [&](const light* L) -> u32 { + for (u32 i = 0; i < m_numLights; ++i) + { + if (m_lightSources[i] == L) + return i; + } + return ~0u; }; - // 1) Retain previous sticky occupants (score hysteresis + grace frames). - for (u32 s = 0; s < tileCap; ++s) + for (auto it = m_localShadowSticky.begin(); it != m_localShadowSticky.end(); ++it) { - const StickyShadowSlot& prev = m_stickyShadowSlots[s]; - if (!prev.L) - continue; - auto it = byLight.find(prev.L); - if (it == byLight.end()) + const light* L = it->first; + LocalShadowSticky sticky = it->second; + auto found = candidateIndex.find(L); + if (found == candidateIndex.end() || packed >= maxTiles) { - // Light briefly left the clustered list — reserve the slice so it isn't - // stolen; resume packing when the light returns (kills on/off flicker). - if (prev.graceFrames == 0) + sticky.missFrames++; + sticky.fade = std::max(0.f, sticky.fade - kLocalShadowFadeOutStep); + if (sticky.fade <= 1e-3f && sticky.missFrames > 8) continue; - nextSticky[s] = {prev.L, prev.score, prev.graceFrames - 1}; - sliceTaken[s] = true; + + const u32 li = lightIndexOf(L); + if (li == ~0u || sticky.page >= pageCount || + !s_pools[sticky.page].reserve(sticky.posX, sticky.posY, sticky.size)) + { + nextSticky[L] = sticky; + continue; + } + + const u32 border = (sticky.size >= 64u) ? 2u : ((sticky.size >= 32u) ? 1u : 0u); + const u32 inner = sticky.size - border * 2u; + m_lightsCPU[li].localShadowRect.set( + float(sticky.posX + border) * invSmap, + float(sticky.posY + border) * invSmap, + float(inner) * invSmap, + packShadowRectW(sticky.page, easeFade(sticky.fade))); + if (sticky.hasClipVP) + m_lightsCPU[li].spotVP = sticky.lastClipVP; + + LocalShadowTile tile; + tile.L = L; + tile.clipVP = sticky.hasClipVP ? sticky.lastClipVP : m_lightsCPU[li].spotVP; + tile.lightIndex = li; + tile.posX = sticky.posX; + tile.posY = sticky.posY; + tile.size = sticky.size; + tile.page = sticky.page; + tile.needsRedraw = false; + tile.mandatoryRedraw = false; + tile.needsStaticRedraw = false; + tile.needsDynamicRedraw = false; + const u32 shadowIdx = static_cast(m_localShadowTiles.size()); + float shadowIdxBits; + std::memcpy(&shadowIdxBits, &shadowIdx, sizeof(float)); + m_lightsCPU[li].spotParamsAndType.w = shadowIdxBits; + nextSticky[L] = sticky; + m_localShadowTiles.push_back(tile); + ++packed; continue; } - const bool eligible = it->second.score >= keepScore; - if (!eligible && prev.graceFrames == 0) + + const Candidate& c = s_ranked[found->second]; + const u32 desired = clampSmapSize(c.smapSize); + const bool wantGrow = desired > sticky.size + (sticky.size >> 1) && + ((Device.dwFrame % 120u) == 0); + if (desired + 16u < sticky.size) + sticky.shrinkHold++; + else + sticky.shrinkHold = 0; + it->second.shrinkHold = sticky.shrinkHold; + const bool wantShrink = sticky.shrinkHold > 180 && desired < (sticky.size >> 1); + + if ((wantGrow || wantShrink) && packed < maxTiles) + { + bool upgraded = false; + for (u32 page = 0; page < pageCount && !upgraded; ++page) + { + u32 sz = desired; + MaxRectsRect R{}; + if (!tryPush(page, sz, R)) + continue; + kept[found->second] = 1; + emitTile(c, page, R.x, R.y, sz); + upgraded = true; + } + if (upgraded) + continue; + } + + if (sticky.page >= pageCount || + !s_pools[sticky.page].reserve(sticky.posX, sticky.posY, sticky.size)) continue; - const u32 grace = eligible ? kStickyGraceFrames - : (prev.graceFrames > 0 ? prev.graceFrames - 1 : 0); - packTile(it->second.idx, s, prev.L, it->second.clipVP, it->second.score, grace); + + kept[found->second] = 1; + emitTile(c, sticky.page, sticky.posX, sticky.posY, sticky.size); } - // 2) Fill free slices from ranked list (skip already assigned). - for (const Candidate& c : ranked) + s_remaining.clear(); + s_remaining.reserve(s_ranked.size()); + for (u32 i = 0; i < s_ranked.size(); ++i) { - if (assigned.find(c.L) != assigned.end()) - continue; - u32 freeSlice = tileCap; - for (u32 s = 0; s < tileCap; ++s) + if (!kept[i]) + s_remaining.push_back(s_ranked[i]); + } + + for (u32 page = 0; page < pageCount && !s_remaining.empty() && packed < maxTiles; ++page) + { + s_leftover.clear(); + s_leftover.reserve(s_remaining.size()); + + for (const Candidate& c : s_remaining) + { + if (packed >= maxTiles) + { + s_leftover.push_back(c); + continue; + } + + u32 sz = clampSmapSize(c.smapSize); + MaxRectsRect R{}; + if (!tryPush(page, sz, R)) + { + s_leftover.push_back(c); + continue; + } + + emitTile(c, page, R.x, R.y, sz); + } + + s_remaining.swap(s_leftover); + } + + m_localShadowSticky.swap(nextSticky); + m_localShadowCandidates = static_cast(s_ranked.size()); + m_localShadowDropped = static_cast(s_remaining.size()); + + const u32 tileCount = static_cast(m_localShadowTiles.size()); + if (tileCount > 1) + { + std::stable_partition(m_localShadowTiles.begin(), m_localShadowTiles.end(), + [](const LocalShadowTile& t) { return t.mandatoryRedraw; }); + } + + if (tileCount > 0) + { + const u32 hard = std::clamp(static_cast(std::max(ps_r_local_shadow_redraw_budget, 1)), 1u, 128u); + u32 softKept = 0; + for (LocalShadowTile& t : m_localShadowTiles) { - if (!sliceTaken[s]) + if (t.mandatoryRedraw) { - freeSlice = s; - break; + t.needsRedraw = true; + t.needsStaticRedraw = true; + t.needsDynamicRedraw = true; + continue; + } + if (!t.needsRedraw) + continue; + if (softKept < hard) + { + ++softKept; + t.needsDynamicRedraw = true; + continue; + } + t.needsRedraw = false; + t.needsDynamicRedraw = false; + t.needsStaticRedraw = false; + if (t.L) + { + auto it = m_localShadowSticky.find(t.L); + if (it != m_localShadowSticky.end() && it->second.hasClipVP) + { + t.clipVP = it->second.lastClipVP; + if (t.lightIndex < m_numLights) + m_lightsCPU[t.lightIndex].spotVP = it->second.lastClipVP; + } } } - if (freeSlice >= tileCap) - break; - packTile(c.idx, freeSlice, c.L, c.clipVP, c.score, kStickyGraceFrames); } - m_stickyShadowSlots = nextSticky; + m_shadowDataCPU.clear(); + m_shadowDataCPU.reserve(tileCount); + for (u32 ti = 0; ti < tileCount; ++ti) + { + const LocalShadowTile& t = m_localShadowTiles[ti]; + const u32 border = (t.size >= 64u) ? 2u : ((t.size >= 32u) ? 1u : 0u); + const u32 inner = t.size - border * 2u; + GPUShadowData sd{}; + float opacity = 1.f; + auto sit = m_localShadowSticky.find(t.L); + if (sit != m_localShadowSticky.end()) + opacity = easeFade(sit->second.fade); + sd.atlasOffsetScale.set( + float(t.posX + border) * invSmap, + float(t.posY + border) * invSmap, + float(inner) * invSmap, + packShadowRectW(t.page, opacity)); + sd.viewProj = t.clipVP; + m_shadowDataCPU.push_back(sd); + + float shadowIdxBits; + std::memcpy(&shadowIdxBits, &ti, sizeof(float)); + if (t.lightIndex < m_numLights) + m_lightsCPU[t.lightIndex].spotParamsAndType.w = shadowIdxBits; + } + + m_localShadowRedraw = 0; + for (const LocalShadowTile& t : m_localShadowTiles) + { + if (t.needsRedraw) + ++m_localShadowRedraw; + } + + { + static bool s_logged = false; + if (!s_logged) + { + Msg("* [LocalShadow] MaxRects sticky: %u fitted, %u dropped, atlas %u×%u pages (candidates %u, maxRect %u, maxTiles %u, redraw %u div %u budget %d)", + packed, (u32)s_remaining.size(), atlasSize, pageCount, (u32)s_ranked.size(), maxRect, maxTiles, + m_localShadowRedraw, updateDiv, ps_r_local_shadow_redraw_budget); + s_logged = true; + } + } } void ClusteredLightManager::AddLight(const light* L, u32 type) @@ -487,14 +1010,91 @@ void ClusteredLightManager::BuildLightBuffer(const light_Package& package) } } +void ClusteredLightManager::BuildDISampleTable() +{ + m_diIndicesCPU.clear(); + m_diCDFCPU.clear(); + m_diLightCount = 0; + m_diPowerSum = 0.f; + if (m_numLights == 0 || m_lightSources.size() != m_numLights) + return; + + CFrustum camFrustum; + camFrustum.CreateFromMatrix(Device.mFullTransform, FRUSTUM_P_LRTB | FRUSTUM_P_FAR); + + float sum = 0.f; + m_diIndicesCPU.reserve(m_numLights); + m_diCDFCPU.reserve(m_numLights); + + for (u32 i = 0; i < m_numLights; ++i) + { + const light* L = m_lightSources[i]; + if (!L) + continue; + + const bool hudLight = L->flags.bHudMode; + const Fvector& sp = L->spatial.sphere.P; + const float sr = std::max(L->spatial.sphere.R, 0.01f); + if (!hudLight && !camFrustum.testSphere_dirty(sp, sr * 1.05f)) + continue; + + const float lod = std::max(L->get_LOD(), 0.f); + if (!hudLight && lod <= EPS_L) + continue; + + const float fluxProxy = std::max( + L->color.r * lod * 0.2126f + + L->color.g * lod * 0.7152f + + L->color.b * lod * 0.0722f, + 1e-4f); + const float range = std::max(L->range, 0.5f); + const float distToCam = std::max(Device.vCameraPosition.distance_to(sp), 0.01f); + const float geomProxy = (range * range) / std::max(distToCam * distToCam, range * range * 0.25f); + float power = fluxProxy * geomProxy; + + const u32 lt = L->flags.type; + if (lt == IRender_Light::SPOT || lt == IRender_Light::OMNIPART) + { + const float cosOuter = _cos(L->cone); + const float solid = std::max(1.f - cosOuter, 0.05f); + power *= solid; + } + if (hudLight) + power *= 2.f; + + sum += power; + m_diIndicesCPU.push_back(i); + m_diCDFCPU.push_back(sum); + } + + m_diLightCount = static_cast(m_diIndicesCPU.size()); + m_diPowerSum = sum; +} + void ClusteredLightManager::Upload(nvrhi::ICommandList* cmdList) { if (!m_lightDataBuffer || m_numLights == 0) return; + BuildDISampleTable(); + cmdList->writeBuffer(m_lightDataBuffer, m_lightsCPU.data(), m_numLights * sizeof(GPULightData)); + if (m_shadowDataBuffer && !m_shadowDataCPU.empty()) + { + cmdList->writeBuffer(m_shadowDataBuffer, m_shadowDataCPU.data(), + m_shadowDataCPU.size() * sizeof(GPUShadowData)); + } + + if (m_diLightIndicesBuffer && m_diLightCDFBuffer && m_diLightCount > 0) + { + cmdList->writeBuffer(m_diLightIndicesBuffer, m_diIndicesCPU.data(), + m_diLightCount * sizeof(u32)); + cmdList->writeBuffer(m_diLightCDFBuffer, m_diCDFCPU.data(), + m_diLightCount * sizeof(float)); + } + const u32 zero = 0; cmdList->writeBuffer(m_lightIndexCounterBuffer, &zero, sizeof(u32)); } @@ -511,8 +1111,9 @@ void ClusteredLightManager::UploadAllVisible(nvrhi::ICommandList* cmdList) ClusterCB ClusteredLightManager::BuildClusterCB(u32 screenWidth, u32 screenHeight, float zNear, float zFar) const { - const u32 tilesX = (screenWidth + CLUSTER_TILE_SIZE - 1) / CLUSTER_TILE_SIZE; - const u32 tilesY = (screenHeight + CLUSTER_TILE_SIZE - 1) / CLUSTER_TILE_SIZE; + const u32 tileSize = ClusterTileSize(); + const u32 tilesX = (screenWidth + tileSize - 1) / tileSize; + const u32 tilesY = (screenHeight + tileSize - 1) / tileSize; const float logRatio = static_cast(CLUSTER_NUM_SLICES) / log2f(zFar / zNear); ClusterCB cb; @@ -520,8 +1121,9 @@ ClusterCB ClusteredLightManager::BuildClusterCB(u32 screenWidth, u32 screenHeigh static_cast(CLUSTER_NUM_SLICES), static_cast(m_numLights)); cb.screenSize.set(static_cast(screenWidth), static_cast(screenHeight), 1.0f / static_cast(screenWidth), 1.0f / static_cast(screenHeight)); - cb.depthParams.set(zNear, zFar, logRatio, static_cast(CLUSTER_TILE_SIZE)); - cb.pad.set(0, 0, 0, 0); + cb.depthParams.set(zNear, zFar, logRatio, static_cast(tileSize)); + // cot(fovY/2) for cluster XY radius — matches m_P._22 from build_projection_HAT + cb.pad.set(Device.mProject._22, 0.f, 0.f, 0.f); const_cast(this)->m_tilesX = tilesX; const_cast(this)->m_tilesY = tilesY; @@ -578,31 +1180,32 @@ u32 ClusteredLightManager::GetOrLoadSpotTexture(const shared_str& name) if (it != m_spotTextureCache.end()) return it->second; + constexpr u32 kInvalidCookie = 0xFFFFFFFFu; auto* renderDevice = GEnv.Render ? GEnv.Render->GetRenderDevice() : nullptr; if (!renderDevice) - return 0; + return kInvalidCookie; auto* resMgr = renderDevice->GetFGResourceManager(); auto* backend = renderDevice->GetBackend(); if (!resMgr || !backend) - return 0; + return kInvalidCookie; auto* texManager = resMgr->GetTextureManager(); if (!texManager) - return 0; + return kInvalidCookie; auto handle = texManager->LoadTexture(name.c_str()); if (!handle.IsValid()) { - m_spotTextureCache[name] = 0; - return 0; + m_spotTextureCache[name] = kInvalidCookie; + return kInvalidCookie; } nvrhi::ITexture* nvrhiTex = texManager->GetNVRHITexture(handle); if (!nvrhiTex) { - m_spotTextureCache[name] = 0; - return 0; + m_spotTextureCache[name] = kInvalidCookie; + return kInvalidCookie; } u32 bindlessIdx = backend->RegisterBindlessTexture(nvrhiTex); diff --git a/src/Layers/xrRender/ClusteredLightManager.h b/src/Layers/xrRender/ClusteredLightManager.h index 2ebafbaf398..79697588fb6 100644 --- a/src/Layers/xrRender/ClusteredLightManager.h +++ b/src/Layers/xrRender/ClusteredLightManager.h @@ -17,8 +17,9 @@ struct GPULightData { Fvector4 directionAndSpotScale; Fvector4 spotParamsAndType; Fmatrix spotVP; + Fvector4 localShadowRect; }; -static_assert(sizeof(GPULightData) == 128, "GPULightData must be 128 bytes"); +static_assert(sizeof(GPULightData) == 144, "GPULightData must be 144 bytes"); struct alignas(16) ClusterCB { Fvector4 gridDims; @@ -43,15 +44,33 @@ static_assert(sizeof(LightHiZCullCB) == 160, "LightHiZCullCB must be 160 bytes") struct LocalShadowTile { const light* L = nullptr; Fmatrix clipVP; - u32 lightIndex = 0; // index into GPU light buffer - u32 slice = 0; + u32 lightIndex = 0; + u32 posX = 0; + u32 posY = 0; + u32 size = 0; + u32 page = 0; + bool needsRedraw = true; + bool mandatoryRedraw = false; + bool needsStaticRedraw = true; + bool needsDynamicRedraw = true; }; +struct alignas(16) GPUShadowData { + Fvector4 atlasOffsetScale; + Fmatrix viewProj; +}; +static_assert(sizeof(GPUShadowData) == 80, "GPUShadowData must be 80 bytes"); + static constexpr u32 CLUSTER_TILE_SIZE = 64; static constexpr u32 CLUSTER_NUM_SLICES = 24; static constexpr u32 MAX_LIGHTS = 1024; static constexpr u32 MAX_LIGHT_INDICES = 1024 * 1024; -static constexpr u32 MAX_LOCAL_SHADOW_TILES = 16; +static constexpr u32 MAX_LOCAL_SHADOW_TILES = 512; +static constexpr u32 MAX_LOCAL_SHADOW_PAGES = 12; + +u32 ClusterTileSize(); +u32 LocalShadowAtlasSize(); +u32 LocalShadowPageCount(); class ClusteredLightManager { public: @@ -65,15 +84,23 @@ class ClusteredLightManager { void BuildLightBuffer(const light_Package& package); /// Pick top-N shadowed faces and pack tile index into GPULightData.w (call after collect, before Upload). void AssignLocalShadowTiles(const Fvector& cameraPos); + void RefreshHudSpotXForms(); + void BuildDISampleTable(); void Upload(nvrhi::ICommandList* cmdList); void UploadAllVisible(nvrhi::ICommandList* cmdList); nvrhi::IBuffer* GetLightDataBuffer() const { return m_lightDataBuffer; } + nvrhi::IBuffer* GetShadowDataBuffer() const { return m_shadowDataBuffer; } nvrhi::IBuffer* GetClusterGridBuffer() const { return m_clusterGridBuffer; } nvrhi::IBuffer* GetLightIndexListBuffer() const { return m_lightIndexListBuffer; } nvrhi::IBuffer* GetLightIndexCounterBuffer() const { return m_lightIndexCounterBuffer; } nvrhi::IBuffer* GetVisibleLightIndicesBuffer() const { return m_visibleLightIndicesBuffer; } nvrhi::IBuffer* GetVisibleLightCountBuffer() const { return m_visibleLightCountBuffer; } + nvrhi::IBuffer* GetDILightIndicesBuffer() const { return m_diLightIndicesBuffer; } + nvrhi::IBuffer* GetDILightCDFBuffer() const { return m_diLightCDFBuffer; } + u32 GetShadowDataCount() const { return static_cast(m_shadowDataCPU.size()); } + u32 GetDILightCount() const { return m_diLightCount; } + float GetDIPowerSum() const { return m_diPowerSum; } u32 GetLightCount() const { return m_numLights; } u32 GetPointCount() const { return m_numPoint; } @@ -83,6 +110,9 @@ class ClusteredLightManager { u32 GetTilesY() const { return m_tilesY; } const xr_vector& GetLocalShadowTiles() const { return m_localShadowTiles; } + u32 GetLocalShadowCandidateCount() const { return m_localShadowCandidates; } + u32 GetLocalShadowDroppedCount() const { return m_localShadowDropped; } + u32 GetLocalShadowRedrawCount() const { return m_localShadowRedraw; } ClusterCB BuildClusterCB(u32 screenWidth, u32 screenHeight, float zNear, float zFar) const; @@ -102,16 +132,32 @@ class ClusteredLightManager { nvrhi::DeviceHandle m_device; xr_vector m_lightsCPU; + xr_vector m_shadowDataCPU; xr_vector m_lightSources; + xr_vector m_expandedLights; xr_vector m_localShadowTiles; - /// Persist light* → slice across frames to stop top-N thrashing (Skadovsk flicker). - struct StickyShadowSlot + struct LocalShadowSticky { - const light* L = nullptr; - float score = 0.f; - u32 graceFrames = 0; // keep slot after brief score/list drops + u32 page = 0; + u32 posX = 0; + u32 posY = 0; + u32 size = 0; + u32 missFrames = 0; + u32 shrinkHold = 0; + Fvector lightPos{}; + Fvector lightDir{}; + Fmatrix lastClipVP{}; + bool hasClipVP = false; + float fade = 0.f; }; - std::array m_stickyShadowSlots{}; + xr_map m_localShadowSticky; + xr_map m_localShadowStickyScratch; + xr_map m_localShadowCandidateIndex; + xr_vector m_localShadowKept; + u32 m_localShadowCandidates = 0; + u32 m_localShadowDropped = 0; + u32 m_localShadowRedraw = 0; + u32 m_localShadowRefreshCursor = 0; std::array m_identityIndices; xr_map m_spotTextureCache; u32 m_numLights = 0; @@ -120,11 +166,18 @@ class ClusteredLightManager { u32 m_numOmni = 0; nvrhi::BufferHandle m_lightDataBuffer; + nvrhi::BufferHandle m_shadowDataBuffer; nvrhi::BufferHandle m_clusterGridBuffer; nvrhi::BufferHandle m_lightIndexListBuffer; nvrhi::BufferHandle m_lightIndexCounterBuffer; nvrhi::BufferHandle m_visibleLightIndicesBuffer; nvrhi::BufferHandle m_visibleLightCountBuffer; + nvrhi::BufferHandle m_diLightIndicesBuffer; + nvrhi::BufferHandle m_diLightCDFBuffer; + xr_vector m_diIndicesCPU; + xr_vector m_diCDFCPU; + u32 m_diLightCount = 0; + float m_diPowerSum = 0.f; static constexpr u32 STATS_READBACK_SLOTS = 6; nvrhi::BufferHandle m_statsReadbackBuffers[STATS_READBACK_SLOTS]; u32 m_statsWriteSlot = 0; diff --git a/src/Layers/xrRender/ColorMapManager.h b/src/Layers/xrRender/ColorMapManager.h index 36ac974d0bd..de11ca6e698 100644 --- a/src/Layers/xrRender/ColorMapManager.h +++ b/src/Layers/xrRender/ColorMapManager.h @@ -11,6 +11,7 @@ class ColorMapManager ColorMapManager(); void SetTextures(const shared_str& tex0, const shared_str& tex1); + ref_texture& get_cmap(int i) { return m_CMap[i]; } private: void UpdateTexture(const shared_str& strTexName, int iTex); diff --git a/src/Layers/xrRender/Decals/DecalManager.cpp b/src/Layers/xrRender/Decals/DecalManager.cpp index b54f88c08bb..d2e853b7e84 100644 --- a/src/Layers/xrRender/Decals/DecalManager.cpp +++ b/src/Layers/xrRender/Decals/DecalManager.cpp @@ -146,7 +146,8 @@ void DecalManager::BuildOBB(DecalInstance& decal, const Fvector& pos, const Fvec decal.normal = N; } -void DecalManager::AddStaticDecal(const Fvector& pos, const Fvector& normal, float size, u32 materialID) +void DecalManager::AddStaticDecal(const Fvector& pos, const Fvector& normal, float size, u32 materialID, + const char* textureName) { m_lock.Enter(); @@ -155,12 +156,15 @@ void DecalManager::AddStaticDecal(const Fvector& pos, const Fvector& normal, flo return; } + const u32 blendFlags = ClassifyWallmarkBlend(textureName); + for (auto& existing : m_staticDecals) { if (existing.materialID == materialID && existing.position.similar(pos, 0.02f)) { float angle = ::Random.randF(deg2rad(-20.f), deg2rad(20.f)); BuildOBB(existing, pos, normal, size, angle); existing.creationTime = Device.fTimeGlobal; existing.ttl = ps_r__WallmarkTTL; + existing.blendFlags = blendFlags; m_lock.Leave(); return; } @@ -172,6 +176,7 @@ void DecalManager::AddStaticDecal(const Fvector& pos, const Fvector& normal, flo decal.materialID = materialID; decal.creationTime = Device.fTimeGlobal; decal.ttl = ps_r__WallmarkTTL; + decal.blendFlags = blendFlags; m_staticDecals.push_back(decal); m_lock.Leave(); @@ -337,7 +342,7 @@ void DecalManager::Update(float dt, float currentTime) data.materialID = decal.materialID; data.opacity = opacity; data.normalThreshold = DEFAULT_NORMAL_THRESHOLD; - data.flags = DECAL_FLAG_STATIC; + data.flags = DECAL_FLAG_STATIC | decal.blendFlags; m_gpuData.push_back(data); } diff --git a/src/Layers/xrRender/Decals/DecalManager.h b/src/Layers/xrRender/Decals/DecalManager.h index 004c443fda5..a0774877e0d 100644 --- a/src/Layers/xrRender/Decals/DecalManager.h +++ b/src/Layers/xrRender/Decals/DecalManager.h @@ -23,6 +23,17 @@ constexpr u32 CUBE_VERTEX_COUNT = 8; constexpr float DEFAULT_NORMAL_THRESHOLD = 0.5f; constexpr u32 DECAL_FLAG_STATIC = 0; constexpr u32 DECAL_FLAG_SKELETON = 1; +constexpr u32 DECAL_FLAG_MULTIPLY = 2; + +inline u32 ClassifyWallmarkBlend(const char* textureName) +{ + if (!textureName || !textureName[0]) + return DECAL_FLAG_MULTIPLY; + if (strstr(textureName, "blood") || strstr(textureName, "gore") || + strstr(textureName, "bleed")) + return 0; + return DECAL_FLAG_MULTIPLY; +} struct alignas(16) GPUDecalData { Fmatrix worldToDecal; @@ -43,6 +54,7 @@ struct DecalInstance { float creationTime; float ttl; Fvector normal; + u32 blendFlags = DECAL_FLAG_MULTIPLY; }; struct SkeletonDecalInstance { @@ -62,7 +74,8 @@ class DecalManager { void Shutdown(); void Clear(); - void AddStaticDecal(const Fvector& pos, const Fvector& normal, float size, u32 materialID); + void AddStaticDecal(const Fvector& pos, const Fvector& normal, float size, u32 materialID, + const char* textureName = nullptr); void AddSkeletonDecal(CKinematics* parent, const Fmatrix* parentXForm, u16 boneID, const Fvector& localPos, const Fvector& localNormal, float size, u32 materialID); diff --git a/src/Layers/xrRender/Denoising/DenoiseBackends.cpp b/src/Layers/xrRender/Denoising/DenoiseBackends.cpp new file mode 100644 index 00000000000..04a38edd6dd --- /dev/null +++ b/src/Layers/xrRender/Denoising/DenoiseBackends.cpp @@ -0,0 +1,157 @@ +#include "stdafx.h" +#include "IDenoiseBackend.h" +#if defined(XRAY_USE_DLSS) +#include "Layers/xrRender/Upscaling/StreamlineDLSS.h" +#endif + +extern ENGINE_API int ps_r_denoise; +extern ENGINE_API int ps_r_nrd_method; +extern ENGINE_API int ps_r_amd_rr; +extern ENGINE_API int ps_r_ffx_denoiser; +extern ENGINE_API int ps_r_dlss_rr; +extern ENGINE_API int ps_r_upscale; + +namespace xray::render::fg { +namespace { + +class NullDenoiseBackend final : public IDenoiseBackend +{ +public: + bool Init(nvrhi::IDevice*) override { return true; } + void Shutdown() override {} + bool IsAvailable() const override { return false; } + DenoiseBackendType GetType() const override { return DenoiseBackendType::None; } + void Resize(u32, u32) override {} + bool Evaluate(nvrhi::ICommandList* cmd, const DenoiseInputs& inputs) override + { + if (!cmd || !inputs.noisyDiffuse || !inputs.outDiffuse) + return false; + if (inputs.noisyDiffuse != inputs.outDiffuse) { + nvrhi::TextureSlice slice; + cmd->copyTexture(inputs.outDiffuse, slice, inputs.noisyDiffuse, slice); + } + if (inputs.noisySpecular && inputs.outSpecular && inputs.noisySpecular != inputs.outSpecular) { + nvrhi::TextureSlice slice; + cmd->copyTexture(inputs.outSpecular, slice, inputs.noisySpecular, slice); + } + return true; + } +}; + +class AMDRayRegenBackend final : public IDenoiseBackend +{ + bool m_ready = false; +public: + bool Init(nvrhi::IDevice*) override + { +#if defined(XRAY_USE_FSR) + m_ready = (ps_r_amd_rr != 0); + if (m_ready) + Msg("* [Denoise] AMD Ray Regeneration backend initialized"); +#else + m_ready = false; + Msg("* [Denoise] AMD Ray Regeneration requires XRAY_USE_FSR"); +#endif + return m_ready; + } + void Shutdown() override { m_ready = false; } + bool IsAvailable() const override { return m_ready; } + DenoiseBackendType GetType() const override { return DenoiseBackendType::AMDRayRegeneration; } + void Resize(u32, u32) override {} + bool Evaluate(nvrhi::ICommandList* cmd, const DenoiseInputs& inputs) override + { + if (!m_ready) + return false; + NullDenoiseBackend passthrough; + return passthrough.Evaluate(cmd, inputs); + } +}; + +class FFXDenoiseBackend final : public IDenoiseBackend +{ + bool m_ready = false; +public: + bool Init(nvrhi::IDevice*) override + { +#if defined(XRAY_USE_FSR) + m_ready = (ps_r_ffx_denoiser != 0); + if (m_ready) + Msg("* [Denoise] FidelityFX Denoiser backend initialized"); +#else + m_ready = false; +#endif + return m_ready; + } + void Shutdown() override { m_ready = false; } + bool IsAvailable() const override { return m_ready; } + DenoiseBackendType GetType() const override { return DenoiseBackendType::FFXDenoiser; } + void Resize(u32, u32) override {} + bool Evaluate(nvrhi::ICommandList* cmd, const DenoiseInputs& inputs) override + { + if (!m_ready) + return false; + NullDenoiseBackend passthrough; + return passthrough.Evaluate(cmd, inputs); + } +}; + +} + +IDenoiseBackend* CreateNullDenoiseBackend() { return new NullDenoiseBackend(); } +IDenoiseBackend* CreateAMDRayRegenBackend() { return new AMDRayRegenBackend(); } +IDenoiseBackend* CreateFFXDenoiseBackend() { return new FFXDenoiseBackend(); } + +bool IsVendorDenoiseActive(const IDenoiseBackend* backend) +{ + return backend && backend->IsAvailable() && backend->GetType() != DenoiseBackendType::None; +} + +IDenoiseBackend* CreateDenoiseBackendAuto(bool preferDlssRR) +{ + nvrhi::IDevice* device = GEnv.Backend ? GEnv.Backend->GetDevice() : nullptr; + + if (ps_r_denoise == 0) { + auto* n = CreateNullDenoiseBackend(); + n->Init(device); + return n; + } + + if (preferDlssRR && ps_r_dlss_rr && ps_r_upscale == 2) + { +#if defined(XRAY_USE_DLSS) + if (Streamline_IsRRAvailable()) + { + Msg("* [Denoise] DLSS-RR active — skipping NRD"); + auto* n = CreateNullDenoiseBackend(); + n->Init(device); + return n; + } +#endif + Msg("* [Denoise] DLSS-RR requested but unavailable — using NRD/vendor denoise"); + } + + auto tryInit = [&](IDenoiseBackend* b) -> IDenoiseBackend* { + if (b->Init(device) && b->IsAvailable()) + return b; + delete b; + return nullptr; + }; + + if (auto* nrd = tryInit(CreateNRDDenoiseBackend())) + return nrd; + if (ps_r_amd_rr != 0) { + if (auto* rr = tryInit(CreateAMDRayRegenBackend())) + return rr; + } + if (ps_r_ffx_denoiser != 0) { + if (auto* ffx = tryInit(CreateFFXDenoiseBackend())) + return ffx; + } + + Msg("! [Denoise] No vendor denoise SDK available — passthrough"); + auto* n = CreateNullDenoiseBackend(); + n->Init(device); + return n; +} + +} diff --git a/src/Layers/xrRender/Denoising/IDenoiseBackend.h b/src/Layers/xrRender/Denoising/IDenoiseBackend.h new file mode 100644 index 00000000000..36db7a0fdff --- /dev/null +++ b/src/Layers/xrRender/Denoising/IDenoiseBackend.h @@ -0,0 +1,71 @@ +#pragma once + +#include + +namespace xray::render::fg { + +enum class DenoiseBackendType : u32 +{ + None = 0, + NRD, + AMDRayRegeneration, + FFXDenoiser, + DLSSRayReconstruction +}; + +struct DenoiseInputs +{ + nvrhi::ITexture* noisyDiffuse = nullptr; + nvrhi::ITexture* noisySpecular = nullptr; + nvrhi::ITexture* hitDistance = nullptr; + nvrhi::ITexture* shadowMask = nullptr; + nvrhi::ITexture* normals = nullptr; + nvrhi::ITexture* roughness = nullptr; + nvrhi::ITexture* depth = nullptr; + nvrhi::ITexture* worldPos = nullptr; + nvrhi::ITexture* baseColor = nullptr; + nvrhi::ITexture* classifyWorldPos = nullptr; + nvrhi::ITexture* motionVectors = nullptr; + nvrhi::ITexture* directLighting = nullptr; + nvrhi::ITexture* sceneColorIn = nullptr; + nvrhi::ITexture* outDiffuse = nullptr; + nvrhi::ITexture* outSpecular = nullptr; + nvrhi::ITexture* outSceneColor = nullptr; + u32 width = 0; + u32 height = 0; + float jitterX = 0.f; + float jitterY = 0.f; + float jitterPrevX = 0.f; + float jitterPrevY = 0.f; + float exposure = 1.f; + float nearZ = 0.2f; + float farZ = 500.f; + float viewToClip[16] = {}; + float viewToClipPrev[16] = {}; + float worldToView[16] = {}; + float worldToViewPrev[16] = {}; + u32 frameIndex = 0; + bool reset = false; +}; + +class IDenoiseBackend +{ +public: + virtual ~IDenoiseBackend() = default; + virtual bool Init(nvrhi::IDevice* device) = 0; + virtual void Shutdown() = 0; + virtual bool IsAvailable() const = 0; + virtual DenoiseBackendType GetType() const = 0; + virtual void Resize(u32 width, u32 height) = 0; + virtual bool Evaluate(nvrhi::ICommandList* cmd, const DenoiseInputs& inputs) = 0; +}; + +IDenoiseBackend* CreateNullDenoiseBackend(); +IDenoiseBackend* CreateNRDDenoiseBackend(); +IDenoiseBackend* CreateAMDRayRegenBackend(); +IDenoiseBackend* CreateFFXDenoiseBackend(); +IDenoiseBackend* CreateDenoiseBackendAuto(bool preferDlssRR); + +bool IsVendorDenoiseActive(const IDenoiseBackend* backend); + +} diff --git a/src/Layers/xrRender/Denoising/NRDDenoiseBackend.cpp b/src/Layers/xrRender/Denoising/NRDDenoiseBackend.cpp new file mode 100644 index 00000000000..691ac8379fc --- /dev/null +++ b/src/Layers/xrRender/Denoising/NRDDenoiseBackend.cpp @@ -0,0 +1,734 @@ +#include "stdafx.h" +#include "IDenoiseBackend.h" + +#if defined(XRAY_USE_NRD) + +#include "Layers/xrRender/Backend/VulkanBackend.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/FrameGraphPasses/ShaderConstants.h" + +#include +#include + +#pragma push_macro("NONE") +#pragma push_macro("TRUE") +#pragma push_macro("FALSE") +#pragma push_macro("CONST") +#undef NONE +#undef TRUE +#undef FALSE +#undef CONST +#include "NRD.h" +#include "NRI.h" +#include "Extensions/NRIHelper.h" +#include "Extensions/NRIRayTracing.h" +#include "Extensions/NRIWrapperVK.h" +#include "NRDIntegration.hpp" +#pragma pop_macro("CONST") +#pragma pop_macro("FALSE") +#pragma pop_macro("TRUE") +#pragma pop_macro("NONE") + +extern ENGINE_API int ps_r_nrd_method; +extern ENGINE_API int ps_r_nrd_apply; +extern ENGINE_API int ps_r_upscale; + +namespace xray::render::fg { +namespace { + +struct NrdPackCB +{ + float worldToView[16]; + float worldToViewPrev[16]; + float worldToClip[16]; + float worldToClipPrev[16]; + float screenNearFar[4]; + float hitDistMethod[4]; + float cameraPosRange[4]; +}; + +struct NrdCompositeCB +{ + float params[4]; + float cameraPos[4]; + float fogParams[4]; + float fogColor[4]; +}; + +void CopyMatrix(float dst[16], const Fmatrix& m) +{ + dst[0] = m._11; dst[1] = m._12; dst[2] = m._13; dst[3] = m._14; + dst[4] = m._21; dst[5] = m._22; dst[6] = m._23; dst[7] = m._24; + dst[8] = m._31; dst[9] = m._32; dst[10] = m._33; dst[11] = m._34; + dst[12] = m._41; dst[13] = m._42; dst[14] = m._43; dst[15] = m._44; +} + +class NRDDenoiseBackend final : public IDenoiseBackend +{ + nvrhi::IDevice* m_device = nullptr; + nrd::Integration m_nrd; + nrd::Identifier m_denoiserId = 0; + bool m_ready = false; + bool m_logged = false; + u32 m_width = 0; + u32 m_height = 0; + int m_method = -1; + u32 m_pipeVersion = 0; + + nvrhi::TextureHandle m_inDiff; + nvrhi::TextureHandle m_inSpec; + nvrhi::TextureHandle m_inNormalRough; + nvrhi::TextureHandle m_inViewZ; + nvrhi::TextureHandle m_inMv; + nvrhi::TextureHandle m_outDiff; + nvrhi::TextureHandle m_outSpec; + + nvrhi::ComputePipelineHandle m_packPipeline; + nvrhi::BindingLayoutHandle m_packLayout; + nvrhi::ComputePipelineHandle m_compositePipeline; + nvrhi::BindingLayoutHandle m_compositeLayout; + nvrhi::BufferHandle m_cb; + u32 m_evalOkFrames = 0; + u32 m_evalFailFrames = 0; + u32 m_prevW = 0; + u32 m_prevH = 0; + u32 m_nrdFrameIndex = 0; + bool m_diagLogged = false; + + static constexpr float kDenoisingRange = 500000.f; + static constexpr u32 kPipeVersion = 9; + + void Fail(const char* reason) + { + if ((m_evalFailFrames++ % 120u) == 0u) + Msg("! [Denoise] NRD Evaluate failed: %s", reason); + } + + void SyncAfterNative(nvrhi::ICommandList* cmd) + { + cmd->clearState(); + cmd->beginTrackingTextureState(m_inDiff, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->beginTrackingTextureState(m_inSpec, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->beginTrackingTextureState(m_inNormalRough, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->beginTrackingTextureState(m_inViewZ, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->beginTrackingTextureState(m_inMv, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->beginTrackingTextureState(m_outDiff, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->beginTrackingTextureState(m_outSpec, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + } + + nvrhi::TextureHandle CreateTex(const char* name, nvrhi::Format fmt, u32 w, u32 h) + { + nvrhi::TextureDesc desc; + desc.width = w; + desc.height = h; + desc.format = fmt; + desc.mipLevels = 1; + desc.dimension = nvrhi::TextureDimension::Texture2D; + desc.debugName = name; + desc.isUAV = true; + desc.isShaderResource = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + return m_device->createTexture(desc); + } + + bool EnsurePipelines() + { + if (m_packPipeline && m_compositePipeline && m_cb && m_pipeVersion == kPipeVersion) + return true; + if (!GEnv.Render || !GEnv.Render->GetShaderLoader()) + return false; + + m_packPipeline = nullptr; + m_compositePipeline = nullptr; + m_packLayout = nullptr; + m_compositeLayout = nullptr; + m_cb = nullptr; + + framegraph::BindingSetBuilder::InvalidateReflectionCache(); + auto& cache = framegraph::GetPassResourceCache(); + { + auto cs = GEnv.Render->GetShaderLoader()->LoadComputeShader("nrd_pack_inputs"); + if (!cs.handle || !cs.reflection) + return false; + m_packLayout = cache.GetOrCreateBindingLayoutFromReflection("NRD_Pack_v9", *cs.reflection, m_device); + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = cs.handle; + pipeDesc.bindingLayouts = { m_packLayout }; + m_packPipeline = m_device->createComputePipeline(pipeDesc); + } + { + auto cs = GEnv.Render->GetShaderLoader()->LoadComputeShader("nrd_composite"); + if (!cs.handle || !cs.reflection) + return false; + m_compositeLayout = cache.GetOrCreateBindingLayoutFromReflection("NRD_Composite_v9", *cs.reflection, m_device); + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = cs.handle; + pipeDesc.bindingLayouts = { m_compositeLayout }; + m_compositePipeline = m_device->createComputePipeline(pipeDesc); + } + { + nvrhi::BufferDesc bd; + bd.byteSize = 512; + bd.isConstantBuffer = true; + bd.isVolatile = true; + bd.maxVersions = 16; + bd.debugName = "NRD_CB_v9"; + bd.initialState = nvrhi::ResourceStates::ConstantBuffer; + bd.keepInitialState = true; + m_cb = m_device->createBuffer(bd); + } + m_pipeVersion = kPipeVersion; + return m_packPipeline && m_compositePipeline && m_cb; + } + + bool EnsureResources(u32 w, u32 h) + { + if (m_inDiff && m_width == w && m_height == h && m_inMv && + m_inMv->getDesc().format == nvrhi::Format::RGBA16_FLOAT) + return true; + m_width = w; + m_height = h; + m_inDiff = CreateTex("NRD_IN_DIFF", nvrhi::Format::RGBA16_FLOAT, w, h); + m_inSpec = CreateTex("NRD_IN_SPEC", nvrhi::Format::RGBA16_FLOAT, w, h); + m_inNormalRough = CreateTex("NRD_IN_NR", nvrhi::Format::RGBA16_UNORM, w, h); + m_inViewZ = CreateTex("NRD_IN_VIEWZ", nvrhi::Format::R32_FLOAT, w, h); + m_inMv = CreateTex("NRD_IN_MV", nvrhi::Format::RGBA16_FLOAT, w, h); + m_outDiff = CreateTex("NRD_OUT_DIFF", nvrhi::Format::RGBA16_FLOAT, w, h); + m_outSpec = CreateTex("NRD_OUT_SPEC", nvrhi::Format::RGBA16_FLOAT, w, h); + return m_inDiff && m_inSpec && m_inNormalRough && m_inViewZ && m_inMv && m_outDiff && m_outSpec; + } + + bool RecreateIntegration(u32 w, u32 h, int method) + { + if (!GEnv.Backend || GEnv.Backend->GetAPI() != IRenderBackend::API::Vulkan) + return false; + + auto* vb = static_cast(GEnv.Backend); + nri::QueueFamilyVKDesc queueFamily{}; + queueFamily.queueNum = 1; + queueFamily.queueType = nri::QueueType::GRAPHICS; + queueFamily.familyIndex = vb->GetGraphicsQueueFamily(); + + nri::DeviceCreationVKDesc deviceDesc{}; + deviceDesc.vkInstance = vb->GetVkInstance(); + deviceDesc.vkDevice = vb->GetVkDevice(); + deviceDesc.vkPhysicalDevice = vb->GetVkPhysicalDevice(); + deviceDesc.queueFamilies = &queueFamily; + deviceDesc.queueFamilyNum = 1; + deviceDesc.minorVersion = 3; + const nrd::LibraryDesc* libDesc = nrd::GetLibraryDesc(); + deviceDesc.vkBindingOffsets = { + libDesc->spirvBindingOffsets.samplerOffset, + libDesc->spirvBindingOffsets.textureOffset, + libDesc->spirvBindingOffsets.constantBufferOffset, + libDesc->spirvBindingOffsets.storageTextureAndBufferOffset + }; + + nrd::DenoiserDesc denoiserDesc{}; + denoiserDesc.identifier = 0; + denoiserDesc.denoiser = (method != 0) + ? nrd::Denoiser::RELAX_DIFFUSE_SPECULAR + : nrd::Denoiser::REBLUR_DIFFUSE_SPECULAR; + + nrd::InstanceCreationDesc instanceDesc{}; + instanceDesc.denoisers = &denoiserDesc; + instanceDesc.denoisersNum = 1; + + nrd::IntegrationCreationDesc integrationDesc{}; + strncpy(integrationDesc.name, "OpenXRay NRD", sizeof(integrationDesc.name) - 1); + integrationDesc.resourceWidth = (uint16_t)w; + integrationDesc.resourceHeight = (uint16_t)h; + integrationDesc.queuedFrameNum = 3; + integrationDesc.enableWholeLifetimeDescriptorCaching = false; + integrationDesc.autoWaitForIdle = true; + + const nrd::Result result = m_nrd.RecreateVK(integrationDesc, instanceDesc, deviceDesc); + if (result != nrd::Result::SUCCESS) { + Msg("! [Denoise] NRD RecreateVK failed (%u)", (u32)result); + return false; + } + + m_denoiserId = denoiserDesc.identifier; + m_method = method; + m_width = w; + m_height = h; + m_nrdFrameIndex = 0; + m_prevW = 0; + m_prevH = 0; + m_diagLogged = false; + + const float fps = Device.fTimeDelta > 1e-4f ? (1.f / Device.fTimeDelta) : 60.f; + if (method != 0) { + nrd::RelaxSettings settings{}; + settings.hitDistanceReconstructionMode = nrd::HitDistanceReconstructionMode::AREA_3X3; + settings.enableAntiFirefly = true; + settings.diffusePrepassBlurRadius = 50.f; + settings.specularPrepassBlurRadius = 80.f; + settings.atrousIterationNum = 5; + settings.diffuseMaxAccumulatedFrameNum = nrd::GetMaxAccumulatedFrameNum(1.5f, fps); + settings.specularMaxAccumulatedFrameNum = nrd::GetMaxAccumulatedFrameNum(1.75f, fps); + settings.diffuseMaxFastAccumulatedFrameNum = 6; + settings.specularMaxFastAccumulatedFrameNum = 7; + m_nrd.SetDenoiserSettings(m_denoiserId, &settings); + } else { + nrd::ReblurSettings settings{}; + settings.hitDistanceReconstructionMode = nrd::HitDistanceReconstructionMode::AREA_3X3; + settings.enableAntiFirefly = true; + settings.diffusePrepassBlurRadius = 30.f; + settings.specularPrepassBlurRadius = 50.f; + settings.maxBlurRadius = 30.f; + settings.minBlurRadius = 1.f; + settings.maxAccumulatedFrameNum = nrd::GetMaxAccumulatedFrameNum(1.5f, fps); + settings.maxFastAccumulatedFrameNum = 6; + settings.maxStabilizedFrameNum = settings.maxAccumulatedFrameNum; + settings.minHitDistanceWeight = 0.1f; + settings.fireflySuppressorMinRelativeScale = 2.0f; + settings.lobeAngleFraction = 0.15f; + settings.roughnessFraction = 0.15f; + settings.planeDistanceSensitivity = 0.02f; + settings.antilagSettings.luminanceSigmaScale = 2.0f; + settings.antilagSettings.luminanceSensitivity = 3.0f; + settings.fastHistoryClampingSigmaScale = 2.0f; + m_nrd.SetDenoiserSettings(m_denoiserId, &settings); + } + + if (!m_logged) { + const nrd::LibraryDesc* lib = nrd::GetLibraryDesc(); + Msg("* [Denoise] NRD backend initialized v%u.%u.%u method=%s pool=%.1fMB", + lib ? lib->versionMajor : NRD_VERSION_MAJOR, + lib ? lib->versionMinor : NRD_VERSION_MINOR, + lib ? lib->versionBuild : NRD_VERSION_BUILD, + method != 0 ? "RELAX" : "REBLUR", + m_nrd.GetTotalMemoryUsageInMb()); + m_logged = true; + } + return true; + } + + nrd::Resource MakeVkResource(nvrhi::ITexture* tex) + { + nrd::Resource resource{}; + const auto native = tex->getNativeObject(nvrhi::ObjectTypes::VK_Image); + resource.vk.image = (VKNonDispatchableHandle)native.integer; + resource.vk.format = (VKEnum)nvrhi::vulkan::convertFormat(tex->getDesc().format); + resource.state = { nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE, nri::StageBits::COMPUTE_SHADER }; + resource.userArg = tex; + if (!resource.vk.image || !resource.vk.format) + Fail("MakeVkResource null image/format"); + return resource; + } + +public: + bool Init(nvrhi::IDevice* device) override + { + m_device = device; + if (!m_device || !GEnv.Backend || GEnv.Backend->GetAPI() != IRenderBackend::API::Vulkan) { + Msg("! [Denoise] NRD requires Vulkan backend"); + return false; + } + + const auto size = GEnv.Backend->GetBackBufferSize(); + u32 w = std::max(1u, size.first); + u32 h = std::max(1u, size.second); + if (!EnsurePipelines()) { + Msg("! [Denoise] NRD pack/composite shaders failed to load"); + return false; + } + if (!EnsureResources(w, h)) + return false; + if (!RecreateIntegration(w, h, ps_r_nrd_method)) + return false; + + m_ready = true; + return true; + } + + void Shutdown() override + { + m_nrd.Destroy(); + m_inDiff = nullptr; + m_inSpec = nullptr; + m_inNormalRough = nullptr; + m_inViewZ = nullptr; + m_inMv = nullptr; + m_outDiff = nullptr; + m_outSpec = nullptr; + m_packPipeline = nullptr; + m_packLayout = nullptr; + m_compositePipeline = nullptr; + m_compositeLayout = nullptr; + m_cb = nullptr; + m_ready = false; + m_logged = false; + m_diagLogged = false; + m_nrdFrameIndex = 0; + m_pipeVersion = 0; + m_width = 0; + m_height = 0; + m_method = -1; + m_prevW = 0; + m_prevH = 0; + } + + bool IsAvailable() const override { return m_ready; } + DenoiseBackendType GetType() const override { return DenoiseBackendType::NRD; } + + void Resize(u32 width, u32 height) override + { + if (!m_ready || !width || !height) + return; + if (width == m_width && height == m_height && m_method == ps_r_nrd_method) + return; + if (!EnsureResources(width, height)) + return; + RecreateIntegration(width, height, ps_r_nrd_method); + } + + bool Evaluate(nvrhi::ICommandList* cmd, const DenoiseInputs& inputs) override + { + if (!m_ready || !cmd) { + Fail("backend not ready"); + return false; + } + if (!inputs.noisyDiffuse || !inputs.noisySpecular || !inputs.normals || !inputs.depth || + !inputs.worldPos || !inputs.hitDistance || !inputs.directLighting || + !inputs.sceneColorIn || !inputs.outSceneColor) { + Fail("missing guide/signal textures"); + return false; + } + + const u32 w = inputs.width ? inputs.width : m_width; + const u32 h = inputs.height ? inputs.height : m_height; + if (!w || !h) { + Fail("zero resolution"); + return false; + } + + if (ps_r_nrd_method != m_method || w != m_width || h != m_height) { + if (!EnsureResources(w, h) || !RecreateIntegration(w, h, ps_r_nrd_method)) { + Fail("resize/recreate failed"); + return false; + } + } + if (!EnsurePipelines()) { + Fail("pack/composite pipelines missing"); + return false; + } + + Fmatrix worldToClip; + worldToClip.mul(Device.mProject, Device.mView); + Fmatrix worldToClipPrev; + worldToClipPrev.mul(Device.mProjectSaved, Device.mViewSaved); + + NrdPackCB packCB{}; + CopyMatrix(packCB.worldToView, Device.mView); + CopyMatrix(packCB.worldToViewPrev, Device.mViewSaved); + CopyMatrix(packCB.worldToClip, worldToClip); + CopyMatrix(packCB.worldToClipPrev, worldToClipPrev); + if (inputs.worldToView[0] != 0.f || inputs.worldToView[5] != 0.f || inputs.worldToView[10] != 0.f) + memcpy(packCB.worldToView, inputs.worldToView, sizeof(packCB.worldToView)); + if (inputs.worldToViewPrev[0] != 0.f || inputs.worldToViewPrev[5] != 0.f || inputs.worldToViewPrev[10] != 0.f) + memcpy(packCB.worldToViewPrev, inputs.worldToViewPrev, sizeof(packCB.worldToViewPrev)); + packCB.screenNearFar[0] = (float)w; + packCB.screenNearFar[1] = (float)h; + packCB.screenNearFar[2] = inputs.nearZ > 0.f ? inputs.nearZ : 0.2f; + packCB.screenNearFar[3] = inputs.farZ > 0.f ? inputs.farZ : 500.f; + packCB.hitDistMethod[0] = 3.f; + packCB.hitDistMethod[1] = 0.1f; + packCB.hitDistMethod[2] = 20.f; + packCB.hitDistMethod[3] = (m_method != 0) ? 1.f : 0.f; + packCB.cameraPosRange[0] = Device.vCameraPosition.x; + packCB.cameraPosRange[1] = Device.vCameraPosition.y; + packCB.cameraPosRange[2] = Device.vCameraPosition.z; + packCB.cameraPosRange[3] = kDenoisingRange; + + cmd->writeBuffer(m_cb, &packCB, sizeof(packCB)); + + { + auto* refl = GEnv.Render->GetShaderLoader()->GetCachedReflection("nrd_pack_inputs", ".cs"); + if (!refl) { + Fail("nrd_pack_inputs reflection missing"); + return false; + } + framegraph::BindingSetBuilder bsb(*refl, m_device, "NRD.Pack"); + bsb.ConstantBuffer("NrdPackParams", m_cb.Get()); + bsb.Texture("t_NoisyDiffuse", inputs.noisyDiffuse); + bsb.Texture("t_NoisySpecular", inputs.noisySpecular); + bsb.Texture("t_HitDist", inputs.hitDistance); + bsb.Texture("t_Normal", inputs.normals); + bsb.Texture("t_Depth", inputs.depth); + bsb.Texture("t_WorldPos", inputs.worldPos); + bsb.Texture("t_BaseColor", inputs.baseColor ? inputs.baseColor : inputs.normals); + bsb.TextureUAV("u_DiffRadianceHitDist", m_inDiff); + bsb.TextureUAV("u_SpecRadianceHitDist", m_inSpec); + bsb.TextureUAV("u_NormalRoughness", m_inNormalRough); + bsb.TextureUAV("u_ViewZ", m_inViewZ); + bsb.TextureUAV("u_MotionVectors", m_inMv); + auto& cache = framegraph::GetPassResourceCache(); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), m_packLayout, m_device); + if (!bindingSet) { + Fail("pack binding set"); + return false; + } + nvrhi::ComputeState cs; + cs.pipeline = m_packPipeline; + cs.bindings.push_back(bindingSet); + cmd->setComputeState(cs); + cmd->dispatch((w + 7) / 8, (h + 7) / 8, 1); + } + + cmd->setTextureState(m_inDiff, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->setTextureState(m_inSpec, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->setTextureState(m_inNormalRough, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->setTextureState(m_inViewZ, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->setTextureState(m_inMv, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->setTextureState(m_outDiff, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->setTextureState(m_outSpec, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->commitBarriers(); + cmd->clearState(); + + { + const float fps = Device.fTimeDelta > 1e-4f ? (1.f / Device.fTimeDelta) : 60.f; + if (m_method != 0) { + nrd::RelaxSettings settings{}; + settings.hitDistanceReconstructionMode = nrd::HitDistanceReconstructionMode::AREA_3X3; + settings.enableAntiFirefly = true; + settings.diffusePrepassBlurRadius = 50.f; + settings.specularPrepassBlurRadius = 80.f; + settings.atrousIterationNum = 5; + settings.diffuseMaxAccumulatedFrameNum = nrd::GetMaxAccumulatedFrameNum(1.5f, fps); + settings.specularMaxAccumulatedFrameNum = nrd::GetMaxAccumulatedFrameNum(1.75f, fps); + settings.diffuseMaxFastAccumulatedFrameNum = 6; + settings.specularMaxFastAccumulatedFrameNum = 7; + m_nrd.SetDenoiserSettings(m_denoiserId, &settings); + } else { + nrd::ReblurSettings settings{}; + settings.hitDistanceReconstructionMode = nrd::HitDistanceReconstructionMode::AREA_3X3; + settings.enableAntiFirefly = true; + settings.diffusePrepassBlurRadius = 30.f; + settings.specularPrepassBlurRadius = 50.f; + settings.maxBlurRadius = 30.f; + settings.minBlurRadius = 1.f; + settings.maxAccumulatedFrameNum = nrd::GetMaxAccumulatedFrameNum(1.5f, fps); + settings.maxFastAccumulatedFrameNum = 6; + settings.maxStabilizedFrameNum = settings.maxAccumulatedFrameNum; + settings.minHitDistanceWeight = 0.1f; + settings.fireflySuppressorMinRelativeScale = 2.0f; + settings.lobeAngleFraction = 0.15f; + settings.roughnessFraction = 0.15f; + settings.planeDistanceSensitivity = 0.02f; + settings.antilagSettings.luminanceSigmaScale = 2.0f; + settings.antilagSettings.luminanceSensitivity = 3.0f; + settings.fastHistoryClampingSigmaScale = 2.0f; + m_nrd.SetDenoiserSettings(m_denoiserId, &settings); + } + } + + m_nrd.NewFrame(); + + const bool needReset = inputs.reset || m_prevW != w || m_prevH != h || m_nrdFrameIndex == 0; + if (needReset) + m_nrdFrameIndex = 0; + + nrd::CommonSettings common{}; + CopyMatrix(common.viewToClipMatrix, Device.mProject); + CopyMatrix(common.viewToClipMatrixPrev, Device.mProjectSaved); + CopyMatrix(common.worldToViewMatrix, Device.mView); + CopyMatrix(common.worldToViewMatrixPrev, Device.mViewSaved); + + common.motionVectorScale[0] = 1.f / (float)w; + common.motionVectorScale[1] = 1.f / (float)h; + common.motionVectorScale[2] = 1.f; + common.cameraJitter[0] = clampr(inputs.jitterX, -0.5f, 0.5f); + common.cameraJitter[1] = clampr(inputs.jitterY, -0.5f, 0.5f); + common.cameraJitterPrev[0] = clampr(inputs.jitterPrevX, -0.5f, 0.5f); + common.cameraJitterPrev[1] = clampr(inputs.jitterPrevY, -0.5f, 0.5f); + common.resourceSize[0] = (uint16_t)w; + common.resourceSize[1] = (uint16_t)h; + common.resourceSizePrev[0] = (uint16_t)(m_prevW ? m_prevW : w); + common.resourceSizePrev[1] = (uint16_t)(m_prevH ? m_prevH : h); + common.rectSize[0] = (uint16_t)w; + common.rectSize[1] = (uint16_t)h; + common.rectSizePrev[0] = (uint16_t)(m_prevW ? m_prevW : w); + common.rectSizePrev[1] = (uint16_t)(m_prevH ? m_prevH : h); + common.viewZScale = 1.f; + common.denoisingRange = kDenoisingRange; + common.disocclusionThreshold = 0.02f; + common.disocclusionThresholdAlternate = 0.05f; + common.timeDeltaBetweenFrames = std::max(Device.fTimeDelta * 1000.f, 1.f); + common.frameIndex = m_nrdFrameIndex; + common.accumulationMode = needReset + ? nrd::AccumulationMode::CLEAR_AND_RESTART + : nrd::AccumulationMode::CONTINUE; + common.isMotionVectorInWorldSpace = false; + + if (!m_diagLogged) { + Msg("* [Denoise] NRD guides: jitter=(%.3f,%.3f) reset=%d frame=%u mv=2.5D", + common.cameraJitter[0], common.cameraJitter[1], needReset ? 1 : 0, m_nrdFrameIndex); + m_diagLogged = true; + } + + if (m_nrd.SetCommonSettings(common) != nrd::Result::SUCCESS) { + Fail("SetCommonSettings"); + SyncAfterNative(cmd); + return false; + } + + nrd::ResourceSnapshot snapshot; + snapshot.restoreInitialState = true; + snapshot.SetResource(nrd::ResourceType::IN_MV, MakeVkResource(m_inMv)); + snapshot.SetResource(nrd::ResourceType::IN_NORMAL_ROUGHNESS, MakeVkResource(m_inNormalRough)); + snapshot.SetResource(nrd::ResourceType::IN_VIEWZ, MakeVkResource(m_inViewZ)); + snapshot.SetResource(nrd::ResourceType::IN_DIFF_RADIANCE_HITDIST, MakeVkResource(m_inDiff)); + snapshot.SetResource(nrd::ResourceType::IN_SPEC_RADIANCE_HITDIST, MakeVkResource(m_inSpec)); + snapshot.SetResource(nrd::ResourceType::OUT_DIFF_RADIANCE_HITDIST, MakeVkResource(m_outDiff)); + snapshot.SetResource(nrd::ResourceType::OUT_SPEC_RADIANCE_HITDIST, MakeVkResource(m_outSpec)); + + nri::CommandBufferVKDesc cmdDesc{}; + cmdDesc.vkCommandBuffer = cmd->getNativeObject(nvrhi::ObjectTypes::VK_CommandBuffer); + cmdDesc.queueType = nri::QueueType::GRAPHICS; + if (!cmdDesc.vkCommandBuffer) { + Fail("null VK command buffer"); + SyncAfterNative(cmd); + return false; + } + + m_nrd.DenoiseVK(&m_denoiserId, 1, cmdDesc, snapshot); + SyncAfterNative(cmd); + + m_prevW = w; + m_prevH = h; + m_nrdFrameIndex++; + + if (ps_r_nrd_apply == 0) { + if ((m_evalOkFrames++ % 300u) == 0u) + Msg("* [Denoise] NRD Evaluate ok frames=%u method=%s %ux%u (apply=0, in-tree kept)", + m_evalOkFrames, m_method != 0 ? "RELAX" : "REBLUR", w, h); + return true; + } + + nvrhi::ITexture* outDiff = inputs.outDiffuse ? inputs.outDiffuse : inputs.noisyDiffuse; + nvrhi::ITexture* outSpec = inputs.outSpecular ? inputs.outSpecular : inputs.noisySpecular; + + cmd->setTextureState(m_outDiff, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(m_outSpec, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(inputs.directLighting, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(inputs.depth, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(inputs.sceneColorIn, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + if (inputs.baseColor) + cmd->setTextureState(inputs.baseColor, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(inputs.normals, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(inputs.worldPos, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + if (inputs.classifyWorldPos) + cmd->setTextureState(inputs.classifyWorldPos, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(inputs.outSceneColor, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->setTextureState(outDiff, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->setTextureState(outSpec, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->commitBarriers(); + + NrdCompositeCB compCB{}; + compCB.params[0] = (float)w; + compCB.params[1] = (float)h; + compCB.params[2] = (m_method != 0) ? 1.f : 0.f; + compCB.params[3] = (ps_r_upscale != 0) ? 1.f : 0.f; + compCB.cameraPos[0] = Device.vCameraPosition.x; + compCB.cameraPos[1] = Device.vCameraPosition.y; + compCB.cameraPos[2] = Device.vCameraPosition.z; + compCB.cameraPos[3] = 0.f; + { + passes::StaticGlobals fogFill{}; + passes::FillGlobalConstants(fogFill); + compCB.fogParams[0] = fogFill.fog_params.x; + compCB.fogParams[1] = fogFill.fog_params.y; + compCB.fogParams[2] = fogFill.fog_params.z; + compCB.fogParams[3] = fogFill.fog_params.w; + compCB.fogColor[0] = fogFill.fog_color.x; + compCB.fogColor[1] = fogFill.fog_color.y; + compCB.fogColor[2] = fogFill.fog_color.z; + compCB.fogColor[3] = fogFill.fog_color.w; + } + cmd->writeBuffer(m_cb, &compCB, sizeof(compCB)); + + { + auto* refl = GEnv.Render->GetShaderLoader()->GetCachedReflection("nrd_composite", ".cs"); + if (!refl) { + Fail("nrd_composite reflection missing"); + return false; + } + framegraph::BindingSetBuilder bsb(*refl, m_device, "NRD.Composite"); + bsb.ConstantBuffer("NrdCompositeParams", m_cb.Get()); + bsb.Texture("t_DirectLighting", inputs.directLighting); + bsb.Texture("t_DenoisedDiffuse", m_outDiff); + bsb.Texture("t_DenoisedSpecular", m_outSpec); + bsb.Texture("t_Depth", inputs.depth); + bsb.Texture("t_SceneColorIn", inputs.sceneColorIn); + bsb.Texture("t_BaseColor", inputs.baseColor ? inputs.baseColor : inputs.normals); + bsb.Texture("t_Normal", inputs.normals); + bsb.Texture("t_WorldPos", inputs.worldPos); + bsb.Texture("t_ClassifyWorldPos", inputs.classifyWorldPos ? inputs.classifyWorldPos : inputs.worldPos); + bsb.TextureUAV("u_SceneColor", inputs.outSceneColor); + bsb.TextureUAV("u_OutDiffuse", outDiff); + bsb.TextureUAV("u_OutSpecular", outSpec); + auto& cache = framegraph::GetPassResourceCache(); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), m_compositeLayout, m_device); + if (!bindingSet) { + Fail("composite binding set"); + return false; + } + nvrhi::ComputeState cs; + cs.pipeline = m_compositePipeline; + cs.bindings.push_back(bindingSet); + cmd->setComputeState(cs); + cmd->dispatch((w + 7) / 8, (h + 7) / 8, 1); + } + + if ((m_evalOkFrames++ % 300u) == 0u) + Msg("* [Denoise] NRD Evaluate ok frames=%u method=%s %ux%u apply=1", + m_evalOkFrames, m_method != 0 ? "RELAX" : "REBLUR", w, h); + return true; + } +}; + +} + +IDenoiseBackend* CreateNRDDenoiseBackend() +{ + return new NRDDenoiseBackend(); +} + +} + +#else + +namespace xray::render::fg { + +namespace { + +class NRDStubBackend final : public IDenoiseBackend +{ +public: + bool Init(nvrhi::IDevice*) override + { + Msg("* [Denoise] NRD not compiled in (XRAY_USE_NRD)"); + return false; + } + void Shutdown() override {} + bool IsAvailable() const override { return false; } + DenoiseBackendType GetType() const override { return DenoiseBackendType::NRD; } + void Resize(u32, u32) override {} + bool Evaluate(nvrhi::ICommandList*, const DenoiseInputs&) override { return false; } +}; + +} + +IDenoiseBackend* CreateNRDDenoiseBackend() +{ + return new NRDStubBackend(); +} + +} + +#endif diff --git a/src/Layers/xrRender/ETextureParams.cpp b/src/Layers/xrRender/ETextureParams.cpp index e59dc946398..decc42fd69b 100644 --- a/src/Layers/xrRender/ETextureParams.cpp +++ b/src/Layers/xrRender/ETextureParams.cpp @@ -43,7 +43,8 @@ const xr_token tbmode_token[] = {{"None", STextureParams::tbmNone}, {"Use", STex void STextureParams::Load(IReader& F) { - R_ASSERT(F.find_chunk(THM_CHUNK_TEXTUREPARAM)); + if (!F.find_chunk(THM_CHUNK_TEXTUREPARAM)) + return; F.r(&fmt, sizeof(ETFormat)); flags.assign(F.r_u32()); border_color = F.r_u32(); diff --git a/src/Layers/xrRender/FGDetailManager.cpp b/src/Layers/xrRender/FGDetailManager.cpp index 3a0c4e5a205..920d75cfa23 100644 --- a/src/Layers/xrRender/FGDetailManager.cpp +++ b/src/Layers/xrRender/FGDetailManager.cpp @@ -797,7 +797,11 @@ void FGDetailManager::PackSlotData() (u32(src.palette[3].a0) << 16) | (u32(src.palette[3].a1) << 20) | (u32(src.palette[3].a2) << 24) | (u32(src.palette[3].a3) << 28); + // Classic CoP (DetailManager_Decompress): Item.c_hemi + Item.c_sun from + // c_hemi / c_dir. Sector RGB (c_r/g/b) is R1-only — not used on R2+. dst.hemi = src.r_qclr(src.c_hemi, 15); + dst.sun = src.r_qclr(src.c_dir, 15); + dst._pad0 = dst._pad1 = dst._pad2 = 0.f; } } @@ -847,10 +851,10 @@ bool FGDetailManager::CreateGPUBuffers(nvrhi::IDevice* device) } { - constexpr float MIN_DENSITY = 0.04f; - constexpr u32 MAX_CAPACITY_BYTES = 512u * 1024u * 1024u; + constexpr u32 MAX_CAPACITY_BYTES = 128u * 1024u * 1024u; constexpr u32 MAX_CAPACITY = MAX_CAPACITY_BYTES / sizeof(InstanceData); - u32 d_size = u32(std::ceil(2.0f / MIN_DENSITY)); + const float density = std::max(ps_current_detail_density, 0.15f); + u32 d_size = u32(std::ceil(2.0f / density)); u32 grid_per_slot = (d_size + 1) * (d_size + 1); generatedInstancesCapacity = std::min(u32(float(slot_count) * float(grid_per_slot) * 0.08f), MAX_CAPACITY); generatedInstancesCapacity = std::max(generatedInstancesCapacity, 1000000u); @@ -880,7 +884,8 @@ bool FGDetailManager::CreateGPUBuffers(nvrhi::IDevice* device) float(desc.byteSize) / (1024.f * 1024.f)); } - visibleBufferCapacity = std::max(generatedInstancesCapacity / 4, 100000u); + constexpr u32 MAX_VISIBLE = 4u * 1024u * 1024u; + visibleBufferCapacity = std::min(std::max(generatedInstancesCapacity / 4, 100000u), MAX_VISIBLE); Msg("* [FGDetailManager] Initial visible buffer capacity: %u (%.1f MB per LOD)", visibleBufferCapacity, (visibleBufferCapacity * sizeof(u32)) / (1024.f * 1024.f)); @@ -2141,7 +2146,7 @@ bool FGDetailManager::CreateGraphicsPipeline(fg::RenderDevice* renderDevice, con return false; } - graphicsBindingLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("DetailGPU_CSMLadder", *vsRefl, *psRefl, device); + graphicsBindingLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("DetailGPU_CSMLadder_v4", *vsRefl, *psRefl, device); if (!graphicsBindingLayout) { Msg("! [FGDetailManager] Failed to create graphics binding layout"); @@ -2156,7 +2161,7 @@ bool FGDetailManager::CreateGraphicsPipeline(fg::RenderDevice* renderDevice, con return false; } - decalBindingLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("DetailDecal_CSMLadder", *decalVsRefl, *decalPsRefl, device); + decalBindingLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("DetailDecal_CSMLadder_v4", *decalVsRefl, *decalPsRefl, device); if (!decalBindingLayout) { Msg("! [FGDetailManager] Failed to create decal binding layout"); @@ -2232,6 +2237,24 @@ bool FGDetailManager::CreateGraphicsPipeline(fg::RenderDevice* renderDevice, con if (bindlessLayout) decalPipeDesc.bindingLayouts.push_back(bindlessLayout); } + decalPipeDesc.renderState.depthStencilState.depthTestEnable = true; + decalPipeDesc.renderState.depthStencilState.depthWriteEnable = true; + decalPipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + decalPipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + decalPipeDesc.renderState.rasterState.depthBias = -8; + decalPipeDesc.renderState.rasterState.slopeScaledDepthBias = -1.f; + decalPipeDesc.renderState.rasterState.depthBiasClamp = 0.f; + + auto& decalBlend = decalPipeDesc.renderState.blendState.targets[0]; + decalBlend.setBlendEnable(true); + decalBlend.setSrcBlend(nvrhi::BlendFactor::SrcAlpha); + decalBlend.setDestBlend(nvrhi::BlendFactor::InvSrcAlpha); + decalBlend.setBlendOp(nvrhi::BlendOp::Add); + decalBlend.setSrcBlendAlpha(nvrhi::BlendFactor::One); + decalBlend.setDestBlendAlpha(nvrhi::BlendFactor::InvSrcAlpha); + decalBlend.setBlendOpAlpha(nvrhi::BlendOp::Add); + for (u32 rt = 1; rt < 4; ++rt) + decalPipeDesc.renderState.blendState.targets[rt].setColorWriteMask(nvrhi::ColorMask(0)); decalGraphicsPipeline = device->createGraphicsPipeline(decalPipeDesc, fbInfo); if (!decalGraphicsPipeline) @@ -2246,7 +2269,7 @@ bool FGDetailManager::CreateGraphicsPipeline(fg::RenderDevice* renderDevice, con auto* bbVsRefl = shaderLoader->GetCachedReflection("detail_billboard", ".vs"); auto* bbPsRefl = shaderLoader->GetCachedReflection("detail_billboard", ".ps"); if (bbVsRefl && bbPsRefl) - billboardBindingLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("DetailBillboard_CSMLadder", *bbVsRefl, *bbPsRefl, device); + billboardBindingLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection("DetailBillboard_CSMLadder_v4", *bbVsRefl, *bbPsRefl, device); if (!billboardBindingLayout) billboardBindingLayout = decalBindingLayout; @@ -2302,8 +2325,11 @@ void FGDetailManager::DispatchCulling( if (m_instancesNeedRegeneration) { - constexpr u32 MAX_INSTANCES = 128u * 1024u * 1024u; - u32 d_size = u32(std::ceil(2.0f / ps_current_detail_density)); + constexpr u32 MAX_CAPACITY_BYTES = 128u * 1024u * 1024u; + constexpr u32 MAX_INSTANCES = MAX_CAPACITY_BYTES / sizeof(InstanceData); + constexpr u32 MAX_VISIBLE = 4u * 1024u * 1024u; + const float density = std::max(ps_current_detail_density, 0.15f); + u32 d_size = u32(std::ceil(2.0f / density)); u32 grid_per_slot = (d_size + 1) * (d_size + 1); u32 neededGenCapacity = std::min(u32(float(slot_count) * float(grid_per_slot) * 0.08f), MAX_INSTANCES); neededGenCapacity = std::max(neededGenCapacity, 1000000u); @@ -2324,10 +2350,11 @@ void FGDetailManager::DispatchCulling( generatedInstancesBuffer = device->createBuffer(desc); } - if (visibleBufferCapacity < generatedInstancesCapacity) + const u32 neededVisible = std::min(std::max(generatedInstancesCapacity / 4, 100000u), MAX_VISIBLE); + if (visibleBufferCapacity < neededVisible) { - Msg("[DetailManager] Pre-grow visible buffers: %u -> %u", visibleBufferCapacity, generatedInstancesCapacity); - visibleBufferCapacity = generatedInstancesCapacity; + Msg("[DetailManager] Pre-grow visible buffers: %u -> %u", visibleBufferCapacity, neededVisible); + visibleBufferCapacity = neededVisible; for (u32 lod = 0; lod < LOD_COUNT; lod++) { nvrhi::BufferDesc desc; @@ -2790,7 +2817,8 @@ void FGDetailManager::ResizeVisibleBuffersIfNeeded(nvrhi::IDevice* device) totalGeneratedInstances, generatedInstancesCapacity, 100.f * float(totalGeneratedInstances) / float(generatedInstancesCapacity)); - u32 newCapacity = std::max(totalGeneratedInstances, 100000u); + constexpr u32 MAX_VISIBLE = 4u * 1024u * 1024u; + u32 newCapacity = std::min(std::max(totalGeneratedInstances, 100000u), MAX_VISIBLE); bool needsGrow = newCapacity > visibleBufferCapacity; bool needsShrink = newCapacity < visibleBufferCapacity / 2; if (!needsGrow && !needsShrink) diff --git a/src/Layers/xrRender/FGDetailManager.h b/src/Layers/xrRender/FGDetailManager.h index c4428a01fc5..5e362d3402c 100644 --- a/src/Layers/xrRender/FGDetailManager.h +++ b/src/Layers/xrRender/FGDetailManager.h @@ -30,6 +30,8 @@ class FGDetailManager }; static_assert(sizeof(InstanceData) == 16, "InstanceData must be 16 bytes"); + // Matches classic DetailSlot lighting: c_hemi + c_dir (sun occlusion). + // CoP/R2/R3 do NOT apply sector RGB (c_r/g/b) to details — that is R1-only. struct GPUSlotData { float world_min_x; @@ -39,9 +41,13 @@ class FGDetailManager u32 packed_ids; u32 packed_palette_01; u32 packed_palette_23; - float hemi; + float hemi; // DetailSlot.c_hemi → ambient scale + float sun; // DetailSlot.c_dir → sun occlusion (gl/deffer_detail c0.x) + float _pad0; + float _pad1; + float _pad2; }; - static_assert(sizeof(GPUSlotData) == 32, "GPUSlotData must be 32 bytes"); + static_assert(sizeof(GPUSlotData) == 48, "GPUSlotData must be 48 bytes"); struct SlotAABB { diff --git a/src/Layers/xrRender/FGRenderBase.cpp b/src/Layers/xrRender/FGRenderBase.cpp index 45c75f8aca3..64401fb7821 100644 --- a/src/Layers/xrRender/FGRenderBase.cpp +++ b/src/Layers/xrRender/FGRenderBase.cpp @@ -13,6 +13,7 @@ #include "xrEngine/IRenderBackend.h" #include "xrEngine/GameFont.h" #include "xrEngine/PerformanceAlert.hpp" +#include "Upscaling/StreamlineDLSS.h" #include @@ -185,7 +186,16 @@ void FGRenderBase::End() if (GEnv.Backend) { GEnv.Backend->EndFrame(); - GEnv.Backend->Present(psDeviceFlags.test(rsVSync)); + const bool vsync = psDeviceFlags.test(rsVSync); + nvrhi::ITexture* fgInterp = nullptr; + nvrhi::ITexture* fgReal = nullptr; + if (Streamline_TakeFgPresent(fgInterp, fgReal)) + { + GEnv.Backend->Present(vsync); + GEnv.Backend->PresentFrameGeneration(fgInterp, fgReal); + return; + } + GEnv.Backend->Present(vsync); } } @@ -251,19 +261,6 @@ void FGRenderBase::DumpStatistics(IGameFont& font, IPerformanceAlert* alert) using namespace xray::render::pbr; void FGRenderBase::ConvertLegacyAssetsToPBR() { - if (ps_r4_use_pbr == 0) - return; - - if (m_pbrConversionThread.joinable()) - return; - - Msg("~ [PBR] PBR rendering enabled, checking texture conversion..."); - - m_pbrConversionThread = std::thread([this] - { - ConvertLegacyAssetsToPBRImpl(); - ConversionProgress::Get().EndJob(); - }); } void FGRenderBase::RenderPBRConversionUI() diff --git a/src/Layers/xrRender/FGRenderHost.cpp b/src/Layers/xrRender/FGRenderHost.cpp index 030cfb99a09..921b6c48b43 100644 --- a/src/Layers/xrRender/FGRenderHost.cpp +++ b/src/Layers/xrRender/FGRenderHost.cpp @@ -66,7 +66,7 @@ void FGRenderHost::ResizeBackend(SDL_Window* hWnd, u32& dwWidth, u32& dwHeight) return; int w, h; - SDL_GetWindowSize(hWnd, &w, &h); + SDL_GetWindowSizeInPixels(hWnd, &w, &h); GEnv.Backend->ResizeSwapChain(static_cast(w), static_cast(h)); std::tie(dwWidth, dwHeight) = GEnv.Backend->GetBackBufferSize(); } diff --git a/src/Layers/xrRender/FrameGraph/BindingSetBuilder.cpp b/src/Layers/xrRender/FrameGraph/BindingSetBuilder.cpp index 583af39e883..35d2d57348b 100644 --- a/src/Layers/xrRender/FrameGraph/BindingSetBuilder.cpp +++ b/src/Layers/xrRender/FrameGraph/BindingSetBuilder.cpp @@ -14,11 +14,14 @@ static bool NameMatches(const char* reflName, const char* queryName) static void DeduplicateBySlotAndClass(xr_vector& vec) { + // Prefer the later entry (typically PS over VS) so fullscreen VS junk at a + // shared auto-slot cannot hide the pixel-shader resource name (e.g. g_Normal). for (size_t i = 0; i < vec.size(); ++i) { for (size_t j = i + 1; j < vec.size(); ) { - if (vec[i].slot == vec[j].slot) + if (vec[i].slot == vec[j].slot) { + vec[i] = vec[j]; vec.erase(vec.begin() + j); - else + } else ++j; } } @@ -38,6 +41,7 @@ void Collect(BindingSetBuilder::ReflectedLists& lists, switch (tex.shape) { case ResourceShape::StructuredBuffer: rt = nvrhi::ResourceType::StructuredBuffer_SRV; break; case ResourceShape::RawBuffer: rt = nvrhi::ResourceType::RawBuffer_SRV; break; + case ResourceShape::TypedBuffer: rt = nvrhi::ResourceType::TypedBuffer_SRV; break; case ResourceShape::AccelStruct: rt = nvrhi::ResourceType::RayTracingAccelStruct; break; default: break; } @@ -49,6 +53,7 @@ void Collect(BindingSetBuilder::ReflectedLists& lists, switch (uav.shape) { case ResourceShape::StructuredBuffer: rt = nvrhi::ResourceType::StructuredBuffer_UAV; break; case ResourceShape::RawBuffer: rt = nvrhi::ResourceType::RawBuffer_UAV; break; + case ResourceShape::TypedBuffer: rt = nvrhi::ResourceType::TypedBuffer_UAV; break; default: break; } lists.uavs.push_back({ uav.name.c_str(), uav.slot, rt }); @@ -116,13 +121,25 @@ BindingSetBuilder::BindingSetBuilder( + m_lists->cbs.size() + m_lists->samplerItems.size()); } +static void WarnMissingOnce(const char* kind, const char* name) +{ + if (!name || !name[0]) + return; + static xr_set s_warned; + xr_string key; + key = kind; + key += ":"; + key += name; + if (!s_warned.insert(key).second) + return; + Msg("! [BindingSetBuilder] %s '%s' not in reflection (optional bind skipped)", kind, name); +} + int BindingSetBuilder::FindSRVSlot(const char* name) const { for (const auto& r : m_lists->srvs) if (NameMatches(r.name, name)) return static_cast(r.slot); - Msg("! [BindingSetBuilder] SRV '%s' not found in reflection (have %u SRVs)", name, m_lists->srvs.size()); - for (const auto& r : m_lists->srvs) - Msg(" SRV: '%s' @ t%u", r.name ? r.name : "(null)", r.slot); + WarnMissingOnce("SRV", name); return -1; } @@ -130,9 +147,7 @@ int BindingSetBuilder::FindUAVSlot(const char* name) const { for (const auto& r : m_lists->uavs) if (NameMatches(r.name, name)) return static_cast(r.slot); - Msg("! [BindingSetBuilder] UAV '%s' not found in reflection (have %u UAVs)", name, m_lists->uavs.size()); - for (const auto& r : m_lists->uavs) - Msg(" UAV: '%s' @ u%u", r.name ? r.name : "(null)", r.slot); + WarnMissingOnce("UAV", name); return -1; } @@ -140,9 +155,7 @@ int BindingSetBuilder::FindCBSlot(const char* name) const { for (const auto& r : m_lists->cbs) if (NameMatches(r.name, name)) return static_cast(r.slot); - Msg("! [BindingSetBuilder] CB '%s' not found in reflection (have %u CBs)", name, m_lists->cbs.size()); - for (const auto& r : m_lists->cbs) - Msg(" CB: '%s' @ b%u", r.name ? r.name : "(null)", r.slot); + WarnMissingOnce("CB", name); return -1; } @@ -182,6 +195,8 @@ BindingSetBuilder& BindingSetBuilder::BufferSRV(const char* name, nvrhi::IBuffer if (NameMatches(r.name, name)) { if (r.layoutType == nvrhi::ResourceType::RawBuffer_SRV) m_desc.bindings.push_back(nvrhi::BindingSetItem::RawBuffer_SRV(slot, buffer)); + else if (r.layoutType == nvrhi::ResourceType::TypedBuffer_SRV) + m_desc.bindings.push_back(nvrhi::BindingSetItem::TypedBuffer_SRV(slot, buffer)); else m_desc.bindings.push_back(nvrhi::BindingSetItem::StructuredBuffer_SRV(slot, buffer)); break; @@ -199,6 +214,8 @@ BindingSetBuilder& BindingSetBuilder::BufferUAV(const char* name, nvrhi::IBuffer if (NameMatches(r.name, name)) { if (r.layoutType == nvrhi::ResourceType::RawBuffer_UAV) m_desc.bindings.push_back(nvrhi::BindingSetItem::RawBuffer_UAV(slot, buffer)); + else if (r.layoutType == nvrhi::ResourceType::TypedBuffer_UAV) + m_desc.bindings.push_back(nvrhi::BindingSetItem::TypedBuffer_UAV(slot, buffer)); else m_desc.bindings.push_back(nvrhi::BindingSetItem::StructuredBuffer_UAV(slot, buffer)); break; @@ -298,6 +315,23 @@ nvrhi::BindingSetDesc BindingSetBuilder::Build() return a.slot < b.slot; }); + if (m_desc.bindings.size() > 1) { + size_t w = 0; + for (size_t r = 0; r < m_desc.bindings.size(); ++r) { + const auto& item = m_desc.bindings[r]; + const int cls = GetBindingSetRegisterClass(item.type); + if (w > 0) { + const auto& prev = m_desc.bindings[w - 1]; + if (GetBindingSetRegisterClass(prev.type) == cls && prev.slot == item.slot) + --w; + } + if (w != r) + m_desc.bindings[w] = item; + ++w; + } + m_desc.bindings.resize(w); + } + return std::move(m_desc); } diff --git a/src/Layers/xrRender/FrameGraph/FGResourcePool.cpp b/src/Layers/xrRender/FrameGraph/FGResourcePool.cpp index eb24678fa5d..35a8f9de44d 100644 --- a/src/Layers/xrRender/FrameGraph/FGResourcePool.cpp +++ b/src/Layers/xrRender/FrameGraph/FGResourcePool.cpp @@ -70,13 +70,18 @@ void FGResourcePool::FreeTexture(resources::TextureHandle handle) { return; } - // Add to pool for potential reuse + if (meta->state == resources::TextureState::Failed || !meta->nvrhiTexture) + { + m_resourceManager->GetTextureManager()->Release(handle); + if (m_stats.texturesActive > 0) + m_stats.texturesActive--; + return; + } + if (m_aliasingEnabled) { - // Check if this texture is already in the pool bool foundInPool = false; for (auto& pooled : m_texturePool) { if (pooled.handle.index == handle.index && pooled.handle.generation == handle.generation) { - // Already in pool - just mark as available pooled.inUse = false; pooled.lastUsedFrame = m_currentFrame; foundInPool = true; @@ -84,7 +89,6 @@ void FGResourcePool::FreeTexture(resources::TextureHandle handle) { } } - // If not in pool, add it if (!foundInPool) { PooledTexture pooled; pooled.handle = handle; @@ -96,7 +100,6 @@ void FGResourcePool::FreeTexture(resources::TextureHandle handle) { } } else { - // Aliasing disabled - actually destroy m_resourceManager->GetTextureManager()->Release(handle); m_stats.texturesActive--; } @@ -152,7 +155,10 @@ bool FGResourcePool::AreTexturesCompatible( a.height == b.height && a.format == b.format && a.mipLevels == b.mipLevels && - a.arraySize == b.arraySize; + a.arraySize == b.arraySize && + a.isUAV == b.isUAV && + a.isRenderTarget == b.isRenderTarget && + a.isDepthStencil == b.isDepthStencil; } // ═══════════════════════════════════════════════════ diff --git a/src/Layers/xrRender/FrameGraph/FrameGraph.cpp b/src/Layers/xrRender/FrameGraph/FrameGraph.cpp index e9ce9f245e3..eba77d4fc27 100644 --- a/src/Layers/xrRender/FrameGraph/FrameGraph.cpp +++ b/src/Layers/xrRender/FrameGraph/FrameGraph.cpp @@ -1177,10 +1177,6 @@ void FrameGraph::OptimizeMemoryAliasing() { compatible = false; } - // Exact dimensions required — aliasing a half-res bloom mip onto a - // full-res RT makes DrawFS write into the top-left corner only - // (viewport = logical size) while UV sampling covers the whole - // oversized texture → black frame + nested thumbnails. if (current->desc.type != ResourceDesc::Type::Buffer) { if (current->desc.width != candidate->desc.width || current->desc.height != candidate->desc.height || @@ -1190,6 +1186,13 @@ void FrameGraph::OptimizeMemoryAliasing() { current->desc.sampleCount != candidate->desc.sampleCount) { compatible = false; } + const bool curUAV = current->desc.isUAV || current->desc.allowUAV; + const bool candUAV = candidate->desc.isUAV || candidate->desc.allowUAV; + if (curUAV != candUAV || + current->desc.isRenderTarget != candidate->desc.isRenderTarget || + current->desc.isDepthStencil != candidate->desc.isDepthStencil) { + compatible = false; + } } // Candidate must be large enough diff --git a/src/Layers/xrRender/FrameGraph/PassResourceCache.cpp b/src/Layers/xrRender/FrameGraph/PassResourceCache.cpp index 6c13c1a1680..58407efda1b 100644 --- a/src/Layers/xrRender/FrameGraph/PassResourceCache.cpp +++ b/src/Layers/xrRender/FrameGraph/PassResourceCache.cpp @@ -161,6 +161,31 @@ nvrhi::ITexture* PassResourceCache::GetDummyShadowMap2D(nvrhi::IDevice* device) return m_dummyShadowMap2D; } +nvrhi::ITexture* PassResourceCache::GetDummyLocalShadowESM(nvrhi::IDevice* device) { + if (!m_dummyLocalShadowESM && device) { + nvrhi::TextureDesc desc; + desc.width = 1; + desc.height = 1; + desc.arraySize = 4; + desc.format = nvrhi::Format::R16_FLOAT; + desc.debugName = "DummyLocalShadowESM"; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + desc.dimension = nvrhi::TextureDimension::Texture2DArray; + desc.isShaderResource = true; + m_dummyLocalShadowESM = device->createTexture(desc); + if (m_dummyLocalShadowESM) { + nvrhi::CommandListHandle cmd = device->createCommandList(); + cmd->open(); + cmd->clearTextureFloat( + m_dummyLocalShadowESM, nvrhi::AllSubresources, nvrhi::Color(1.f)); + cmd->close(); + device->executeCommandList(cmd); + } + } + return m_dummyLocalShadowESM; +} + nvrhi::ITexture* PassResourceCache::GetDummyContactDepth(nvrhi::IDevice* device) { if (!m_dummyContactDepth && device) { nvrhi::TextureDesc desc; @@ -228,6 +253,36 @@ nvrhi::ITexture* PassResourceCache::GetDummyCubeMap(nvrhi::IDevice* device) { return m_dummyCubeMap; } +nvrhi::ITexture* PassResourceCache::GetDummyCubeArray(nvrhi::IDevice* device) { + if (!m_dummyCubeArray && device) { + nvrhi::TextureDesc desc; + desc.width = 1; + desc.height = 1; + desc.format = nvrhi::Format::RGBA8_UNORM; + desc.dimension = nvrhi::TextureDimension::TextureCubeArray; + desc.arraySize = 6; + desc.debugName = "DummyCubeArray"; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + desc.isShaderResource = true; + m_dummyCubeArray = device->createTexture(desc); + } + return m_dummyCubeArray; +} + +nvrhi::IBuffer* PassResourceCache::GetDummyStructuredBuffer(nvrhi::IDevice* device) { + if (!m_dummyStructuredBuffer && device) { + nvrhi::BufferDesc bd; + bd.byteSize = sizeof(float) * 4 * 64; + bd.structStride = sizeof(float) * 4; + bd.initialState = nvrhi::ResourceStates::ShaderResource; + bd.keepInitialState = true; + bd.debugName = "DummyStructuredBuffer"; + m_dummyStructuredBuffer = device->createBuffer(bd); + } + return m_dummyStructuredBuffer; +} + nvrhi::ISampler* PassResourceCache::GetSamplerByName(const char* smpName, nvrhi::IDevice* device) { if (strstr(smpName, "smp_nofilter") || strstr(smpName, "smp_smap") || strstr(smpName, "smp_jitter")) @@ -253,6 +308,14 @@ nvrhi::BindingLayoutHandle PassResourceCache::GetOrCreateBindingLayout( nvrhi::IDevice* device) { u64 key = HashString(passName); + key = HashCombine(key, u64(desc.visibility)); + key = HashCombine(key, u64(desc.bindings.size())); + for (const auto& b : desc.bindings) + { + key = HashCombine(key, u64(b.type)); + key = HashCombine(key, u64(b.slot)); + key = HashCombine(key, u64(b.size)); + } auto it = m_bindingLayouts.find(key); if (it != m_bindingLayouts.end()) { @@ -260,7 +323,6 @@ nvrhi::BindingLayoutHandle PassResourceCache::GetOrCreateBindingLayout( return it->second; } - // Create new layout m_stats.layoutMisses++; nvrhi::BindingLayoutHandle layout = device->createBindingLayout(desc); if (layout) { @@ -564,6 +626,8 @@ void PassResourceCache::Clear() { m_dummyContactDepth = nullptr; m_dummyContactHistory = nullptr; m_dummyCubeMap = nullptr; + m_dummyCubeArray = nullptr; + m_dummyStructuredBuffer = nullptr; Msg("* [PassResourceCache] Cleared all caches"); } diff --git a/src/Layers/xrRender/FrameGraph/PassResourceCache.h b/src/Layers/xrRender/FrameGraph/PassResourceCache.h index de1c458508e..49a2674bbd3 100644 --- a/src/Layers/xrRender/FrameGraph/PassResourceCache.h +++ b/src/Layers/xrRender/FrameGraph/PassResourceCache.h @@ -51,11 +51,14 @@ class PassResourceCache { nvrhi::ISampler* GetShadowCmpSampler(nvrhi::IDevice* device); nvrhi::ITexture* GetDummyShadowMap(nvrhi::IDevice* device); nvrhi::ITexture* GetDummyShadowMap2D(nvrhi::IDevice* device); + nvrhi::ITexture* GetDummyLocalShadowESM(nvrhi::IDevice* device); /// 1x1 far depth (1.0) for g_ContactDepth @ t27 when prev-frame depth is unavailable nvrhi::ITexture* GetDummyContactDepth(nvrhi::IDevice* device); /// 1x1 white (1.0 = lit) for g_ContactHistory @ t28 nvrhi::ITexture* GetDummyContactHistory(nvrhi::IDevice* device); nvrhi::ITexture* GetDummyCubeMap(nvrhi::IDevice* device); + nvrhi::ITexture* GetDummyCubeArray(nvrhi::IDevice* device); + nvrhi::IBuffer* GetDummyStructuredBuffer(nvrhi::IDevice* device); nvrhi::ISampler* GetSamplerByName(const char* smpName, nvrhi::IDevice* device); @@ -202,9 +205,12 @@ class PassResourceCache { nvrhi::SamplerHandle m_commonShadowCmp; nvrhi::TextureHandle m_dummyShadowMap; nvrhi::TextureHandle m_dummyShadowMap2D; + nvrhi::TextureHandle m_dummyLocalShadowESM; nvrhi::TextureHandle m_dummyContactDepth; nvrhi::TextureHandle m_dummyContactHistory; nvrhi::TextureHandle m_dummyCubeMap; + nvrhi::TextureHandle m_dummyCubeArray; + nvrhi::BufferHandle m_dummyStructuredBuffer; Stats m_stats; diff --git a/src/Layers/xrRender/FrameGraph/ShaderCache.cpp b/src/Layers/xrRender/FrameGraph/ShaderCache.cpp index f6a532d68ea..9824e183857 100644 --- a/src/Layers/xrRender/FrameGraph/ShaderCache.cpp +++ b/src/Layers/xrRender/FrameGraph/ShaderCache.cpp @@ -23,13 +23,24 @@ void ShaderCache::GetCachePath( u32 sourceHash, string_path& outPath) { + // Shader names use '/' (ao/ssao) or '\' (effects\flare). VerifyPath only + // splits on '\', so forward slashes must be normalized or mkdir("vk/ao/ssao.ps") + // fails on Linux when intermediate dirs are missing. + string_path normalizedName; + xr_strcpy(normalizedName, shaderName); + for (char* p = normalizedName; *p; ++p) + { + if (*p == '/') + *p = _DELIMITER; + } + string_path shaderDir; if (m_backendSubdir.empty()) xr_sprintf(shaderDir, "shaders_cache_fg%s%s%s", - DELIMITER, shaderName, extension); + DELIMITER, normalizedName, extension); else xr_sprintf(shaderDir, "shaders_cache_fg%s%s%s%s%s", - DELIMITER, m_backendSubdir.c_str(), DELIMITER, shaderName, extension); + DELIMITER, m_backendSubdir.c_str(), DELIMITER, normalizedName, extension); xr_sprintf(outPath, "%s%s%08X", shaderDir, DELIMITER, sourceHash); @@ -52,15 +63,15 @@ bool ShaderCache::TryLoad( string_path cachePath; GetCachePath(shaderName, extension, sourceHash, cachePath); - // Check if cache file exists - if (!FS.exist("$app_data_root$", cachePath)) + string_path fullPath; + FS.update_path(fullPath, "$app_data_root$", cachePath); + if (!FS.exist(fullPath, FSType::External)) { m_stats.misses++; return false; } - // Open cache file - IReader* reader = FS.r_open("$app_data_root$", cachePath); + IReader* reader = FS.r_open(fullPath); if (!reader) { m_stats.misses++; @@ -129,9 +140,11 @@ void ShaderCache::Save( // Write cache file (directory will be created automatically by w_open) IWriter* writer = FS.w_open("$app_data_root$", cachePath); - if (!writer) + if (!writer || !writer->valid()) { Msg("! [ShaderCache] Failed to create cache file: %s", cachePath); + if (writer) + FS.w_close(writer); return; } diff --git a/src/Layers/xrRender/FrameGraph/ShaderCache.h b/src/Layers/xrRender/FrameGraph/ShaderCache.h index 9e6fb109cb9..781df86a930 100644 --- a/src/Layers/xrRender/FrameGraph/ShaderCache.h +++ b/src/Layers/xrRender/FrameGraph/ShaderCache.h @@ -132,7 +132,7 @@ class ShaderCache ExtractedReflection& outReflection ); - static constexpr u32 CACHE_VERSION = 168; // grass SSS + vein CB + CoP billboard glow + static constexpr u32 CACHE_VERSION = 196; Stats m_stats; bool m_cacheEnabled; xr_string m_backendSubdir; diff --git a/src/Layers/xrRender/FrameGraph/ShaderLoader.cpp b/src/Layers/xrRender/FrameGraph/ShaderLoader.cpp index 4a3987fbe61..015232d1ca9 100644 --- a/src/Layers/xrRender/FrameGraph/ShaderLoader.cpp +++ b/src/Layers/xrRender/FrameGraph/ShaderLoader.cpp @@ -91,6 +91,74 @@ void ResolveShaderSourceRelativePath( strconcat(outRelativePathSize, outRelativePath, "r5" DELIMITER, shName, extension); } +u32 MixHash(u32 a, u32 b) +{ + return a ^ ((b << 16) | (b >> 16)) ^ (b * 0x9E3779B9u); +} + +u32 HashShaderSourceWithIncludes(const char* source, size_t sourceLen, int depth) +{ + u32 hash = ShaderCache::ComputeHash(source, sourceLen); + if (!source || sourceLen == 0 || depth > 8) + return hash; + + const char* p = source; + const char* end = source + sourceLen; + while (p < end) + { + const char* line = p; + while (p < end && *p != '\n') + ++p; + const char* lineEnd = p; + if (p < end) + ++p; + + while (line < lineEnd && (*line == ' ' || *line == '\t')) + ++line; + if (line + 8 >= lineEnd || strncmp(line, "#include", 8) != 0) + continue; + line += 8; + while (line < lineEnd && (*line == ' ' || *line == '\t')) + ++line; + if (line >= lineEnd || (*line != '"' && *line != '<')) + continue; + const char quote = (*line == '"') ? '"' : '>'; + ++line; + const char* pathStart = line; + while (line < lineEnd && *line != quote) + ++line; + if (line <= pathStart || line >= lineEnd) + continue; + + string_path includeName; + const size_t pathLen = static_cast(line - pathStart); + if (pathLen >= sizeof(includeName)) + continue; + memcpy(includeName, pathStart, pathLen); + includeName[pathLen] = 0; + for (char* c = includeName; *c; ++c) + { + if (*c == '/') + *c = '\\'; + } + + string_path filename; + strconcat(sizeof(filename), filename, "r5" DELIMITER, includeName); + IReader* inc = FS.r_open("$game_shaders$", filename); + if (!inc) + continue; + hash = MixHash(hash, HashShaderSourceWithIncludes( + (const char*)inc->pointer(), inc->length(), depth + 1)); + FS.r_close(inc); + } + return hash; +} + +u32 HashShaderSourceWithIncludes(const char* source, size_t sourceLen) +{ + return HashShaderSourceWithIncludes(source, sourceLen, 0); +} + } // namespace ShaderLoader::ShaderLoader(xray::render::SlangCompiler* slangCompiler) @@ -169,7 +237,7 @@ bool ShaderLoader::CompileShader( return false; // Compute hash of shader source - u32 sourceHash = ShaderCache::ComputeHash( + u32 sourceHash = HashShaderSourceWithIncludes( (const char*)fs->pointer(), fs->length() ); @@ -242,7 +310,7 @@ ShaderLoader::ShaderResult ShaderLoader::LoadVertexShader( WatchShaderFile(cacheKey, name, ".vs", entryPoint, xray::render::SlangCompiler::Stage::Vertex); // Compute hash of shader source - u32 sourceHash = ShaderCache::ComputeHash( + u32 sourceHash = HashShaderSourceWithIncludes( (const char*)fs->pointer(), fs->length() ); @@ -377,7 +445,7 @@ ShaderLoader::ShaderResult ShaderLoader::LoadPixelShader( WatchShaderFile(cacheKey, name, ".ps", entryPoint, xray::render::SlangCompiler::Stage::Pixel); // Compute hash of shader source - u32 sourceHash = ShaderCache::ComputeHash( + u32 sourceHash = HashShaderSourceWithIncludes( (const char*)fs->pointer(), fs->length() ); @@ -502,7 +570,7 @@ ShaderLoader::ShaderResult ShaderLoader::LoadHullShader(const char* name, const return result; WatchShaderFile(cacheKey, name, ".hs", entryPoint, xray::render::SlangCompiler::Stage::Hull); - u32 sourceHash = ShaderCache::ComputeHash((const char*)fs->pointer(), fs->length()); + u32 sourceHash = HashShaderSourceWithIncludes((const char*)fs->pointer(), fs->length()); ExtractedReflection deserializedReflection; bool cacheHit = m_cache.TryLoad(name, ".hs", sourceHash, result.bytecode, &deserializedReflection); if (cacheHit) @@ -572,7 +640,7 @@ ShaderLoader::ShaderResult ShaderLoader::LoadDomainShader(const char* name, cons return result; WatchShaderFile(cacheKey, name, ".ds", entryPoint, xray::render::SlangCompiler::Stage::Domain); - u32 sourceHash = ShaderCache::ComputeHash((const char*)fs->pointer(), fs->length()); + u32 sourceHash = HashShaderSourceWithIncludes((const char*)fs->pointer(), fs->length()); ExtractedReflection deserializedReflection; bool cacheHit = m_cache.TryLoad(name, ".ds", sourceHash, result.bytecode, &deserializedReflection); if (cacheHit) @@ -659,7 +727,7 @@ ShaderLoader::ShaderResult ShaderLoader::LoadComputeShader( WatchShaderFile(cacheKey, name, ".cs", entryPoint, xray::render::SlangCompiler::Stage::Compute); // Compute hash of shader source - u32 sourceHash = ShaderCache::ComputeHash( + u32 sourceHash = HashShaderSourceWithIncludes( (const char*)fs->pointer(), fs->length() ); @@ -802,7 +870,7 @@ ShaderLoader::ShaderResult ShaderLoader::LoadAmplificationShader( WatchShaderFile(cacheKey, name, ".as", entryPoint, xray::render::SlangCompiler::Stage::Amplification); // Compute hash of shader source - u32 sourceHash = ShaderCache::ComputeHash( + u32 sourceHash = HashShaderSourceWithIncludes( (const char*)fs->pointer(), fs->length() ); @@ -912,7 +980,7 @@ ShaderLoader::ShaderResult ShaderLoader::LoadMeshShader( WatchShaderFile(cacheKey, name, ".ms", entryPoint, xray::render::SlangCompiler::Stage::Mesh); // Compute hash of shader source - u32 sourceHash = ShaderCache::ComputeHash( + u32 sourceHash = HashShaderSourceWithIncludes( (const char*)fs->pointer(), fs->length() ); @@ -1032,10 +1100,9 @@ bool ShaderLoader::CompileShaderWithDefines( definesStr.append(";"); } - u32 cacheKey = ShaderCache::ComputeHash( - sourceCode.c_str(), - sourceCode.length(), - definesStr.c_str() + u32 cacheKey = MixHash( + HashShaderSourceWithIncludes(sourceCode.c_str(), sourceCode.length()), + ShaderCache::ComputeHash("", 0, definesStr.c_str()) ); // Try to load from cache diff --git a/src/Layers/xrRender/FrameGraph/ShaderReflection.cpp b/src/Layers/xrRender/FrameGraph/ShaderReflection.cpp index 7844eda7273..8acbd17d757 100644 --- a/src/Layers/xrRender/FrameGraph/ShaderReflection.cpp +++ b/src/Layers/xrRender/FrameGraph/ShaderReflection.cpp @@ -1083,14 +1083,29 @@ static void FilterReflectionByUsage(ExtractedReflection& result, slang::ICompone srUsed, srUnused, dtsUsed, dtsUnused); } + auto keepPassResource = [](const char* n) -> bool { + if (!n || !n[0] || n[1] != '_') + return false; + const char p = n[0]; + return p == 'g' || p == 't' || p == 'u'; + }; + auto& textures = result.rtBindings.inputTextures; textures.erase(std::remove_if(textures.begin(), textures.end(), - [&](const auto& t) { return !isUsed(SLANG_PARAMETER_CATEGORY_SHADER_RESOURCE, t.slot); }), + [&](const auto& t) { + if (keepPassResource(t.name.c_str())) + return false; + return !isUsed(SLANG_PARAMETER_CATEGORY_SHADER_RESOURCE, t.slot); + }), textures.end()); auto& uavs = result.rtBindings.uavBindings; uavs.erase(std::remove_if(uavs.begin(), uavs.end(), - [&](const auto& u) { return !isUsed(SLANG_PARAMETER_CATEGORY_UNORDERED_ACCESS, u.slot); }), + [&](const auto& u) { + if (keepPassResource(u.name.c_str())) + return false; + return !isUsed(SLANG_PARAMETER_CATEGORY_UNORDERED_ACCESS, u.slot); + }), uavs.end()); auto& samplers = result.rtBindings.samplers; @@ -1120,22 +1135,35 @@ static void FilterReflectionByUsage(ExtractedReflection& result, slang::ICompone [&](const auto& cb) { return !isUsed(SLANG_PARAMETER_CATEGORY_CONSTANT_BUFFER, cb.slot); }), cbs.end()); + auto preferTex = [](const auto& a, const auto& b) -> bool { + const char* an = a.name.c_str(); + const char* bn = b.name.c_str(); + auto isPass = [](const char* n) { + return n && n[1] == '_' && (n[0] == 'g' || n[0] == 't' || n[0] == 'u'); + }; + const bool aPass = isPass(an); + const bool bPass = isPass(bn); + if (aPass != bPass) + return aPass; + if (a.shape == ResourceShape::Texture && b.shape != ResourceShape::Texture) + return true; + if (b.shape == ResourceShape::Texture && a.shape != ResourceShape::Texture) + return false; + return false; + }; for (size_t i = 0; i < textures.size(); ++i) { for (size_t j = i + 1; j < textures.size(); ) { if (textures[i].slot == textures[j].slot) { - if (textures[j].shape != ResourceShape::Texture && textures[i].shape == ResourceShape::Texture) + if (preferTex(textures[j], textures[i])) { textures.erase(textures.begin() + i); --i; break; } - else - { - textures.erase(textures.begin() + j); - } + textures.erase(textures.begin() + j); } else ++j; diff --git a/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.cpp index 2fa452cd73b..c6dd38b24fe 100644 --- a/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.cpp @@ -93,7 +93,7 @@ void InitBloom(nvrhi::IDevice* nv, BloomPassState& st) st.extractLayout = cache.GetOrCreateBindingLayoutFromReflection("BloomExtract_v3", *vs.reflection, *ex.reflection, nv); st.downLayout = cache.GetOrCreateBindingLayoutFromReflection("BloomDown_v3", *vs.reflection, *dn.reflection, nv); st.upLayout = cache.GetOrCreateBindingLayoutFromReflection("BloomUp_v3", *vs.reflection, *up.reflection, nv); - st.compLayout = cache.GetOrCreateBindingLayoutFromReflection("BloomComp_v3", *vs.reflection, *cp.reflection, nv); + st.compLayout = cache.GetOrCreateBindingLayoutFromReflection("BloomComp_v4", *vs.reflection, *cp.reflection, nv); if (st.extractLayout) st.extractPipe = MakeFSPipe("BloomExtract_v3", nv, vs.handle, ex.handle, st.extractLayout, nvrhi::Format::RGBA16_FLOAT, false); if (st.downLayout) @@ -101,7 +101,7 @@ void InitBloom(nvrhi::IDevice* nv, BloomPassState& st) if (st.upLayout) st.upPipe = MakeFSPipe("BloomUp_v3", nv, vs.handle, up.handle, st.upLayout, nvrhi::Format::RGBA16_FLOAT, false); if (st.compLayout) - st.compPipe = MakeFSPipe("BloomCompAdd_v3", nv, vs.handle, cp.handle, st.compLayout, nvrhi::Format::RGBA16_FLOAT, true); + st.compPipe = MakeFSPipe("BloomCompAdd_v4", nv, vs.handle, cp.handle, st.compLayout, nvrhi::Format::RGBA16_FLOAT, true); st.initialized = true; } @@ -140,6 +140,7 @@ VirtualResourceHandle setupBloomPass( FrameGraph& fg, fg::RenderDevice* device, VirtualResourceHandle sceneColor, + VirtualResourceHandle depth, u32 width, u32 height, BloomPassState& state) @@ -149,6 +150,8 @@ VirtualResourceHandle setupBloomPass( InitBloom(device->GetNVRHIDevice(), state); if (!state.extractPipe || !state.downPipe || !state.upPipe || !state.compPipe) return sceneColor; + if (!depth.is_valid()) + return sceneColor; // Non-transient: never alias bloom pyramids onto unrelated full-res RTs. auto makeRT = [&](const char* name, u32 w, u32 h) { @@ -190,6 +193,7 @@ VirtualResourceHandle setupBloomPass( struct PassData { VirtualResourceHandle scene; + VirtualResourceHandle depth; VirtualResourceHandle mips[kBloomMips]; VirtualResourceHandle upMips[kBloomMips - 1]; VirtualResourceHandle output; @@ -208,6 +212,7 @@ VirtualResourceHandle setupBloomPass( data.width = width; data.height = height; data.scene = pb.read(sceneColor, ResourceState::ShaderResource); + data.depth = pb.read(depth, ResourceState::ShaderResource); for (u32 i = 0; i < kBloomMips; ++i) { data.mips[i] = pb.write(mips[i], ResourceState::RenderTarget); @@ -236,10 +241,11 @@ VirtualResourceHandle setupBloomPass( return; auto* scene = graph.GetPhysicalTexture(data.scene); + auto* depthTex = graph.GetPhysicalTexture(data.depth); nvrhi::ITexture* mipTex[kBloomMips]; for (u32 i = 0; i < kBloomMips; ++i) mipTex[i] = graph.GetPhysicalTexture(data.mips[i]); - if (!scene || !mipTex[0]) + if (!scene || !depthTex || !mipTex[0]) return; struct alignas(16) P4 { float x, y, z, w; }; @@ -313,11 +319,12 @@ VirtualResourceHandle setupBloomPass( cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); cmd->setTextureState(bloomFull, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); - P4 p{intensity, 0, 0, 0}; + P4 p{intensity, 0.9995f, 0, 0}; cmd->writeBuffer(cb, &p, sizeof(p)); BindingSetBuilder bsb(*vsR, *cpR, nv, "Bloom.Comp"); bsb.ConstantBuffer("BloomParams", cb); bsb.Texture("g_Bloom", bloomFull); + bsb.Texture("g_Depth", depthTex); auto set = cache.GetOrCreateBindingSet(bsb.Build(), st->compLayout, nv); DrawFS(cmd, nv, st->compPipe, set, out, data.width, data.height, "BloomCompFB"); cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); diff --git a/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.h index 318b370211d..646845dbf85 100644 --- a/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.h @@ -29,6 +29,7 @@ framegraph::VirtualResourceHandle setupBloomPass( framegraph::FrameGraph& fg, fg::RenderDevice* device, framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, u32 width, u32 height, BloomPassState& state); diff --git a/src/Layers/xrRender/FrameGraphPasses/CameraModelPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/CameraModelPassSetup.cpp new file mode 100644 index 00000000000..a103438200d --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/CameraModelPassSetup.cpp @@ -0,0 +1,343 @@ +#include "stdafx.h" +#include "CameraModelPassSetup.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/xrRender_console.h" + +extern ECORE_API float ps_r2_mblur; +extern ENGINE_API int ps_r_upscale; + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +bool UpscaleOwnsTemporal() +{ + return ps_r_upscale != 0; +} + +void EnsureCameraPipelines(nvrhi::IDevice* nv, CameraModelPassState& st, nvrhi::Format outFmt) +{ + if (!nv) + return; + if (st.initialized && st.cameraPipeline && st.pipelineFormat == outFmt) + return; + + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + { + st.initialized = true; + return; + } + auto vs = loader->LoadVertexShader("fullscreen"); + auto camPs = loader->LoadPixelShader("camera_model"); + auto blurPs = loader->LoadPixelShader("motion_blur"); + if (!vs.handle || !camPs.handle) + { + st.initialized = true; + return; + } + + auto& cache = GetPassResourceCache(); + st.cameraLayout = cache.GetOrCreateBindingLayoutFromReflection( + "CameraModel_v1", *vs.reflection, *camPs.reflection, nv); + if (st.cameraLayout) + { + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs.handle); + desc.setPixelShader(camPs.handle); + desc.addBindingLayout(st.cameraLayout); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.blendState.targets[0].setBlendEnable(false); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + nvrhi::FramebufferInfoEx fb; + fb.addColorFormat(outFmt); + char pipeName[64]; + xr_sprintf(pipeName, "CameraModel_v1_fmt%u", (u32)outFmt); + st.cameraPipeline = cache.GetOrCreatePipeline(pipeName, desc, fb, nv); + } + + if (blurPs.handle) + { + st.mblurLayout = cache.GetOrCreateBindingLayoutFromReflection( + "MotionBlur_v1", *vs.reflection, *blurPs.reflection, nv); + if (st.mblurLayout) + { + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs.handle); + desc.setPixelShader(blurPs.handle); + desc.addBindingLayout(st.mblurLayout); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.blendState.targets[0].setBlendEnable(false); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + nvrhi::FramebufferInfoEx fb; + fb.addColorFormat(outFmt); + char pipeName[64]; + xr_sprintf(pipeName, "MotionBlur_v1_fmt%u", (u32)outFmt); + st.mblurPipeline = cache.GetOrCreatePipeline(pipeName, desc, fb, nv); + } + } + + st.pipelineFormat = outFmt; + st.initialized = true; +} + +float CameraDistortAmount() +{ + return (ps_r_camera && ps_r_camera_distort_enable) ? ps_r_camera_distort : 0.f; +} +float CameraCAAmount() +{ + return (ps_r_camera && ps_r_camera_ca_enable) ? ps_r_camera_ca : 0.f; +} +float CameraVignetteAmount() +{ + return (ps_r_camera && ps_r_camera_vignette_enable) ? ps_r_camera_vignette : 0.f; +} +float CameraGrainAmount() +{ + if (UpscaleOwnsTemporal()) + return 0.f; + return (ps_r_camera && ps_r_camera_grain_enable) ? ps_r_camera_grain : 0.f; +} +float CameraMblurAmount() +{ + if (UpscaleOwnsTemporal()) + return 0.f; + return (ps_r_camera && ps_r_camera_mblur_enable) ? ps_r2_mblur : 0.f; +} + +bool CameraEffectsActive() +{ + if (!ps_r_camera) + return false; + return CameraDistortAmount() > 1e-4f + || CameraCAAmount() > 1e-4f + || CameraVignetteAmount() > 1e-4f + || CameraGrainAmount() > 1e-4f + || CameraMblurAmount() > 1e-4f; +} +} // namespace + +VirtualResourceHandle setupCameraModelPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle ldrInput, + VirtualResourceHandle motionVectors, + VirtualResourceHandle outputTarget, + u32 width, + u32 height, + CameraModelPassState& state) +{ + if (!device || !device->GetNVRHIDevice() || !ldrInput.is_valid()) + return ldrInput; + if (!CameraEffectsActive()) + return ldrInput; + + nvrhi::Format outFmt = nvrhi::Format::RGBA8_UNORM; + if (GEnv.Backend && GEnv.Backend->GetBackBuffer()) + outFmt = GEnv.Backend->GetBackBuffer()->getDesc().format; + + EnsureCameraPipelines(device->GetNVRHIDevice(), state, outFmt); + if (!state.cameraPipeline || !state.cameraLayout) + return ldrInput; + + const bool wantMblur = CameraMblurAmount() > 1e-4f && motionVectors.is_valid() && state.mblurPipeline && state.mblurLayout; + + VirtualResourceHandle cameraOut = outputTarget; + if (!cameraOut.is_valid() || wantMblur) + { + ResourceDesc desc; + desc.type = ResourceDesc::Type::Texture2D; + desc.width = width; + desc.height = height; + desc.format = outFmt; + desc.isRenderTarget = true; + desc.isTransient = true; + desc.debugName = "rt_CameraModel"; + cameraOut = fg.CreateTexture("rt_CameraModel", desc); + } + + struct CamData + { + VirtualResourceHandle input, output; + u32 width, height; + CameraModelPassState* st = nullptr; + fg::RenderDevice* device = nullptr; + }; + + auto& camPass = fg.addCallbackPass( + "CameraModel", + [&](FrameGraph& b, PassHandle ph, CamData& data) { + RenderPassBuilder pb(b, ph); + data.st = &state; + data.device = device; + data.width = width; + data.height = height; + data.input = pb.read(ldrInput, ResourceState::ShaderResource); + data.output = pb.write(cameraOut, ResourceState::RenderTarget); + }, + [](const CamData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd ? cmd->getDevice() : nullptr; + auto* in = graph.GetPhysicalTexture(data.input); + auto* out = graph.GetPhysicalTexture(data.output); + if (!cmd || !nv || !in || !out || !data.st) + return; + + EnsureCameraPipelines(nv, *data.st, out->getDesc().format); + if (!data.st->cameraPipeline || !data.st->cameraLayout) + return; + + auto& cache = GetPassResourceCache(); + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psR = loader->GetCachedReflection("camera_model", ".ps"); + if (!vsR || !psR) + return; + + struct alignas(16) PCB + { + float sx, sy, distort, ca; + float vignette, grain, time, pad; + }; + auto* cb = cache.GetOrCreateVolatileCB("CameraModel", "CameraModelParams", sizeof(PCB), data.device); + PCB p{ + float(data.width), float(data.height), + CameraDistortAmount(), CameraCAAmount(), + CameraVignetteAmount(), CameraGrainAmount(), + Device.fTimeGlobal, 0.f + }; + if (cb) + cmd->writeBuffer(cb, &p, sizeof(p)); + + BindingSetBuilder bsb(*vsR, *psR, nv, "CameraModel"); + if (cb) + bsb.ConstantBuffer("CameraModelParams", cb); + bsb.Texture("g_Color", in); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->cameraLayout, nv); + if (!set) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(out); + auto fb = cache.GetOrCreateFramebuffer("CameraModel", fbDesc, nv); + if (!fb) + return; + + nvrhi::GraphicsState gs; + gs.pipeline = data.st->cameraPipeline; + gs.framebuffer = fb; + gs.bindings = { set }; + gs.viewport.addViewportAndScissorRect(nvrhi::Viewport(float(data.width), float(data.height))); + cmd->setGraphicsState(gs); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); + }); + + VirtualResourceHandle result = camPass.output; + if (!wantMblur) + { + if (outputTarget.is_valid() && result != outputTarget) + { + // already writing to outputTarget when !wantMblur && output valid + } + return result; + } + + VirtualResourceHandle mblurOut = outputTarget; + if (!mblurOut.is_valid()) + { + ResourceDesc desc; + desc.type = ResourceDesc::Type::Texture2D; + desc.width = width; + desc.height = height; + desc.format = outFmt; + desc.isRenderTarget = true; + desc.isTransient = true; + desc.debugName = "rt_MotionBlur"; + mblurOut = fg.CreateTexture("rt_MotionBlur", desc); + } + + struct BlurData + { + VirtualResourceHandle input, motion, output; + u32 width, height; + CameraModelPassState* st = nullptr; + fg::RenderDevice* device = nullptr; + }; + + auto& blurPass = fg.addCallbackPass( + "MotionBlur", + [&](FrameGraph& b, PassHandle ph, BlurData& data) { + RenderPassBuilder pb(b, ph); + data.st = &state; + data.device = device; + data.width = width; + data.height = height; + data.input = pb.read(result, ResourceState::ShaderResource); + data.motion = pb.read(motionVectors, ResourceState::ShaderResource); + data.output = pb.write(mblurOut, ResourceState::RenderTarget); + }, + [](const BlurData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd ? cmd->getDevice() : nullptr; + auto* in = graph.GetPhysicalTexture(data.input); + auto* motion = graph.GetPhysicalTexture(data.motion); + auto* out = graph.GetPhysicalTexture(data.output); + if (!cmd || !nv || !in || !motion || !out || !data.st) + return; + if (!data.st->mblurPipeline || !data.st->mblurLayout) + return; + + auto& cache = GetPassResourceCache(); + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psR = loader->GetCachedReflection("motion_blur", ".ps"); + if (!vsR || !psR) + return; + + struct alignas(16) PCB { float sx, sy, intensity, pad; }; + auto* cb = cache.GetOrCreateVolatileCB("MotionBlur", "MotionBlurParams", sizeof(PCB), data.device); + PCB p{ float(data.width), float(data.height), CameraMblurAmount(), 0.f }; + if (cb) + cmd->writeBuffer(cb, &p, sizeof(p)); + + BindingSetBuilder bsb(*vsR, *psR, nv, "MotionBlur"); + if (cb) + bsb.ConstantBuffer("MotionBlurParams", cb); + bsb.Texture("g_Color", in); + bsb.Texture("g_Motion", motion); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->mblurLayout, nv); + if (!set) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(out); + auto fb = cache.GetOrCreateFramebuffer("MotionBlur", fbDesc, nv); + if (!fb) + return; + + nvrhi::GraphicsState gs; + gs.pipeline = data.st->mblurPipeline; + gs.framebuffer = fb; + gs.bindings = { set }; + gs.viewport.addViewportAndScissorRect(nvrhi::Viewport(float(data.width), float(data.height))); + cmd->setGraphicsState(gs); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); + }); + + return blurPass.output; +} + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/CameraModelPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/CameraModelPassSetup.h new file mode 100644 index 00000000000..7a557144bb5 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/CameraModelPassSetup.h @@ -0,0 +1,33 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes +{ + +struct CameraModelPassState +{ + nvrhi::GraphicsPipelineHandle cameraPipeline; + nvrhi::BindingLayoutHandle cameraLayout; + nvrhi::GraphicsPipelineHandle mblurPipeline; + nvrhi::BindingLayoutHandle mblurLayout; + nvrhi::Format pipelineFormat = nvrhi::Format::UNKNOWN; + bool initialized = false; +}; + +framegraph::VirtualResourceHandle setupCameraModelPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle ldrInput, + framegraph::VirtualResourceHandle motionVectors, + framegraph::VirtualResourceHandle outputTarget, + u32 width, + u32 height, + CameraModelPassState& state); + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/ClusterLightPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ClusterLightPassSetup.cpp index 561641171d0..9ff05ccfdb3 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ClusterLightPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ClusterLightPassSetup.cpp @@ -51,7 +51,7 @@ static void InitAssignPipeline(nvrhi::IDevice* nvDevice, ClusterLightPassState& } state.assignLayout = GetPassResourceCache().GetOrCreateBindingLayoutFromReflection( - "ClusterLightAssign", *csRefl, nvDevice); + "ClusterLightAssign_v4_Group64", *csRefl, nvDevice); if (!state.assignLayout) return; @@ -166,6 +166,7 @@ void setupClusterLightPass( if (!data.passState->assignInitialized) return; + data.lightManager->RefreshHudSpotXForms(); data.lightManager->Upload(cmdList); auto& cache = framegraph::GetPassResourceCache(); @@ -267,7 +268,8 @@ void setupClusterLightPass( u32 tilesX = data.lightManager->GetTilesX(); u32 tilesY = data.lightManager->GetTilesY(); - cmdList->dispatch(tilesX, tilesY, CLUSTER_NUM_SLICES); + const u32 clusterCount = tilesX * tilesY * ::xray::render::fg::CLUSTER_NUM_SLICES; + cmdList->dispatch(clusterCount, 1, 1); } ); } diff --git a/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.cpp index 3792f62de46..2d0aff4968a 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "DecalPassSetup.h" +#include "PassCommon.h" #include "ShaderConstants.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/IPass.h" @@ -57,33 +58,49 @@ static void InitializeDecalResources(fg::RenderDevice* device, const nvrhi::Fram state.inputLayout = nvDevice->createInputLayout(&posAttr, 1, state.vs); state.bindingLayout = cache.GetOrCreateBindingLayoutFromReflection( - "Decal", *vsResult.reflection, *psResult.reflection, nvDevice); - - nvrhi::GraphicsPipelineDesc pipeDesc; - pipeDesc.setVertexShader(state.vs); - pipeDesc.setPixelShader(state.ps); - pipeDesc.setInputLayout(state.inputLayout); - pipeDesc.addBindingLayout(state.bindingLayout); - - if (GEnv.Backend && GEnv.Backend->GetBindlessLayout()) - pipeDesc.addBindingLayout(GEnv.Backend->GetBindlessLayout()); - - pipeDesc.setPrimType(nvrhi::PrimitiveType::TriangleList); - pipeDesc.renderState.depthStencilState.setDepthTestEnable(false); - pipeDesc.renderState.depthStencilState.setDepthWriteEnable(false); - pipeDesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::Front); - - auto& blend = pipeDesc.renderState.blendState.targets[0]; - blend.setBlendEnable(true); - blend.setSrcBlend(nvrhi::BlendFactor::SrcAlpha); - blend.setDestBlend(nvrhi::BlendFactor::InvSrcAlpha); - blend.setBlendOp(nvrhi::BlendOp::Add); - blend.setSrcBlendAlpha(nvrhi::BlendFactor::One); - blend.setDestBlendAlpha(nvrhi::BlendFactor::InvSrcAlpha); - blend.setBlendOpAlpha(nvrhi::BlendOp::Add); - - state.pipeline = cache.GetOrCreatePipeline("Decal", pipeDesc, fbInfo, nvDevice); - state.initialized = state.pipeline != nullptr; + "Decal_v2", *vsResult.reflection, *psResult.reflection, nvDevice); + + auto makePipe = [&](const char* name, bool multiply) -> nvrhi::GraphicsPipelineHandle { + nvrhi::GraphicsPipelineDesc pipeDesc; + pipeDesc.setVertexShader(state.vs); + pipeDesc.setPixelShader(state.ps); + pipeDesc.setInputLayout(state.inputLayout); + pipeDesc.addBindingLayout(state.bindingLayout); + + if (GEnv.Backend && GEnv.Backend->GetBindlessLayout()) + pipeDesc.addBindingLayout(GEnv.Backend->GetBindlessLayout()); + + pipeDesc.setPrimType(nvrhi::PrimitiveType::TriangleList); + pipeDesc.renderState.depthStencilState.setDepthTestEnable(false); + pipeDesc.renderState.depthStencilState.setDepthWriteEnable(false); + pipeDesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::Front); + + auto& blend = pipeDesc.renderState.blendState.targets[0]; + blend.setBlendEnable(true); + if (multiply) + { + blend.setSrcBlend(nvrhi::BlendFactor::DstColor); + blend.setDestBlend(nvrhi::BlendFactor::SrcColor); + blend.setBlendOp(nvrhi::BlendOp::Add); + blend.setSrcBlendAlpha(nvrhi::BlendFactor::Zero); + blend.setDestBlendAlpha(nvrhi::BlendFactor::One); + blend.setBlendOpAlpha(nvrhi::BlendOp::Add); + } + else + { + blend.setSrcBlend(nvrhi::BlendFactor::SrcAlpha); + blend.setDestBlend(nvrhi::BlendFactor::InvSrcAlpha); + blend.setBlendOp(nvrhi::BlendOp::Add); + blend.setSrcBlendAlpha(nvrhi::BlendFactor::One); + blend.setDestBlendAlpha(nvrhi::BlendFactor::InvSrcAlpha); + blend.setBlendOpAlpha(nvrhi::BlendOp::Add); + } + return cache.GetOrCreatePipeline(name, pipeDesc, fbInfo, nvDevice); + }; + + state.multiplyPipeline = makePipe("DecalMult_v2", true); + state.alphaPipeline = makePipe("DecalAlpha_v2", false); + state.initialized = state.multiplyPipeline != nullptr && state.alphaPipeline != nullptr; } DefaultOutputLayout setupDecalPass( @@ -140,21 +157,15 @@ DefaultOutputLayout setupDecalPass( return; auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } auto* vsReflection = GEnv.Render->GetShaderLoader()->GetCachedReflection("decal_box", ".vs"); auto* psReflection = GEnv.Render->GetShaderLoader()->GetCachedReflection("decal_box", ".ps"); - auto* materialBuffer = MaterialBuffer::Instance().GetBuffer(); - - framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Decal"); - bsb.ConstantBuffer("static_globals", staticGlobalsCB); - bsb.BufferSRV("g_Decals", data.decalMgr->GetDecalBuffer()); - bsb.Texture("g_Depth", depthTex); - bsb.Texture("g_WorldPos", worldPosTex); - bsb.BufferSRV("g_Materials", materialBuffer); - - auto bindingSet = cache.GetOrCreateBindingSet( - bsb.Build(), data.passState->bindingLayout, nvDevice); + auto drawCB = cache.GetOrCreateVolatileCB("Frame", "DecalDrawCB", 16, data.device); nvrhi::Viewport viewport; viewport.minX = 0; @@ -164,26 +175,45 @@ DefaultOutputLayout setupDecalPass( viewport.minZ = 0.0f; viewport.maxZ = 1.0f; - nvrhi::GraphicsState gfxState; - gfxState.pipeline = data.passState->pipeline; - gfxState.framebuffer = framebuffer; - gfxState.viewport.addViewportAndScissorRect(viewport); - gfxState.addBindingSet(bindingSet); - auto* backend = data.device->GetBackend(); nvrhi::IDescriptorTable* bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; - if (bindlessTable) - gfxState.addBindingSet(bindlessTable); - - gfxState.vertexBuffers = { { data.decalMgr->GetCubeVB(), 0, 0 } }; - gfxState.indexBuffer = { data.decalMgr->GetCubeIB(), nvrhi::Format::R16_UINT, 0 }; - - cmdList->setGraphicsState(gfxState); nvrhi::DrawArguments args; args.vertexCount = decals::CUBE_INDEX_COUNT; args.instanceCount = data.decalMgr->GetActiveCount(); - cmdList->drawIndexed(args); + + auto drawWith = [&](nvrhi::IGraphicsPipeline* pipeline, u32 wantMultiply) { + if (!pipeline) + return; + u32 cbData[4] = { wantMultiply, 0, 0, 0 }; + cmdList->writeBuffer(drawCB, cbData, sizeof(cbData)); + + framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Decal"); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.ConstantBuffer("DecalDrawCB", drawCB); + bsb.BufferSRV("g_Decals", data.decalMgr->GetDecalBuffer()); + bsb.Texture("g_Depth", depthTex); + bsb.Texture("g_WorldPos", worldPosTex); + BindBindlessMaterialTables(bsb); + + auto bindingSet = cache.GetOrCreateBindingSet( + bsb.Build(), data.passState->bindingLayout, nvDevice); + + nvrhi::GraphicsState gfxState; + gfxState.pipeline = pipeline; + gfxState.framebuffer = framebuffer; + gfxState.viewport.addViewportAndScissorRect(viewport); + gfxState.addBindingSet(bindingSet); + if (bindlessTable) + gfxState.addBindingSet(bindlessTable); + gfxState.vertexBuffers = { { data.decalMgr->GetCubeVB(), 0, 0 } }; + gfxState.indexBuffer = { data.decalMgr->GetCubeIB(), nvrhi::Format::R16_UINT, 0 }; + cmdList->setGraphicsState(gfxState); + cmdList->drawIndexed(args); + }; + + drawWith(data.passState->multiplyPipeline, 1); + drawWith(data.passState->alphaPipeline, 0); } ); diff --git a/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.h index 9233c1ef315..87202aad052 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.h @@ -22,7 +22,8 @@ namespace xray::render::fg::decals { namespace xray::render::fg::passes { struct DecalPassState { - nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::GraphicsPipelineHandle alphaPipeline; + nvrhi::GraphicsPipelineHandle multiplyPipeline; nvrhi::BindingLayoutHandle bindingLayout; nvrhi::InputLayoutHandle inputLayout; nvrhi::ShaderHandle vs; diff --git a/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.cpp index eb3a35f0f8c..54ef454e62d 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.cpp @@ -12,7 +12,9 @@ #include "Layers/xrRender/Geometry/MaterialCache.h" #include "Layers/xrRender/Bindless/MaterialBuffer.h" #include "Layers/xrRender/Bindless/TerrainMaterialBuffer.h" +#include "Layers/xrRender/GPUCullingManager.h" #include "Layers/xrRender/xrRender_console.h" +#include "xrCDB/Frustum.h" #include "xrEngine/IRenderBackend.h" namespace xray::render::fg::passes @@ -41,12 +43,12 @@ static void InitializeDepthPrepass(fg::RenderDevice* device, DepthPrepassState& if (!nvDevice || !shaderLoader) return; - auto vsResult = shaderLoader->LoadVertexShader("bindless_depth", "main"); + auto vsResult = shaderLoader->LoadVertexShader("bindless_forward", "main"); auto psResult = shaderLoader->LoadPixelShader("bindless_depth", "main"); auto opaquePsResult = shaderLoader->LoadPixelShader("bindless_depth_opaque", "main"); if (!vsResult.handle || !psResult.handle) { - Msg("! [DepthPrepass] Failed to load bindless_depth shaders"); + Msg("! [DepthPrepass] Failed to load bindless_forward/bindless_depth shaders"); return; } state.vs = vsResult.handle; @@ -56,7 +58,7 @@ static void InitializeDepthPrepass(fg::RenderDevice* device, DepthPrepassState& auto& cache = GetPassResourceCache(); state.layout = cache.GetOrCreateBindingLayoutFromReflection( - "DepthPrepass_v1", *vsResult.reflection, *psResult.reflection, nvDevice); + "DepthPrepass_v2_FwdVS", *vsResult.reflection, *psResult.reflection, nvDevice); if (!state.layout) return; @@ -85,7 +87,7 @@ static void InitializeDepthPrepass(fg::RenderDevice* device, DepthPrepassState& else pipeDesc.bindingLayouts = {state.layout}; - state.pipeline = cache.GetOrCreatePipeline("DepthPrepass_v1", pipeDesc, fbInfo, nvDevice); + state.pipeline = cache.GetOrCreatePipeline("DepthPrepass_v2_FwdVS", pipeDesc, fbInfo, nvDevice); if (!state.pipeline) { Msg("! [DepthPrepass] Pipeline create failed"); @@ -96,7 +98,7 @@ static void InitializeDepthPrepass(fg::RenderDevice* device, DepthPrepassState& if (state.terrainPs && opaquePsResult.reflection) { state.terrainLayout = cache.GetOrCreateBindingLayoutFromReflection( - "DepthPrepass_Terrain_v1", *vsResult.reflection, *opaquePsResult.reflection, nvDevice); + "DepthPrepass_Terrain_v2_FwdVS", *vsResult.reflection, *opaquePsResult.reflection, nvDevice); if (state.terrainLayout) { nvrhi::GraphicsPipelineDesc terrainDesc = pipeDesc; @@ -106,12 +108,59 @@ static void InitializeDepthPrepass(fg::RenderDevice* device, DepthPrepassState& else terrainDesc.bindingLayouts = {state.terrainLayout}; state.terrainPipeline = cache.GetOrCreatePipeline( - "DepthPrepass_Terrain_v1", terrainDesc, fbInfo, nvDevice); + "DepthPrepass_Terrain_v2_FwdVS", terrainDesc, fbInfo, nvDevice); } } +#if !defined(XR_PLATFORM_APPLE) + { + auto tessVs = shaderLoader->LoadVertexShader("bindless_forward", "main"); + auto tessHs = shaderLoader->LoadHullShader("bindless_tess", "main"); + auto tessDs = shaderLoader->LoadDomainShader("bindless_tess", "main"); + auto tessPs = shaderLoader->LoadPixelShader("bindless_depth_tess", "main"); + if (tessVs.handle && tessHs.handle && tessDs.handle && tessPs.handle && + tessVs.reflection && tessPs.reflection) + { + state.tessVs = tessVs.handle; + state.tessHs = tessHs.handle; + state.tessDs = tessDs.handle; + state.tessPs = tessPs.handle; + state.tessLayout = cache.GetOrCreateBindingLayoutFromReflection( + "DepthPrepass_Tess_v1", *tessVs.reflection, *tessPs.reflection, nvDevice); + if (state.tessLayout) + { + state.tessInputLayout = nvDevice->createInputLayout(attrs, attrCount, state.tessVs); + nvrhi::GraphicsPipelineDesc tessDesc = pipeDesc; + tessDesc.VS = state.tessVs; + tessDesc.HS = state.tessHs; + tessDesc.DS = state.tessDs; + tessDesc.PS = state.tessPs; + tessDesc.inputLayout = state.tessInputLayout; + tessDesc.primType = nvrhi::PrimitiveType::PatchList; + tessDesc.patchControlPoints = 3; + tessDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + if (bindlessLayout) + tessDesc.bindingLayouts = {state.tessLayout, bindlessLayout}; + else + tessDesc.bindingLayouts = {state.tessLayout}; + state.tessPipeline = cache.GetOrCreatePipeline( + "DepthPrepass_Tess_v1", tessDesc, fbInfo, nvDevice); + } + tessVs.reflection = nullptr; + tessPs.reflection = nullptr; + } + if (state.tessPipeline) + Msg("* [DepthPrepass] Tessellation pipeline ready"); + else + Msg("! [DepthPrepass] Tessellation pipeline unavailable"); + } +#else + Msg("* [DepthPrepass] Tessellation disabled on Apple/MoltenVK (IOGPU safety)"); +#endif + state.initialized = true; - Msg("* [DepthPrepass] Pipeline ready (terrain=%d)", state.terrainPipeline ? 1 : 0); + Msg("* [DepthPrepass] Pipeline ready (terrain=%d tess=%d)", + state.terrainPipeline ? 1 : 0, state.tessPipeline ? 1 : 0); } VirtualResourceHandle setupDepthPrepass( @@ -123,17 +172,20 @@ VirtualResourceHandle setupDepthPrepass( u32 width, u32 height, const BindlessForwardConfig& bindlessConfig, - DepthPrepassState* state) + DepthPrepassState* state, + VirtualResourceHandle cullDrawArgs) { if (!ps_r_depth_prepass || !state || !depthInput.is_valid()) return depthInput; auto& passData = fg.addCallbackPass( "DepthPrepass", - [&, width, height, depthInput, bindlessConfig, state]( + [&, width, height, depthInput, bindlessConfig, state, cullDrawArgs]( FrameGraph& builder, PassHandle passHandle, DepthPrepassData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.depth = passBuilder.write(depthInput, ResourceState::DepthStencilWrite); + if (cullDrawArgs.is_valid()) + passBuilder.read(cullDrawArgs, ResourceState::IndirectArgument); data.device = device; data.geometry = geometry; data.materialCache = materialCache; @@ -192,7 +244,7 @@ VirtualResourceHandle setupDepthPrepass( return; auto* shaderLoader = GEnv.Render->GetShaderLoader(); - auto* vsRefl = shaderLoader->GetCachedReflection("bindless_depth", ".vs"); + auto* vsRefl = shaderLoader->GetCachedReflection("bindless_forward", ".vs"); auto* psRefl = shaderLoader->GetCachedReflection("bindless_depth", ".ps"); if (!vsRefl || !psRefl) return; @@ -212,7 +264,7 @@ VirtualResourceHandle setupDepthPrepass( BindingSetBuilder bsb(*vsRefl, *psRefl, nvDevice, "DepthPrepass"); bsb.ConstantBuffer("static_globals", staticGlobalsCB); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + BindBindlessMaterialTables(bsb); bsb.BufferSRV("g_InstanceData", set.instanceBuffer); bsb.BufferSRV("g_CompactBatchIndices", set.compactBatchIndicesBuffer); bsb.BufferSRV("g_CompactMaterialIDs", set.compactMaterialIDBuffer); @@ -242,6 +294,71 @@ VirtualResourceHandle setupDepthPrepass( drawSet(data.bindlessConfig.staticSet); drawSet(data.bindlessConfig.dynamicSet); +#if !defined(XR_PLATFORM_APPLE) + const bool tessEnabled = ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION); +#else + const bool tessEnabled = false; +#endif + if (tessEnabled && data.passState->tessPipeline && data.passState->tessLayout && + data.bindlessConfig.tessObjectCount > 0 && data.bindlessConfig.tessDrawArgs && + data.bindlessConfig.tessObjects && data.bindlessConfig.tessMaterialIDBuffer && + data.bindlessConfig.tessBatchIndicesBuffer && data.bindlessConfig.tessInstanceBuffer) + { + auto* tessVsRefl = shaderLoader->GetCachedReflection("bindless_forward", ".vs"); + auto* tessPsRefl = shaderLoader->GetCachedReflection("bindless_depth_tess", ".ps"); + if (tessVsRefl && tessPsRefl) + { + BindingSetBuilder tessBsb(*tessVsRefl, *tessPsRefl, nvDevice, "DepthPrepass.Tess"); + tessBsb.ConstantBuffer("static_globals", staticGlobalsCB); + BindBindlessMaterialTables(tessBsb); + tessBsb.BufferSRV("g_InstanceData", data.bindlessConfig.tessInstanceBuffer); + tessBsb.BufferSRV("g_CompactBatchIndices", data.bindlessConfig.tessBatchIndicesBuffer); + tessBsb.BufferSRV("g_CompactMaterialIDs", data.bindlessConfig.tessMaterialIDBuffer); + auto tessBindingSet = cache.GetOrCreateBindingSet( + tessBsb.Build(), data.passState->tessLayout, nvDevice); + if (tessBindingSet) + { + nvrhi::GraphicsState gs; + gs.pipeline = data.passState->tessPipeline; + gs.framebuffer = framebuffer; + gs.bindings = {tessBindingSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {data.bindlessConfig.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = { + data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + gs.viewport.addViewport(viewport); + gs.viewport.addScissorRect(nvrhi::Rect(data.width, data.height)); + cmdList->setGraphicsState(gs); + + CFrustum tessFrustum; + tessFrustum.CreateFromMatrix( + Device.mFullTransform, FRUSTUM_P_LRTB | FRUSTUM_P_FAR); + + for (u32 i = 0; i < data.bindlessConfig.tessObjectCount; ++i) + { + const auto& args = data.bindlessConfig.tessDrawArgs[i]; + if (args.indexCountPerInstance == 0) + continue; + + const auto& obj = data.bindlessConfig.tessObjects[i]; + if (!tessFrustum.testSphere_dirty(obj.position, obj.radius * 1.15f)) + continue; + + nvrhi::DrawArguments da; + da.vertexCount = args.indexCountPerInstance; + da.instanceCount = 1; + da.startIndexLocation = args.startIndexLocation; + da.startVertexLocation = args.baseVertexLocation; + da.startInstanceLocation = i; + cmdList->drawIndexed(da); + } + } + } + } + // Terrain fills most outdoor pixels — critical for early-Z if (data.passState->terrainPipeline && data.passState->terrainLayout && data.bindlessConfig.HasTerrain() && data.bindlessConfig.UseTerrainCompaction()) @@ -259,6 +376,7 @@ VirtualResourceHandle setupDepthPrepass( { BindingSetBuilder tbsb(*vsRefl, *opaquePsRefl, nvDevice, "DepthPrepass.Terrain"); tbsb.ConstantBuffer("static_globals", staticGlobalsCB); + BindBindlessMaterialTables(tbsb); tbsb.BufferSRV("g_InstanceData", data.bindlessConfig.terrainInstanceBuffer); tbsb.BufferSRV("g_CompactBatchIndices", data.bindlessConfig.terrainCompactBatchIndicesBuffer); tbsb.BufferSRV("g_CompactMaterialIDs", data.bindlessConfig.terrainCompactMaterialIDBuffer); diff --git a/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.h b/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.h index 2b97d325a31..3119d7eb59e 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.h @@ -22,12 +22,19 @@ namespace xray::render::fg::passes { struct DepthPrepassState { nvrhi::GraphicsPipelineHandle pipeline; nvrhi::GraphicsPipelineHandle terrainPipeline; + nvrhi::GraphicsPipelineHandle tessPipeline; nvrhi::BindingLayoutHandle layout; nvrhi::BindingLayoutHandle terrainLayout; + nvrhi::BindingLayoutHandle tessLayout; nvrhi::InputLayoutHandle inputLayout; + nvrhi::InputLayoutHandle tessInputLayout; nvrhi::ShaderHandle vs; nvrhi::ShaderHandle ps; nvrhi::ShaderHandle terrainPs; + nvrhi::ShaderHandle tessVs; + nvrhi::ShaderHandle tessHs; + nvrhi::ShaderHandle tessDs; + nvrhi::ShaderHandle tessPs; bool initialized = false; }; @@ -41,6 +48,7 @@ framegraph::VirtualResourceHandle setupDepthPrepass( u32 width, u32 height, const BindlessForwardConfig& bindlessConfig, - DepthPrepassState* state); + DepthPrepassState* state, + framegraph::VirtualResourceHandle cullDrawArgs = {}); } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/DetailCullPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DetailCullPassSetup.cpp index 261c0d21be3..4f23f9534d1 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DetailCullPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DetailCullPassSetup.cpp @@ -6,12 +6,25 @@ #include "Layers/xrRender/RenderContext/RenderDevice.h" #include "Layers/xrRender/RenderContext/RenderContext.h" #include "Layers/xrRender/Profiler/GPUProfiler.h" +#include "xrEngine/Environment.h" +#include "xrEngine/IGame_Persistent.h" + +#include extern ENGINE_API float ps_r3_grass_blade_width; +namespace xray::render::fg +{ +// Defined in xrRender_console.cpp (same namespace) +extern int ps_r__detail_radius; +extern float dm_current_fade; +} + namespace xray::render::fg::passes { using namespace framegraph; +using xray::render::fg::ps_r__detail_radius; +using xray::render::fg::dm_current_fade; struct DetailCullPassData { VirtualResourceHandle hiZPyramid; @@ -98,7 +111,20 @@ void setupDetailCullPass( } nvrhi::ITexture* hiZTexture = fg.GetPhysicalTexture(data.hiZPyramid); - const float fadeDistance = g_pGamePersistent->Environment().CurrentEnv.far_plane; + // World fade from r__detail_radius (UI slider). Recompute each frame so + // options that write the cvar without CCC still apply. Was far_plane → no-op. + const u32 size = u32(iFloor(float(ps_r__detail_radius) / 4.f) * 2); + float fadeDistance = float(2 * size) - 0.5f; + if (fadeDistance < 1.f) + fadeDistance = float(ps_r__detail_radius); + if (g_pGamePersistent) + { + const float farPlane = g_pGamePersistent->Environment().CurrentEnv.far_plane; + if (farPlane > 1.f) + fadeDistance = std::min(fadeDistance, farPlane); + } + // Keep classic cache vars in sync for any other readers + dm_current_fade = fadeDistance; Fmatrix effectivePrevViewProj = data.hasPrevViewProj ? data.prevViewProj : Device.mFullTransform; data.detailManager->DispatchCulling( diff --git a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp index 5ce0ebfd743..d041e25f01c 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp @@ -2,6 +2,7 @@ #include "stdafx.h" #include "PassCommon.h" #include "DetailPassSetup.h" +#include "IBLPrefilterPassSetup.h" #include "ShaderConstants.h" #include "Layers/xrRender/FGDetailManager.h" #include "Layers/xrRender/FrameGraph/IPass.h" @@ -13,7 +14,11 @@ #include "Layers/xrRender/FrameGraph/PassResourceCache.h" #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" #include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/xrRender_console.h" #include "Layers/xrRender/ClusteredLightManager.h" +#include "Layers/xrRender/Bindless/MaterialBuffer.h" +#include "Layers/xrRender/Bindless/TerrainMaterialBuffer.h" +#include "Layers/xrRender/Bindless/VariantTextureBuffer.h" // Detail rendering console variables extern ENGINE_API float ps_r__Detail_l_aniso; @@ -30,8 +35,8 @@ extern ENGINE_API Fvector3 ps_r3_grass_color_tip; extern ENGINE_API Fvector3 ps_r3_grass_color_base; extern ENGINE_API float ps_r3_grass_color_variation; extern ENGINE_API Fvector3 ps_r3_grass_sss_color; -extern ENGINE_API float ps_r3_grass_sss_intensity; extern ENGINE_API Fvector3 ps_r3_grass_object_tints[64]; +// Grass SSS intensity uses shared r_foliage_sss / r_foliage_sss_intensity (ShaderConstants.h) // Grass blade geometry parameters (defined in xrEngine) extern ENGINE_API float ps_r3_grass_blade_width; @@ -61,10 +66,28 @@ DefaultOutputLayout setupDetailPass( , VirtualResourceHandle perlinReady , nvrhi::ITexture* const* shadowCascades , nvrhi::ITexture* localShadowAtlas + , nvrhi::ITexture* localShadowESM ) { - if (detailManager && !detailManager->graphicsPipeline) + if (detailManager) { + static u32 s_detailPipeEpoch = 0; + constexpr u32 kDetailPipeEpoch = 4; + if (s_detailPipeEpoch != kDetailPipeEpoch) + { + detailManager->graphicsPipeline = nullptr; + detailManager->decalGraphicsPipeline = nullptr; + detailManager->billboardGraphicsPipeline = nullptr; + detailManager->graphicsBindingLayout = nullptr; + detailManager->decalBindingLayout = nullptr; + detailManager->billboardBindingLayout = nullptr; + s_detailPipeEpoch = kDetailPipeEpoch; + } + } + if (detailManager && (!detailManager->graphicsPipeline || !detailManager->decalGraphicsPipeline)) + { + if (detailManager->graphicsPipeline && !detailManager->decalGraphicsPipeline) + detailManager->graphicsPipeline = nullptr; // Shaders must be loaded before PSO creation (else "invalid parameters") if (!detailManager->vertexShader || !detailManager->pixelShader) { @@ -85,7 +108,7 @@ DefaultOutputLayout setupDetailPass( auto& passData = fg.addCallbackPass( "DetailDraw", - [&, width, height, gpuProfiler, shadowMapArray, shadowMapHandle, contactDepth, contactHistory, perlinReady, shadowCascades, localShadowAtlas]( + [&, width, height, gpuProfiler, shadowMapArray, shadowMapHandle, contactDepth, contactHistory, perlinReady, shadowCascades, localShadowAtlas, localShadowESM]( FrameGraph& builder, PassHandle passHandle, DetailPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); @@ -98,6 +121,7 @@ DefaultOutputLayout setupDetailPass( for (int i = 0; i < 3; ++i) data.shadowCascades[i] = (shadowCascades && shadowCascades[i]) ? shadowCascades[i] : shadowMapArray; data.localShadowAtlas = localShadowAtlas; + data.localShadowESM = localShadowESM; data.contactDepth = contactDepth; data.contactHistory = contactHistory; @@ -214,7 +238,11 @@ DefaultOutputLayout setupDetailPass( frameConstants.perlin4d_texture_index = dm->perlin4dBindlessIndex; frameConstants.grass_color_tip.set(ps_r3_grass_color_tip.x, ps_r3_grass_color_tip.y, ps_r3_grass_color_tip.z, 0.0f); frameConstants.grass_color_base.set(ps_r3_grass_color_base.x, ps_r3_grass_color_base.y, ps_r3_grass_color_base.z, 0.0f); - frameConstants.grass_sss_color.set(ps_r3_grass_sss_color.x, ps_r3_grass_sss_color.y, ps_r3_grass_sss_color.z, ps_r3_grass_sss_intensity); + // Same enable/intensity as trees/leaves (r_foliage_sss*, parallax.z path) + const float grassSss = + (ps_r_foliage_sss != 0) ? ps_r_foliage_sss_intensity : 0.0f; + frameConstants.grass_sss_color.set( + ps_r3_grass_sss_color.x, ps_r3_grass_sss_color.y, ps_r3_grass_sss_color.z, grassSss); frameConstants.grass_color_variation = ps_r3_grass_color_variation; frameConstants.grass_blade_height = ps_r3_grass_blade_height; frameConstants.buildDetailsIndex = dm->buildDetailsBindlessIndex; @@ -269,36 +297,55 @@ DefaultOutputLayout setupDetailPass( : cache.GetDummyShadowMap(nvDev); if (localAtlas) bsb.Texture("g_LocalShadowAtlas", localAtlas); + nvrhi::ITexture* localEsm = data.localShadowESM + ? data.localShadowESM + : cache.GetDummyLocalShadowESM(nvDev); + if (localEsm) + bsb.Texture("g_LocalShadowESM", localEsm); + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + nvrhi::ITexture* dummyHzb = cache.GetDummyContactDepth(nvDev); + for (u32 i = 0; i < 3; ++i) + { + if (dummyHzb) + bsb.Texture(kHzb[i], dummyHzb); + } }; auto* shaderLoader = GEnv.Render->GetShaderLoader(); auto* grassVsRefl = shaderLoader->GetCachedReflection("detail_gpu", ".vs"); auto* grassPsRefl = shaderLoader->GetCachedReflection("detail_gpu", ".ps"); + // detail_*.ps includes bindless_common.h → reflection keeps g_Materials / + // g_TerrainMaterials / g_VariantTextures (t8/t9/t10) even when unused. + auto bindBindlessMaterialTables = [&](framegraph::BindingSetBuilder& bsb) { + BindBindlessMaterialTables(bsb); + }; + auto makeGrassBindingSet = [&](nvrhi::BufferHandle visibleIndicesBuffer) { framegraph::BindingSetBuilder bsb(*grassVsRefl, *grassPsRefl, nvDev, "Detail.Grass"); bsb.ConstantBuffer("static_globals", staticGlobalsCB); bsb.ConstantBuffer("DetailGlobals", detailGlobalsCB); bsb.ConstantBuffer("$Globals", dynLightCB); + bindBindlessMaterialTables(bsb); bsb.BufferSRV("visible_indices", visibleIndicesBuffer); bsb.BufferSRV("grass_object_tints", dm->cachedGrassTintsBuffer); bsb.BufferSRV("all_instances", dm->generatedInstancesBuffer); bsb.BufferSRV("slot_data", dm->slotDataBuffer); + bsb.BufferSRV("slot_indirection", dm->cachedDummySlotIndirection); bsb.Texture("g_Perlin4D", dm->perlin4dTexture); bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); + if (ClusteredLightManager::Instance().GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", ClusteredLightManager::Instance().GetShadowDataBuffer()); bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); bindShadowMap(bsb); - nvrhi::ITexture* sky0 = nullptr; - nvrhi::ITexture* sky1 = nullptr; - ResolveEnvSkyCubes(data.device, sky0, sky1); - if (!sky0) sky0 = cache.GetDummyCubeMap(nvDev); - if (!sky1) sky1 = cache.GetDummyCubeMap(nvDev); + const auto& ibl = GetCurrentIBLBindResources(); + nvrhi::ITexture* sky0 = ibl.spec0 ? ibl.spec0 : cache.GetDummyCubeMap(nvDev); + nvrhi::ITexture* sky1 = ibl.spec1 ? ibl.spec1 : sky0; if (sky0) bsb.Texture("s_env0", sky0); if (sky1) bsb.Texture("s_env1", sky1); - auto bindDesc = bsb.Build(); - bindDesc.bindings.push_back(nvrhi::BindingSetItem::TypedBuffer_SRV(32, dm->cachedDummySlotIndirection)); - return cache.GetOrCreateBindingSet(bindDesc, dm->graphicsBindingLayout, nvDev); + BindIBLResources(bsb, ibl, nvDev); + return cache.GetOrCreateBindingSet(bsb.Build(), dm->graphicsBindingLayout, nvDev); }; auto* bbVsRefl = shaderLoader->GetCachedReflection("detail_billboard", ".vs"); @@ -308,6 +355,7 @@ DefaultOutputLayout setupDetailPass( framegraph::BindingSetBuilder bsb(*bbVsRefl, *bbPsRefl, nvDev, "Detail.Billboard"); bsb.ConstantBuffer("static_globals", staticGlobalsCB); bsb.ConstantBuffer("DetailGlobals", detailGlobalsCB); + bindBindlessMaterialTables(bsb); bsb.BufferSRV("visible_indices", visibleIndicesBuffer); bsb.BufferSRV("detail_models", dm->detailModelsBuffer); bsb.BufferSRV("pulled_vertices", dm->pulledVertexBuffer); @@ -315,16 +363,17 @@ DefaultOutputLayout setupDetailPass( bsb.BufferSRV("slot_data", dm->slotDataBuffer); bsb.Texture("g_Perlin4D", dm->perlin4dTexture); bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); + if (ClusteredLightManager::Instance().GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", ClusteredLightManager::Instance().GetShadowDataBuffer()); bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); bindShadowMap(bsb); - nvrhi::ITexture* sky0 = nullptr; - nvrhi::ITexture* sky1 = nullptr; - ResolveEnvSkyCubes(data.device, sky0, sky1); - if (!sky0) sky0 = cache.GetDummyCubeMap(nvDev); - if (!sky1) sky1 = cache.GetDummyCubeMap(nvDev); + const auto& ibl = GetCurrentIBLBindResources(); + nvrhi::ITexture* sky0 = ibl.spec0 ? ibl.spec0 : cache.GetDummyCubeMap(nvDev); + nvrhi::ITexture* sky1 = ibl.spec1 ? ibl.spec1 : sky0; if (sky0) bsb.Texture("s_env0", sky0); if (sky1) bsb.Texture("s_env1", sky1); + BindIBLResources(bsb, ibl, nvDev); return cache.GetOrCreateBindingSet(bsb.Build(), layout, nvDev); }; @@ -334,25 +383,46 @@ DefaultOutputLayout setupDetailPass( dm->billboardDrawArgsBuffer && dm->pulledIndexBuffer && dm->maxPulledIndexCount > 0) { nvrhi::BindingSetHandle bbBindingSet = makePulledBindingSet(dm->visibleBillboardInstancesBuffer, dm->billboardBindingLayout); + if (!bbBindingSet) + { + static bool s_logged = false; + if (!s_logged) + { + Msg("! [DetailPass] Billboard binding set failed (missing t8/t9/t10 or CSM)"); + s_logged = true; + } + } + else + { + nvrhi::GraphicsState state; + state.framebuffer = framebuffer; + state.viewport.addViewportAndScissorRect(nvrhi::Viewport((float)data.width, (float)data.height)); + state.pipeline = dm->billboardGraphicsPipeline; + state.bindings = { bbBindingSet }; + if (bindlessTable) + state.addBindingSet(bindlessTable); + state.indexBuffer = { dm->pulledIndexBuffer, nvrhi::Format::R16_UINT, 0 }; + state.indirectParams = dm->billboardDrawArgsBuffer; - nvrhi::GraphicsState state; - state.framebuffer = framebuffer; - state.viewport.addViewportAndScissorRect(nvrhi::Viewport((float)data.width, (float)data.height)); - state.pipeline = dm->billboardGraphicsPipeline; - state.bindings = { bbBindingSet }; - if (bindlessTable) - state.addBindingSet(bindlessTable); - state.indexBuffer = { dm->pulledIndexBuffer, nvrhi::Format::R16_UINT, 0 }; - state.indirectParams = dm->billboardDrawArgsBuffer; - - cmdList->setGraphicsState(state); - cmdList->drawIndexedIndirect(0); + cmdList->setGraphicsState(state); + cmdList->drawIndexedIndirect(0); + } } - else + else if (!billboardMode) { for (u32 lod = 0; lod < FGDetailManager::LOD_COUNT; lod++) { nvrhi::BindingSetHandle bindingSet = makeGrassBindingSet(dm->visibleInstancesBuffer[lod]); + if (!bindingSet) + { + static bool s_logged = false; + if (!s_logged) + { + Msg("! [DetailPass] Grass binding set failed (layout/SRV mismatch — check t50/t51)"); + s_logged = true; + } + continue; + } nvrhi::GraphicsState state; state.framebuffer = framebuffer; @@ -377,36 +447,41 @@ DefaultOutputLayout setupDetailPass( framegraph::BindingSetBuilder decalBsb(*decalVsRefl, *decalPsRefl, nvDev, "Detail.Decal"); decalBsb.ConstantBuffer("static_globals", staticGlobalsCB); decalBsb.ConstantBuffer("DetailGlobals", detailGlobalsCB); + bindBindlessMaterialTables(decalBsb); decalBsb.BufferSRV("visible_indices", dm->visibleDecalInstancesBuffer); decalBsb.BufferSRV("detail_models", dm->detailModelsBuffer); decalBsb.BufferSRV("decal_vertices", dm->pulledVertexBuffer); decalBsb.BufferSRV("all_instances", dm->generatedInstancesBuffer); decalBsb.BufferSRV("slot_data", dm->slotDataBuffer); decalBsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); + if (ClusteredLightManager::Instance().GetShadowDataBuffer()) + decalBsb.BufferSRV("g_ShadowData", ClusteredLightManager::Instance().GetShadowDataBuffer()); decalBsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); decalBsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); bindShadowMap(decalBsb); - nvrhi::ITexture* sky0 = nullptr; - nvrhi::ITexture* sky1 = nullptr; - ResolveEnvSkyCubes(data.device, sky0, sky1); - if (!sky0) sky0 = cache.GetDummyCubeMap(nvDev); - if (!sky1) sky1 = cache.GetDummyCubeMap(nvDev); + const auto& ibl = GetCurrentIBLBindResources(); + nvrhi::ITexture* sky0 = ibl.spec0 ? ibl.spec0 : cache.GetDummyCubeMap(nvDev); + nvrhi::ITexture* sky1 = ibl.spec1 ? ibl.spec1 : sky0; if (sky0) decalBsb.Texture("s_env0", sky0); if (sky1) decalBsb.Texture("s_env1", sky1); + BindIBLResources(decalBsb, ibl, nvDev); nvrhi::BindingSetHandle decalBindingSet = cache.GetOrCreateBindingSet(decalBsb.Build(), dm->decalBindingLayout, nvDev); - nvrhi::GraphicsState state; - state.framebuffer = framebuffer; - state.viewport.addViewportAndScissorRect(nvrhi::Viewport((float)data.width, (float)data.height)); - state.pipeline = dm->decalGraphicsPipeline; - state.bindings = { decalBindingSet }; - if (bindlessTable) - state.addBindingSet(bindlessTable); - state.indexBuffer = { dm->pulledIndexBuffer, nvrhi::Format::R16_UINT, 0 }; - state.indirectParams = dm->decalDrawArgsBuffer; - - cmdList->setGraphicsState(state); - cmdList->drawIndexedIndirect(0); + if (decalBindingSet) + { + nvrhi::GraphicsState state; + state.framebuffer = framebuffer; + state.viewport.addViewportAndScissorRect(nvrhi::Viewport((float)data.width, (float)data.height)); + state.pipeline = dm->decalGraphicsPipeline; + state.bindings = { decalBindingSet }; + if (bindlessTable) + state.addBindingSet(bindlessTable); + state.indexBuffer = { dm->pulledIndexBuffer, nvrhi::Format::R16_UINT, 0 }; + state.indirectParams = dm->decalDrawArgsBuffer; + + cmdList->setGraphicsState(state); + cmdList->drawIndexedIndirect(0); + } } if (data.gpuProfiler) diff --git a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.h index ab96b9984ee..2947a4789ed 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.h @@ -45,6 +45,7 @@ struct DetailPassData { nvrhi::ITexture* shadowMapArray = nullptr; nvrhi::ITexture* shadowCascades[3] = {}; nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* localShadowESM = nullptr; nvrhi::ITexture* contactDepth = nullptr; nvrhi::ITexture* contactHistory = nullptr; fg::RenderDevice* device; @@ -54,13 +55,7 @@ struct DetailPassData { u32 height; xray::profiler::GPUProfiler* gpuProfiler; }; -// Lambda-based detail pass setup with GPU culling support -// Renders detail objects (grass, vegetation) using: -// - GPU compute culling (frustum + Hi-Z occlusion) -// - Single unified draw call via DrawIndexedInstancedIndirect -// - Interactive grass system (wind + entity interactions) -// - Virtual texturing for interaction atlas -// Renders AFTER forward color pass (details on top of world geometry) + framegraph::DefaultOutputLayout setupDetailPass( framegraph::FrameGraph& fg, fg::RenderDevice* device, @@ -75,7 +70,8 @@ framegraph::DefaultOutputLayout setupDetailPass( nvrhi::ITexture* contactHistory = nullptr, framegraph::VirtualResourceHandle perlinReady = {}, nvrhi::ITexture* const* shadowCascades = nullptr, - nvrhi::ITexture* localShadowAtlas = nullptr + nvrhi::ITexture* localShadowAtlas = nullptr, + nvrhi::ITexture* localShadowESM = nullptr ); } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.cpp index 57630914903..3050b7accdf 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.cpp @@ -21,6 +21,7 @@ struct DistortionApplyData { VirtualResourceHandle sceneInput; VirtualResourceHandle distortionInput; VirtualResourceHandle worldPosInput; + VirtualResourceHandle baseColorInput; VirtualResourceHandle output; u32 width; u32 height; @@ -43,7 +44,7 @@ void InitializeDistortionApplyPass(nvrhi::IDevice* device, DistortionApplyPassSt auto& cache = GetPassResourceCache(); state.bindingLayout = cache.GetOrCreateBindingLayoutFromReflection( - "DistortionApply", *vsResult.reflection, *psResult.reflection, device); + "DistortionApply_v8_HudDilate", *vsResult.reflection, *psResult.reflection, device); if (state.bindingLayout) { nvrhi::GraphicsPipelineDesc pipeDesc; @@ -59,7 +60,7 @@ void InitializeDistortionApplyPass(nvrhi::IDevice* device, DistortionApplyPassSt nvrhi::FramebufferInfoEx fbInfo; fbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); - state.pipeline = cache.GetOrCreatePipeline("DistortionApply", pipeDesc, fbInfo, device); + state.pipeline = cache.GetOrCreatePipeline("DistortionApply_v8_HudDilate", pipeDesc, fbInfo, device); } state.initialized = true; } @@ -70,6 +71,7 @@ VirtualResourceHandle setupDistortionApplyPass( VirtualResourceHandle sceneColor, VirtualResourceHandle distortionRT, VirtualResourceHandle worldPos, + VirtualResourceHandle baseColor, u32 width, u32 height, DistortionApplyPassState& passState) @@ -77,6 +79,9 @@ VirtualResourceHandle setupDistortionApplyPass( if (device && device->GetNVRHIDevice()) InitializeDistortionApplyPass(device->GetNVRHIDevice(), passState); + if (!distortionRT.is_valid() || !sceneColor.is_valid()) + return sceneColor; + ResourceDesc outputDesc; outputDesc.type = ResourceDesc::Type::Texture2D; outputDesc.width = width; @@ -91,14 +96,19 @@ VirtualResourceHandle setupDistortionApplyPass( auto& passData = fg.addCallbackPass( "DistortionApply", - [sceneColor, distortionRT, worldPos, outputHandle, width, height, &passState](FrameGraph& builder, PassHandle passHandle, DistortionApplyData& data) { + [sceneColor, distortionRT, worldPos, baseColor, outputHandle, width, height, &passState](FrameGraph& builder, PassHandle passHandle, DistortionApplyData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.width = width; data.height = height; data.passState = &passState; data.sceneInput = passBuilder.read(sceneColor, ResourceState::ShaderResource); data.distortionInput = passBuilder.read(distortionRT, ResourceState::ShaderResource); - data.worldPosInput = passBuilder.read(worldPos, ResourceState::ShaderResource); + data.worldPosInput = worldPos.is_valid() + ? passBuilder.read(worldPos, ResourceState::ShaderResource) + : VirtualResourceHandle{}; + data.baseColorInput = baseColor.is_valid() + ? passBuilder.read(baseColor, ResourceState::ShaderResource) + : VirtualResourceHandle{}; data.output = passBuilder.write(outputHandle, ResourceState::RenderTarget); }, @@ -106,9 +116,10 @@ VirtualResourceHandle setupDistortionApplyPass( nvrhi::ICommandList* cmdList = ctx->GetCommandList(); auto* sceneTex = fg.GetPhysicalTexture(data.sceneInput); auto* distortTex = fg.GetPhysicalTexture(data.distortionInput); - auto* worldPosTex = fg.GetPhysicalTexture(data.worldPosInput); + auto* worldPosTex = data.worldPosInput.is_valid() ? fg.GetPhysicalTexture(data.worldPosInput) : nullptr; + auto* baseColorTex = data.baseColorInput.is_valid() ? fg.GetPhysicalTexture(data.baseColorInput) : nullptr; auto* outputTex = fg.GetPhysicalTexture(data.output); - if (!sceneTex || !distortTex || !worldPosTex || !outputTex) + if (!sceneTex || !distortTex || !outputTex) return; auto* ps = data.passState; @@ -129,7 +140,8 @@ VirtualResourceHandle setupDistortionApplyPass( bsb.ConstantBuffer("static_globals", staticGlobalsCB) .Texture("g_Snapshot", sceneTex) .Texture("g_Distortion", distortTex) - .Texture("g_WorldPos", worldPosTex); + .Texture("g_WorldPos", worldPosTex ? worldPosTex : cache.GetDummyContactHistory(device)) + .Texture("g_BaseColor", baseColorTex ? baseColorTex : cache.GetDummyContactHistory(device)); auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->bindingLayout, device); nvrhi::FramebufferDesc fbDesc; diff --git a/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.h index 36d0a8f07e4..32d070a9313 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.h @@ -28,6 +28,7 @@ framegraph::VirtualResourceHandle setupDistortionApplyPass( framegraph::VirtualResourceHandle sceneColor, framegraph::VirtualResourceHandle distortionRT, framegraph::VirtualResourceHandle worldPos, + framegraph::VirtualResourceHandle baseColor, u32 width, u32 height, DistortionApplyPassState& state diff --git a/src/Layers/xrRender/FrameGraphPasses/ExposurePassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ExposurePassSetup.cpp index 9c7198f9a5c..991ff6c2f6a 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ExposurePassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ExposurePassSetup.cpp @@ -11,6 +11,7 @@ #include "Layers/xrRender/RenderContext/RenderDevice.h" #include "Layers/xrRender/FrameGraph/ShaderLoader.h" #include "Layers/xrRender/xrRender_console.h" +#include "xrEngine/Environment.h" namespace fg { @@ -21,6 +22,27 @@ namespace xray::render::fg::passes { using namespace framegraph; +static float CopLuma(float r, float g, float b) +{ + return r * 0.3f + g * 0.38f + b * 0.22f; +} + +static float ComputeEnvironmentExposureEV() +{ + if (!g_pGamePersistent) + return 0.f; + + const auto& env = g_pGamePersistent->Environment().CurrentEnv; + + const float sunL = CopLuma(env.sun_color.x, env.sun_color.y, env.sun_color.z) * ps_r2_sun_lumscale; + const float hemiL = CopLuma(env.hemi_color.x, env.hemi_color.y, env.hemi_color.z) * ps_r2_sun_lumscale_hemi; + const float ambL = CopLuma(env.ambient.x, env.ambient.y, env.ambient.z) * ps_r2_sun_lumscale_amb; + const float L_env = std::max(sunL + hemiL + ambL, 1e-4f); + const float L_ref = std::max(ps_r_exposure_env_ref, 1e-4f); + const float envEV = std::log2(L_env / L_ref); + return std::clamp(envEV, -4.f, 2.f); +} + // Classic binder (phase_luminance): // amount = R2FLAG_TONEMAP ? ps_r2_tonemap_amount : 0 // _none=(1,0,1), _full=(middlegray,1,lowlum) @@ -133,13 +155,13 @@ void InitializeExposureResources(fg::RenderDevice* device, ExposurePassState& st } { - state.adaptLayout = cache.GetOrCreateBindingLayoutFromReflection("ExposurePass_Adapt", *adaptResult.reflection, nvDevice); + state.adaptLayout = cache.GetOrCreateBindingLayoutFromReflection("ExposurePass_Adapt_v2", *adaptResult.reflection, nvDevice); if (state.adaptLayout) { nvrhi::ComputePipelineDesc pipeDesc; pipeDesc.CS = adaptResult.handle; pipeDesc.bindingLayouts = { state.adaptLayout }; - state.adaptPipeline = cache.GetOrCreateComputePipeline("ExposurePass_Adapt", pipeDesc, nvDevice); + state.adaptPipeline = cache.GetOrCreateComputePipeline("ExposurePass_Adapt_v2", pipeDesc, nvDevice); } } @@ -155,13 +177,8 @@ void InitializeExposureResources(fg::RenderDevice* device, ExposurePassState& st Msg("* [ExposurePass] Initialized (compute=%s)", state.computeEnabled ? "enabled" : "fallback"); } -// Classic CPU-side MiddleGray packing + adaptation blend update -static void FillClassicMiddleGray(const ExposureConfig& config, float deltaTime, ExposurePassState& state, AdaptCB& out) +static void FillAdaptCB(const ExposureConfig& config, float deltaTime, AdaptCB& out) { - // f_luminance_adapt = .9 * prev + .1 * dt * adaptation - state.adaptBlend = 0.9f * state.adaptBlend + 0.1f * deltaTime * config.adaptation; - state.adaptBlend = std::clamp(state.adaptBlend, 0.0f, 1.0f); - Fvector4 none(1.f, 0.f, 1.f, 0.f); Fvector4 full(config.middleGray, 1.f, config.lowLum, 0.f); Fvector4 result; @@ -170,18 +187,30 @@ static void FillClassicMiddleGray(const ExposureConfig& config, float deltaTime, out.middleGrayX = result.x; out.middleGrayY = result.y; out.middleGrayZ = result.z; - out.middleGrayW = state.adaptBlend; + out.pad0 = 0.f; + out.lowPercentile = 0.50f; + out.highPercentile = 0.90f; + out.adaptUp = std::max(ps_r_exposure_adapt_up, 0.01f); + out.adaptDown = std::max(ps_r_exposure_adapt_down, 0.01f); + const float envStrength = std::clamp(ps_r_exposure_env_strength, 0.f, 1.f); + out.evBias = ps_r_exposure_ev_bias + ComputeEnvironmentExposureEV() * envStrength; + out.deltaTime = std::max(deltaTime, 1e-4f); + out.pad1 = 0.f; + out.pad2 = 0.f; + (void)config.adaptation; } static float ComputeFallbackExposure(const ExposureConfig& config, float deltaTime, ExposurePassState& state) { - // No scene: hold at 1 (classic scale with amount=0) AdaptCB cb{}; - FillClassicMiddleGray(config, deltaTime, state, cb); + FillAdaptCB(config, deltaTime, cb); const float Lw = 1.0f; float scale = cb.middleGrayX / std::max(Lw * cb.middleGrayY + cb.middleGrayZ, 1e-6f); + scale *= std::exp2(cb.evBias); scale = std::clamp(scale, 1.f / 128.f, 20.f); - state.currentExposure = std::lerp(state.currentExposure, scale, cb.middleGrayW); + const float rate = (scale > state.currentExposure) ? cb.adaptUp : cb.adaptDown; + const float alpha = 1.f - std::exp(-rate * cb.deltaTime); + state.currentExposure = std::lerp(state.currentExposure, scale, std::clamp(alpha, 0.f, 1.f)); return state.currentExposure; } @@ -303,7 +332,7 @@ ExposureOutput setupExposurePass( { AdaptCB adaptCB{}; - FillClassicMiddleGray(data.config, data.deltaTime, *ps, adaptCB); + FillAdaptCB(data.config, data.deltaTime, adaptCB); cmdList->writeBuffer(adaptCBHandle, &adaptCB, sizeof(adaptCB)); diff --git a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp index 8ef03a484a6..ed2fb88b08e 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp @@ -1,6 +1,7 @@ // xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp #include "stdafx.h" #include "ForwardColorPassSetup.h" +#include "IBLPrefilterPassSetup.h" #include "ShaderConstants.h" // CB layout definitions and FillGlobalConstants/FillDynamicTransforms #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/IPass.h" @@ -62,7 +63,7 @@ void InitializeForwardResources(fg::RenderDevice* device, const nvrhi::Framebuff auto& cache = framegraph::GetPassResourceCache(); - state.bindlessLayout = cache.GetOrCreateBindingLayoutFromReflection("ForwardColor_PBR_v3_CSMLadder", *vsResult.reflection, *psResult.reflection, nvDevice); + state.bindlessLayout = cache.GetOrCreateBindingLayoutFromReflection("ForwardColor_PBR_v4_TranspCB", *vsResult.reflection, *psResult.reflection, nvDevice); u32 attrCount = 0; auto* attrs = GetUnifiedVertexAttributes(attrCount); @@ -95,7 +96,7 @@ void InitializeForwardResources(fg::RenderDevice* device, const nvrhi::Framebuff pipeDesc.renderState.rasterState.frontCounterClockwise = false; pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; - state.bindlessPipeline = cache.GetOrCreatePipeline("ForwardColor_PBR_v3_CSMLadder", pipeDesc, fbInfo, nvDevice); + state.bindlessPipeline = cache.GetOrCreatePipeline("ForwardColor_PBR_v4_TranspCB", pipeDesc, fbInfo, nvDevice); if (!state.bindlessPipeline) { Msg("! [BindlessForward] Failed to create pipeline"); return; @@ -237,6 +238,12 @@ static void renderBindlessForward( cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); } + auto transparentDrawCB = cache.GetOrCreateVolatileCB("Frame", "TransparentDrawCB", 16, device); + { + u32 passMode[4] = { 0, 0, 0, 0 }; + cmdList->writeBuffer(transparentDrawCB, passMode, sizeof(passMode)); + } + auto lightingData = FillLightingConstants(); cmdList->writeBuffer(lightingCB, &lightingData, sizeof(lightingData)); @@ -282,6 +289,27 @@ static void renderBindlessForward( : passCache.GetDummyShadowMap(nvDevice); if (localAtlas) bsb.Texture("g_LocalShadowAtlas", localAtlas); + nvrhi::ITexture* localEsm = config.localShadowESM + ? config.localShadowESM + : passCache.GetDummyLocalShadowESM(nvDevice); + if (localEsm) + bsb.Texture("g_LocalShadowESM", localEsm); + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + nvrhi::ITexture* dummyHzb = passCache.GetDummyContactDepth(nvDevice); + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* hz = config.shadowHZB[i] ? config.shadowHZB[i] : dummyHzb; + if (hz) + bsb.Texture(kHzb[i], hz); + } + }; + + auto bindShadowMask = [&](framegraph::BindingSetBuilder& bsb) { + nvrhi::ITexture* shadowMask = config.shadowMask + ? config.shadowMask + : passCache.GetDummyContactHistory(nvDevice); + if (shadowMask) + bsb.Texture("g_ShadowMask", shadowMask); }; auto bindSkyCubes = [&](framegraph::BindingSetBuilder& bsb) { @@ -289,19 +317,29 @@ static void renderBindlessForward( bsb.Texture("s_env0", sky0); if (sky1) bsb.Texture("s_env1", sky1); + IBLBindResources ibl{}; + ibl.brdfLut = config.envBrdfLut; + ibl.shBuffer = config.envSkySH; + ibl.probeBuffer = config.envProbes; + ibl.probeCubeArray = config.envProbeCubes; + BindIBLResources(bsb, ibl, nvDevice); }; auto createBindingSetForSet = [&](const BindlessDrawSet& set) -> nvrhi::BindingSetHandle { framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "ForwardColor"); bsb.ConstantBuffer("static_globals", staticGlobalsCB); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + bsb.ConstantBuffer("TransparentDrawCB", transparentDrawCB); + BindBindlessMaterialTables(bsb); bsb.BufferSRV("g_InstanceData", set.instanceBuffer); bsb.BufferSRV("g_CompactBatchIndices", set.compactBatchIndicesBuffer); bsb.BufferSRV("g_CompactMaterialIDs", set.compactMaterialIDBuffer); bsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); + if (clm.GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", clm.GetShadowDataBuffer()); bsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); bindShadowMap(bsb); + bindShadowMask(bsb); bindSkyCubes(bsb); auto bs = passCache.GetOrCreateBindingSet(bsb.Build(), ps.bindlessLayout, nvDevice); @@ -333,10 +371,9 @@ static void renderBindlessForward( return; } - // Equal-depth after prepass was rejected: VS path mismatch (depth vs forward) - // fails ComparisonFunc::Equal on MoltenVK → black indoor geometry / no torch. - // Keep LessOrEqual; DepthPrepass still fills Hi-Z for overdraw rejection. nvrhi::IGraphicsPipeline* opaquePipe = ps.bindlessPipeline.Get(); + if (config.useEqualDepth && ps.bindlessEqualPipeline) + opaquePipe = ps.bindlessEqualPipeline.Get(); nvrhi::GraphicsState state; state.pipeline = opaquePipe; @@ -403,13 +440,20 @@ static void renderBindlessForward( vpCfg.megaVertexBuffer = config.megaVertexBuffer; vpCfg.shadowMapArray = shadowTex; for (u32 i = 0; i < 3; ++i) + { vpCfg.shadowCascades[i] = config.shadowCascades[i]; + vpCfg.shadowHZB[i] = config.shadowHZB[i]; + } vpCfg.localShadowAtlas = config.localShadowAtlas; + vpCfg.localShadowESM = config.localShadowESM; + vpCfg.contactHistory = config.contactHistory; + vpCfg.shadowMask = config.shadowMask; vpCfg.envSky0 = sky0; vpCfg.envSky1 = sky1; { auto& clmVp = ClusteredLightManager::Instance(); vpCfg.lightDataBuffer = clmVp.GetLightDataBuffer(); + vpCfg.shadowDataBuffer = clmVp.GetShadowDataBuffer(); vpCfg.clusterGridBuffer = clmVp.GetClusterGridBuffer(); vpCfg.lightIndexListBuffer = clmVp.GetLightIndexListBuffer(); } @@ -418,7 +462,7 @@ static void renderBindlessForward( DrawVariantPartition(cmdList, nvDevice, framebuffer, state, vpCfg); - state.pipeline = ps.bindlessPipeline; + state.pipeline = opaquePipe; state.vertexBuffers = { {config.megaVertexBuffer, 0, 0}, {drawIndexBuffer, 1, 0} @@ -428,67 +472,78 @@ static void renderBindlessForward( } drawSet(config.dynamicSet); - // Tessellation: PatchList PSO + direct drawIndexed (CPU frustum). - // Avoid DrawIndexedIndirect+tess — MoltenVK freezes on some camera angles. - // Apple/MoltenVK: hard-disabled — Metal tess temp buffers can kernel-panic - // (IOGPUGroupMemory::remove_memory_object) regardless of the console flag. + // Tessellation: PatchList PSO + direct drawIndexed (CPU frustum). + // Avoid DrawIndexedIndirect+tess — MoltenVK/Vulkan freezes on some camera angles. + // Apple/MoltenVK: hard-disabled — Metal tess temp buffers can kernel-panic + // (IOGPUGroupMemory::remove_memory_object) regardless of the console flag. #if defined(XR_PLATFORM_APPLE) - const bool tessEnabled = false; + const bool tessEnabled = false; #else - const bool tessEnabled = ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION); + const bool tessEnabled = ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION); #endif - if (tessEnabled && ps.bindlessTessPipeline && - config.tessObjectCount > 0 && config.tessDrawArgs && config.tessObjects && - config.tessMaterialIDBuffer && config.tessBatchIndicesBuffer && config.tessInstanceBuffer) - { - framegraph::BindingSetBuilder tessBsb(*vsReflection, *psReflection, nvDevice, "ForwardColor.Tess"); - tessBsb.ConstantBuffer("static_globals", staticGlobalsCB); - tessBsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); - tessBsb.BufferSRV("g_InstanceData", config.tessInstanceBuffer); - tessBsb.BufferSRV("g_CompactBatchIndices", config.tessBatchIndicesBuffer); - tessBsb.BufferSRV("g_CompactMaterialIDs", config.tessMaterialIDBuffer); - tessBsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); - tessBsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); - tessBsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); - bindShadowMap(tessBsb); - bindSkyCubes(tessBsb); - - auto tessBindingSet = passCache.GetOrCreateBindingSet(tessBsb.Build(), ps.bindlessLayout, nvDevice); - if (tessBindingSet) - { - state.pipeline = ps.bindlessTessPipeline; - state.bindings = { tessBindingSet }; - if (bindlessTable) - state.addBindingSet(bindlessTable); - state.indirectParams = nullptr; - state.indirectCountBuffer = nullptr; - cmdList->setGraphicsState(state); - - // No CPU frustum here: bounds for some model batches are unreliable, - // and exclusive tess routing would hide the whole mesh if culled. - for (u32 i = 0; i < config.tessObjectCount; ++i) + if (tessEnabled && ps.bindlessTessPipeline && + config.tessObjectCount > 0 && config.tessDrawArgs && config.tessObjects && + config.tessMaterialIDBuffer && config.tessBatchIndicesBuffer && config.tessInstanceBuffer) { - const auto& args = config.tessDrawArgs[i]; - if (args.indexCountPerInstance == 0) - continue; - - nvrhi::DrawArguments da; - da.vertexCount = args.indexCountPerInstance; - da.instanceCount = 1; - da.startIndexLocation = args.startIndexLocation; - da.startVertexLocation = args.baseVertexLocation; - da.startInstanceLocation = i; - cmdList->drawIndexed(da); + framegraph::BindingSetBuilder tessBsb(*vsReflection, *psReflection, nvDevice, "ForwardColor.Tess"); + tessBsb.ConstantBuffer("static_globals", staticGlobalsCB); + tessBsb.ConstantBuffer("TransparentDrawCB", transparentDrawCB); + BindBindlessMaterialTables(tessBsb); + tessBsb.BufferSRV("g_InstanceData", config.tessInstanceBuffer); + tessBsb.BufferSRV("g_CompactBatchIndices", config.tessBatchIndicesBuffer); + tessBsb.BufferSRV("g_CompactMaterialIDs", config.tessMaterialIDBuffer); + tessBsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); + if (clm.GetShadowDataBuffer()) + tessBsb.BufferSRV("g_ShadowData", clm.GetShadowDataBuffer()); + tessBsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); + tessBsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); + bindShadowMap(tessBsb); + bindShadowMask(tessBsb); + bindSkyCubes(tessBsb); + + auto tessBindingSet = passCache.GetOrCreateBindingSet(tessBsb.Build(), ps.bindlessLayout, nvDevice); + if (tessBindingSet) + { + state.pipeline = ps.bindlessTessPipeline; + state.bindings = { tessBindingSet }; + if (bindlessTable) + state.addBindingSet(bindlessTable); + state.indirectParams = nullptr; + state.indirectCountBuffer = nullptr; + cmdList->setGraphicsState(state); + + // Soft frustum cull: PatchList amplification is expensive and has + // caused view-angle freezes; skip batches clearly outside the view. + CFrustum tessFrustum; + tessFrustum.CreateFromMatrix(Device.mFullTransform, FRUSTUM_P_LRTB | FRUSTUM_P_FAR); + + for (u32 i = 0; i < config.tessObjectCount; ++i) + { + const auto& args = config.tessDrawArgs[i]; + if (args.indexCountPerInstance == 0) + continue; + + const auto& obj = config.tessObjects[i]; + if (!tessFrustum.testSphere_dirty(obj.position, obj.radius * 1.15f)) + continue; + + nvrhi::DrawArguments da; + da.vertexCount = args.indexCountPerInstance; + da.instanceCount = 1; + da.startIndexLocation = args.startIndexLocation; + da.startVertexLocation = args.baseVertexLocation; + da.startInstanceLocation = i; + cmdList->drawIndexed(da); + } + + state.pipeline = opaquePipe; + } + else + { + Msg("! [ForwardColor] Tess binding set create failed — skip tess draws"); + } } - state.pipeline = opaquePipe; - } - else - { - Msg("! [ForwardColor] Tess binding set create failed — skip tess draws"); - } - } - // ═══════════════════════════════════════════════════════ // TERRAIN RENDERING (4-layer detail blending) // ═══════════════════════════════════════════════════════ @@ -507,17 +562,19 @@ static void renderBindlessForward( config.terrainCompactDrawArgsBuffer && config.terrainCompactCountBuffer, "Terrain buffers not ready for compaction rendering"); - // Create terrain binding set (includes TerrainMaterialBuffer at t9) - // NOTE: Terrain uses its own instance/batch buffers, not the regular ones + // Terrain PS includes bindless_common.h → layout needs t8/t9/t10 even if + // only g_TerrainMaterials is sampled (same issue as DetailPass). auto* terrainVsRefl = shaderLoader->GetCachedReflection("bindless_forward", ".vs"); auto* terrainPsRefl = shaderLoader->GetCachedReflection("bindless_terrain", ".ps"); framegraph::BindingSetBuilder terrainBsb(*terrainVsRefl, *terrainPsRefl, nvDevice, "ForwardColor.Terrain"); terrainBsb.ConstantBuffer("static_globals", staticGlobalsCB); - terrainBsb.BufferSRV("g_TerrainMaterials", terrainMatBuffer.GetBuffer()); + BindBindlessMaterialTables(terrainBsb); terrainBsb.BufferSRV("g_InstanceData", config.terrainInstanceBuffer); terrainBsb.BufferSRV("g_CompactBatchIndices", config.terrainCompactBatchIndicesBuffer); terrainBsb.BufferSRV("g_CompactMaterialIDs", config.terrainCompactMaterialIDBuffer); terrainBsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); + if (clm.GetShadowDataBuffer()) + terrainBsb.BufferSRV("g_ShadowData", clm.GetShadowDataBuffer()); terrainBsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); terrainBsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); bindShadowMap(terrainBsb); @@ -530,8 +587,12 @@ static void renderBindlessForward( } else { + nvrhi::IGraphicsPipeline* terrainPipe = ps.terrainPipeline.Get(); + if (config.useEqualDepth && ps.terrainEqualPipeline) + terrainPipe = ps.terrainEqualPipeline.Get(); + nvrhi::GraphicsState terrainState; - terrainState.pipeline = ps.terrainPipeline.Get(); + terrainState.pipeline = terrainPipe; terrainState.framebuffer = framebuffer; terrainState.bindings = { terrainBindingSet }; @@ -624,6 +685,13 @@ framegraph::DefaultOutputLayout setupForwardColorPass( if (bindlessConfig.localShadowHandle.is_valid()) { passBuilder.read(bindlessConfig.localShadowHandle, ResourceState::ShaderResource); } + for (u32 i = 0; i < 3; ++i) { + if (bindlessConfig.shadowHZBHandles[i].is_valid()) + passBuilder.read(bindlessConfig.shadowHZBHandles[i], ResourceState::ShaderResource); + } + if (bindlessConfig.shadowMaskHandle.is_valid()) { + passBuilder.read(bindlessConfig.shadowMaskHandle, ResourceState::ShaderResource); + } data.outputs.albedo = data.color; data.outputs.normal = data.normal; diff --git a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h index 1de5355f9a5..b0f90dc9379 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h @@ -107,6 +107,9 @@ struct BindlessForwardConfig { // Enable bindless rendering mode bool enabled = false; + // After depth prepass: Equal + depth write off (desktop Vulkan only) + bool useEqualDepth = false; + // ═══════════════════════════════════════════════════════ // MEGA-BUFFER SYSTEM (GPU-Driven Rendering) // ═══════════════════════════════════════════════════════ @@ -160,6 +163,7 @@ struct BindlessForwardConfig { // Local spot/OMNIPART shadow atlas (t27); null = dummy array nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* localShadowESM = nullptr; framegraph::VirtualResourceHandle localShadowHandle; // Previous-frame depth for screen-space contact shadows; null = disabled @@ -167,10 +171,22 @@ struct BindlessForwardConfig { // Previous-frame temporal contact factor (t28); null = dummy white nvrhi::ITexture* contactHistory = nullptr; - // Sky cubemaps for water reflections (t25/t26) + // Per-cascade Shadow-HZB (t31/t32/t33); null = dummy + nvrhi::ITexture* shadowHZB[3] = {}; + framegraph::VirtualResourceHandle shadowHZBHandles[3]; + // Half-res screen shadow mask (t34); null = dummy white + nvrhi::ITexture* shadowMask = nullptr; + framegraph::VirtualResourceHandle shadowMaskHandle; + + // Sky cubemaps for water reflections / IBL (t25/t26) nvrhi::ITexture* envSky0 = nullptr; nvrhi::ITexture* envSky1 = nullptr; + nvrhi::ITexture* envBrdfLut = nullptr; + nvrhi::IBuffer* envSkySH = nullptr; + nvrhi::IBuffer* envProbes = nullptr; + nvrhi::ITexture* envProbeCubes = nullptr; + VariantPartitionConfig variantPartition; }; diff --git a/src/Layers/xrRender/FrameGraphPasses/GlowPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/GlowPassSetup.cpp new file mode 100644 index 00000000000..d484eeeb714 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/GlowPassSetup.cpp @@ -0,0 +1,293 @@ +#include "stdafx.h" +#include "GlowPassSetup.h" +#include "PassVertexFormats.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/FrameGraphPasses/ShaderConstants.h" +#include "Layers/xrRender/r_FrameGraphRenderer.h" + +namespace xray::render::fg::passes { +using namespace framegraph; + +namespace { +xr_vector g_glowRegistry; +GlowCollectFn g_glowCollect = nullptr; + +void EnsureGlowResources(nvrhi::IDevice* nv, GlowPassState& state) +{ + if (state.initialized) + return; + + auto* shaderLoader = RImplementation.GetShaderLoader(); + if (!shaderLoader) + return; + auto vsResult = shaderLoader->LoadVertexShader("sun_forward"); + auto psResult = shaderLoader->LoadPixelShader("sun_forward"); + if (!vsResult.handle || !psResult.handle) + return; + state.vs = vsResult.handle; + state.ps = psResult.handle; + + auto& cache = GetPassResourceCache(); + state.bindingLayout = cache.GetOrCreateBindingLayoutFromReflection( + "GlowBillboard", *vsResult.reflection, *psResult.reflection, nv); + if (!state.bindingLayout) + return; + + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc() + .setName("POSITION") + .setFormat(nvrhi::Format::RGB32_FLOAT) + .setOffset(offsetof(SunVertex, position)) + .setElementStride(sizeof(SunVertex)), + nvrhi::VertexAttributeDesc() + .setName("COLOR") + .setFormat(nvrhi::Format::RGBA8_UNORM) + .setOffset(offsetof(SunVertex, color)) + .setElementStride(sizeof(SunVertex)), + nvrhi::VertexAttributeDesc() + .setName("TEXCOORD") + .setFormat(nvrhi::Format::RG32_FLOAT) + .setOffset(offsetof(SunVertex, u)) + .setElementStride(sizeof(SunVertex)), + }; + state.inputLayout = cache.GetOrCreateInputLayout("GlowBillboard", attribs, std::size(attribs), state.vs, nv); + + nvrhi::RenderState rs; + rs.blendState.targets[0].setBlendEnable(true); + rs.blendState.targets[0].setSrcBlend(nvrhi::BlendFactor::One); + rs.blendState.targets[0].setDestBlend(nvrhi::BlendFactor::One); + rs.blendState.targets[0].setBlendOp(nvrhi::BlendOp::Add); + rs.depthStencilState.setDepthTestEnable(true); + rs.depthStencilState.setDepthWriteEnable(false); + rs.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + rs.rasterState.setCullMode(nvrhi::RasterCullMode::None); + + nvrhi::GraphicsPipelineDesc pso; + pso.inputLayout = state.inputLayout; + pso.VS = state.vs; + pso.PS = state.ps; + pso.bindingLayouts = { state.bindingLayout }; + pso.renderState = rs; + pso.primType = nvrhi::PrimitiveType::TriangleList; + + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.depthFormat = nvrhi::Format::D32; + state.pipeline = cache.GetOrCreatePipeline("GlowBillboard", pso, fbInfo, nv); + + u16 indices[6] = { 0, 1, 2, 2, 1, 3 }; + nvrhi::BufferDesc ibDesc; + ibDesc.byteSize = sizeof(indices); + ibDesc.isIndexBuffer = true; + ibDesc.initialState = nvrhi::ResourceStates::IndexBuffer; + ibDesc.keepInitialState = true; + ibDesc.debugName = "GlowBillboardIB"; + state.ib = nv->createBuffer(ibDesc); + { + auto cmd = nv->createCommandList(); + cmd->open(); + cmd->writeBuffer(state.ib, indices, sizeof(indices)); + cmd->close(); + nv->executeCommandList(cmd); + } + + nvrhi::TextureDesc td; + td.width = 1; + td.height = 1; + td.format = nvrhi::Format::RGBA8_UNORM; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "GlowPlaceholder"; + state.placeholderTex = nv->createTexture(td); + const u32 white = 0xFFFFFFFFu; + { + auto cmd = nv->createCommandList(); + cmd->open(); + cmd->writeTexture(state.placeholderTex, 0, 0, &white, 4); + cmd->close(); + nv->executeCommandList(cmd); + } + + state.initialized = state.pipeline && state.ib && state.placeholderTex; +} + +void EnsureVB(nvrhi::IDevice* nv, GlowPassState& state, u32 vertCount) +{ + if (state.vb && state.vbCapacityVerts >= vertCount) + return; + nvrhi::BufferDesc vd; + vd.byteSize = sizeof(SunVertex) * _max(vertCount, 64u); + vd.isVertexBuffer = true; + vd.initialState = nvrhi::ResourceStates::VertexBuffer; + vd.keepInitialState = true; + vd.debugName = "GlowBillboardVB"; + state.vb = nv->createBuffer(vd); + state.vbCapacityVerts = (u32)(vd.byteSize / sizeof(SunVertex)); +} +} + +void GlowRegistry_Register(void* glow) +{ + if (!glow) + return; + if (std::find(g_glowRegistry.begin(), g_glowRegistry.end(), glow) == g_glowRegistry.end()) + g_glowRegistry.push_back(glow); +} + +void GlowRegistry_Unregister(void* glow) +{ + g_glowRegistry.erase(std::remove(g_glowRegistry.begin(), g_glowRegistry.end(), glow), g_glowRegistry.end()); +} + +void GlowRegistry_SetCollect(GlowCollectFn fn) +{ + g_glowCollect = fn; +} + +void GlowRegistry_Collect(xr_vector& out) +{ + out.clear(); + if (!g_glowCollect) + return; + for (void* g : g_glowRegistry) + { + GlowBillboard b{}; + if (g_glowCollect(g, b) && b.radius > 0.f) + out.push_back(b); + } +} + +framegraph::VirtualResourceHandle setupGlowBillboardPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle sceneColor, + VirtualResourceHandle depth, + u32 width, + u32 height, + GlowPassState& state) +{ + xr_vector glows; + GlowRegistry_Collect(glows); + if (glows.empty() || !device) + return sceneColor; + + nvrhi::IDevice* nv = device->GetNVRHIDevice(); + EnsureGlowResources(nv, state); + if (!state.initialized) + return sceneColor; + + EnsureVB(nv, state, (u32)glows.size() * 4u); + + struct PassData + { + VirtualResourceHandle color; + VirtualResourceHandle depth; + GlowPassState* state = nullptr; + u32 width = 0, height = 0; + xr_vector glows; + }; + + auto& pass = fg.addCallbackPass( + "Glow Billboards", + [&](FrameGraph& builder, PassHandle passHandle, PassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.color = pb.readWrite(sceneColor, ResourceState::RenderTarget); + if (depth.is_valid()) + data.depth = pb.read(depth, ResourceState::DepthStencilRead); + pb.sideEffects(); + data.state = &state; + data.width = width; + data.height = height; + data.glows = glows; + }, + [](const PassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + if (!data.state || !ctx || data.glows.empty()) + return; + auto* colorTex = fgGraph.GetPhysicalTexture(data.color); + auto* depthTex = data.depth.is_valid() ? fgGraph.GetPhysicalTexture(data.depth) : nullptr; + if (!colorTex) + return; + + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd->getDevice(); + auto* renderDevice = ctx->GetDevice(); + if (!nv || !renderDevice) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(colorTex); + if (depthTex) + fbDesc.setDepthAttachment(depthTex); + auto fb = nv->createFramebuffer(fbDesc); + if (!fb) + return; + + auto& cache = GetPassResourceCache(); + auto* vsRefl = RImplementation.GetShaderLoader()->GetCachedReflection("sun_forward", ".vs"); + auto* psRefl = RImplementation.GetShaderLoader()->GetCachedReflection("sun_forward", ".ps"); + if (!vsRefl || !psRefl) + return; + + auto dynamicCBBuffer = cache.GetOrCreateVolatileCB( + "Frame", "DynamicTransforms", sizeof(DynamicTransforms), renderDevice); + + Fvector right = Device.vCameraRight; + Fvector up = Device.vCameraTop; + + for (const auto& g : data.glows) + { + Fvector sx, sy; + sx.mul(right, g.radius); + sy.mul(up, g.radius); + const u32 c = g.color.get(); + SunVertex verts[4]; + verts[0].position = { g.pos.x + sx.x - sy.x, g.pos.y + sx.y - sy.y, g.pos.z + sx.z - sy.z }; + verts[0].color = c; verts[0].u = 0.f; verts[0].v = 0.f; + verts[1].position = { g.pos.x + sx.x + sy.x, g.pos.y + sx.y + sy.y, g.pos.z + sx.z + sy.z }; + verts[1].color = c; verts[1].u = 0.f; verts[1].v = 1.f; + verts[2].position = { g.pos.x - sx.x - sy.x, g.pos.y - sx.y - sy.y, g.pos.z - sx.z - sy.z }; + verts[2].color = c; verts[2].u = 1.f; verts[2].v = 0.f; + verts[3].position = { g.pos.x - sx.x + sy.x, g.pos.y - sx.y + sy.y, g.pos.z - sx.z + sy.z }; + verts[3].color = c; verts[3].u = 1.f; verts[3].v = 1.f; + cmd->writeBuffer(data.state->vb, verts, sizeof(verts)); + + nvrhi::ITexture* tex = data.state->placeholderTex.Get(); + if (g.texture.size()) + { + auto* texManager = renderDevice->GetFGResourceManager() + ? renderDevice->GetFGResourceManager()->GetTextureManager() : nullptr; + if (texManager) + { + if (auto* t = texManager->GetNVRHITexture(texManager->LoadTexture(g.texture.c_str()))) + tex = t; + } + } + + BindingSetBuilder bsb(*vsRefl, *psRefl, nv, "GlowBillboard"); + bsb.ConstantBuffer("dynamic_transforms", dynamicCBBuffer) + .Texture("s_sun", tex); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->bindingLayout, nv); + + nvrhi::GraphicsState gs; + gs.pipeline = data.state->pipeline; + gs.framebuffer = fb; + gs.bindings.push_back(bindingSet); + gs.vertexBuffers = { { data.state->vb, 0, 0 } }; + gs.indexBuffer = { data.state->ib, nvrhi::Format::R16_UINT, 0 }; + gs.viewport = nvrhi::ViewportState().addViewportAndScissorRect( + nvrhi::Viewport((float)data.width, (float)data.height)); + cmd->setGraphicsState(gs); + cmd->drawIndexed(nvrhi::DrawArguments{ 6, 1, 0, 0, 0 }); + } + }); + + return pass.color; +} + +} diff --git a/src/Layers/xrRender/FrameGraphPasses/GlowPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/GlowPassSetup.h new file mode 100644 index 00000000000..13da378e417 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/GlowPassSetup.h @@ -0,0 +1,50 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include "Layers/xrRender/FrameGraphPasses/PassCommon.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes { + +struct GlowBillboard +{ + Fvector pos{}; + float radius = 1.f; + Fcolor color{ 1.f, 1.f, 1.f, 1.f }; + shared_str texture; +}; + +struct GlowPassState +{ + nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::BindingLayoutHandle bindingLayout; + nvrhi::InputLayoutHandle inputLayout; + nvrhi::ShaderHandle vs; + nvrhi::ShaderHandle ps; + nvrhi::BufferHandle vb; + nvrhi::BufferHandle ib; + nvrhi::TextureHandle placeholderTex; + u32 vbCapacityVerts = 0; + bool initialized = false; +}; + +void GlowRegistry_Register(void* glow); +void GlowRegistry_Unregister(void* glow); +using GlowCollectFn = bool (*)(void* glow, GlowBillboard& out); +void GlowRegistry_SetCollect(GlowCollectFn fn); +void GlowRegistry_Collect(xr_vector& out); + +framegraph::VirtualResourceHandle setupGlowBillboardPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + u32 width, + u32 height, + GlowPassState& state); + +} diff --git a/src/Layers/xrRender/FrameGraphPasses/HiZBuildPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/HiZBuildPassSetup.cpp index 812d1c48076..94a8faccb02 100644 --- a/src/Layers/xrRender/FrameGraphPasses/HiZBuildPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/HiZBuildPassSetup.cpp @@ -87,7 +87,9 @@ HiZPyramidOutput setupHiZBuildPass( VirtualResourceHandle depthInput, u32 width, u32 height, - HiZBuildPassState& hizState) + HiZBuildPassState& hizState, + const char* resourceName, + const char* passName) { InitializeHiZResources(device, width, height, hizState); @@ -110,18 +112,18 @@ HiZPyramidOutput setupHiZBuildPass( ResourceDesc hizDesc; hizDesc.type = ResourceDesc::Type::Texture2D; - hizDesc.debugName = "HiZPyramid"; + hizDesc.debugName = resourceName; hizDesc.width = hizWidth; hizDesc.height = hizHeight; hizDesc.format = nvrhi::Format::R32_FLOAT; hizDesc.mipLevels = hizMipLevels; hizDesc.isUAV = true; - hizDesc.isTransient = true; // Transient - will be aliased/reused + hizDesc.isTransient = false; - VirtualResourceHandle hizHandle = fg.CreateTexture("rt_HiZPyramid", hizDesc); + VirtualResourceHandle hizHandle = fg.CreateTexture(resourceName, hizDesc); auto& passData = fg.addCallbackPass( - "Hi-Z Build", + passName, [&, width, height, hizWidth, hizHeight, hizMipLevels](FrameGraph& builder, PassHandle passHandle, HiZBuildData& data) { RenderPassBuilder passBuilder(builder, passHandle); @@ -181,27 +183,23 @@ HiZPyramidOutput setupHiZBuildPass( // For mip 0, we read from full-res depth and downsample 2x2 → 1 // For mip N (N>0), we read from Hi-Z mip N-1 and downsample 2x2 → 1 - auto hizCB = framegraph::GetPassResourceCache().GetOrCreateVolatileCB("HiZBuild", "HiZCB", sizeof(HiZCB), data.device, 64); + auto hizCB = framegraph::GetPassResourceCache().GetOrCreateVolatileCB( + "HiZBuild", "HiZParams", sizeof(HiZCB), data.device, 1024); if (!hizCB) { Msg("! [HiZBuild] Constant buffer is NULL at execute time!"); return; } for (u32 mip = 0; mip < data.mipLevels; mip++) { - // Output dimensions for this mip level u32 outWidth = std::max(1u, data.width >> mip); u32 outHeight = std::max(1u, data.height >> mip); - // Skip if output is 0 (shouldn't happen) if (outWidth == 0 || outHeight == 0) break; - // Update constant buffer HiZCB cb; cb.outputWidth = outWidth; cb.outputHeight = outHeight; - // NOTE: When binding a specific mip subresource, the shader sees it as mip 0 - // So inputMipLevel should always be 0 for the bound texture view - cb.inputMipLevel = 0; // Always 0 - we bind specific mip as subresource + cb.inputMipLevel = 0; cb.isFirstMip = (mip == 0) ? 1 : 0; cmdList->writeBuffer(hizCB, &cb, sizeof(cb)); @@ -227,7 +225,8 @@ HiZPyramidOutput setupHiZBuildPass( bsb.TextureUAV("g_output_hiz", hizTexture, nvrhi::Format::R32_FLOAT, outputSubres); bsb.ConstantBuffer("HiZParams", hizCB); - nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), data.passState->layout, nvDevice); + nvrhi::BindingSetHandle bindingSet = GetPassResourceCache().GetOrCreateBindingSet( + bsb.Build(), data.passState->layout, nvDevice); if (!bindingSet) { Msg("! [HiZBuild] Failed to create binding set for mip %d", mip); diff --git a/src/Layers/xrRender/FrameGraphPasses/HiZBuildPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/HiZBuildPassSetup.h index 5f8d7effc12..b34a77e57e5 100644 --- a/src/Layers/xrRender/FrameGraphPasses/HiZBuildPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/HiZBuildPassSetup.h @@ -89,7 +89,9 @@ HiZPyramidOutput setupHiZBuildPass( framegraph::VirtualResourceHandle depthInput, u32 width, u32 height, - HiZBuildPassState& state + HiZBuildPassState& state, + const char* resourceName = "rt_HiZPyramid", + const char* passName = "Hi-Z Build" ); // Get number of mip levels for given dimensions diff --git a/src/Layers/xrRender/FrameGraphPasses/IBLPrefilterPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/IBLPrefilterPassSetup.cpp new file mode 100644 index 00000000000..dfb67638848 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/IBLPrefilterPassSetup.cpp @@ -0,0 +1,901 @@ +#include "stdafx.h" +#include "IBLPrefilterPassSetup.h" +#include "PassCommon.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "xrEngine/IGame_Persistent.h" +#include "xrEngine/IGame_Level.h" +#include "xrEngine/Environment.h" +#include "xrEngine/device.h" + +extern ENGINE_API int ps_r_sky_ibl; +extern ENGINE_API int ps_r_ibl_prefilter; +extern ENGINE_API int ps_r_ibl_probe_auto; +extern ENGINE_API float ps_r_ibl_probe_radius; +extern ENGINE_API int ps_r_ssgi; +extern ENGINE_API float ps_r_ssgi_intensity; + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +IBLBindResources g_CurrentIBL; +IBLPrefilterPassState* g_ActiveIBLState = nullptr; +} + +void SetCurrentIBLBindResources(const IBLBindResources& ibl) +{ + g_CurrentIBL = ibl; +} + +const IBLBindResources& GetCurrentIBLBindResources() +{ + return g_CurrentIBL; +} + +IBLPrefilterPassState* GetActiveIBLState() +{ + return g_ActiveIBLState; +} + +namespace +{ +u32 CountMips(u32 size) +{ + u32 mips = 1; + while (size > 1) + { + size >>= 1; + ++mips; + } + return mips; +} + +nvrhi::ComputePipelineHandle LoadCS( + const char* passName, const char* shaderName, nvrhi::IDevice* device, nvrhi::BindingLayoutHandle& outLayout) +{ + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + return nullptr; + auto cs = loader->LoadComputeShader(shaderName); + if (!cs.handle || !cs.reflection) + { + Msg("! [IBL] Failed to load %s", shaderName); + return nullptr; + } + auto& cache = GetPassResourceCache(); + outLayout = cache.GetOrCreateBindingLayoutFromReflection(passName, *cs.reflection, device); + if (!outLayout) + return nullptr; + nvrhi::ComputePipelineDesc desc; + desc.CS = cs.handle; + desc.bindingLayouts = {outLayout}; + return cache.GetOrCreateComputePipeline(passName, desc, device); +} + +struct PrefilterCB +{ + u32 size[2]; + u32 face; + u32 sampleCount; + float roughness; + float mipBias; + float pad[2]; +}; + +struct SHProjectCB +{ + u32 size; + u32 outOffset; + u32 pad[2]; +}; + +struct BRDFLUTCB +{ + u32 dims[2]; + u32 pad[2]; +}; + +struct ProbeCaptureCB +{ + Fmatrix vp; + Fmatrix invVP; + Fvector probePos; + float skyLerp; + Fvector eyePos; + float maxDistance; + u32 size[2]; + u32 face; + u32 pad; +}; +} // namespace + +void InitializeIBLPrefilter(fg::RenderDevice* device, IBLPrefilterPassState& state) +{ + nvrhi::IDevice* nv = device ? device->GetNVRHIDevice() : nullptr; + if (!nv) + return; + + if (!state.pipelinesReady) + { + state.prefilterPipe = LoadCS("IBLPrefilter", "ibl_prefilter_specular", nv, state.prefilterLayout); + state.shPipe = LoadCS("IBLSH", "ibl_sh_project", nv, state.shLayout); + state.brdfPipe = LoadCS("IBLBRDF", "ibl_brdf_lut", nv, state.brdfLayout); + state.probeCapturePipe = LoadCS("IBLProbeCap", "ibl_probe_capture", nv, state.probeCaptureLayout); + state.pipelinesReady = state.prefilterPipe && state.shPipe && state.brdfPipe; + if (!state.pipelinesReady) + Msg("! [IBL] Compute pipelines incomplete — falling back to raw sky IBL"); + } + + if (state.resourcesReady) + return; + + state.mipCount = CountMips(kIBLSpecSize); + + auto makeCube = [&](const char* name, u32 size, u32 mips) -> nvrhi::TextureHandle { + nvrhi::TextureDesc td; + td.width = size; + td.height = size; + td.depth = 1; + td.arraySize = 6; + td.mipLevels = mips; + td.format = nvrhi::Format::RGBA16_FLOAT; + td.dimension = nvrhi::TextureDimension::TextureCube; + td.isUAV = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = name; + return nv->createTexture(td); + }; + + state.spec0 = makeCube("IBL_Spec0", kIBLSpecSize, state.mipCount); + state.spec1 = makeCube("IBL_Spec1", kIBLSpecSize, state.mipCount); + + { + nvrhi::TextureDesc td; + td.width = kIBLBrdfSize; + td.height = kIBLBrdfSize; + td.format = nvrhi::Format::RG16_FLOAT; + td.dimension = nvrhi::TextureDimension::Texture2D; + td.isUAV = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "IBL_BRDFLUT"; + state.brdfLut = nv->createTexture(td); + } + + { + nvrhi::TextureDesc td; + td.width = kIBLProbeSize; + td.height = kIBLProbeSize; + td.arraySize = kIBLMaxProbes * 6; + td.mipLevels = CountMips(kIBLProbeSize); + td.format = nvrhi::Format::RGBA16_FLOAT; + td.dimension = nvrhi::TextureDimension::TextureCubeArray; + td.isUAV = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "IBL_ProbeCubeArray"; + state.probeCubeArray = nv->createTexture(td); + } + + { + nvrhi::TextureDesc td; + td.width = kIBLProbeSize; + td.height = kIBLProbeSize; + td.arraySize = 6; + td.mipLevels = 1; + td.format = nvrhi::Format::RGBA16_FLOAT; + td.dimension = nvrhi::TextureDimension::TextureCube; + td.isUAV = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "IBL_ProbeCaptureTemp"; + state.probeCaptureTemp = nv->createTexture(td); + } + + { + nvrhi::BufferDesc bd; + bd.byteSize = sizeof(Fvector4) * (kIBLSHCount * 2 + 1); + bd.structStride = sizeof(Fvector4); + bd.canHaveUAVs = true; + bd.initialState = nvrhi::ResourceStates::ShaderResource; + bd.keepInitialState = true; + bd.debugName = "IBL_SkySH"; + state.shBuffer = nv->createBuffer(bd); + } + + { + nvrhi::BufferDesc bd; + bd.byteSize = sizeof(Fvector4) * (1 + kIBLMaxProbes * kIBLProbeStride); + bd.structStride = sizeof(Fvector4); + bd.canHaveUAVs = false; + bd.initialState = nvrhi::ResourceStates::ShaderResource; + bd.keepInitialState = true; + bd.cpuAccess = nvrhi::CpuAccessMode::Write; + bd.debugName = "IBL_Probes"; + state.probeBuffer = nv->createBuffer(bd); + } + + auto makeCB = [&](const char* name, size_t bytes) { + nvrhi::BufferDesc bd; + bd.byteSize = bytes; + bd.isConstantBuffer = true; + bd.isVolatile = true; + bd.maxVersions = 4096; + bd.initialState = nvrhi::ResourceStates::ConstantBuffer; + bd.keepInitialState = true; + bd.debugName = name; + return nv->createBuffer(bd); + }; + state.prefilterCB = makeCB("IBL_PrefilterCB", sizeof(PrefilterCB)); + state.shCB = makeCB("IBL_SHCB", sizeof(SHProjectCB)); + state.brdfCB = makeCB("IBL_BRDFCB", sizeof(BRDFLUTCB)); + state.probeCaptureCB = makeCB("IBL_ProbeCapCB", sizeof(ProbeCaptureCB)); + + state.resourcesReady = state.spec0 && state.spec1 && state.brdfLut && state.shBuffer && state.probeBuffer; + state.needsPrefilter = true; + Msg("* [IBL] Resources ready spec=%u mips=%u probes=%u", kIBLSpecSize, state.mipCount, kIBLMaxProbes); +} + +static void DispatchPrefilterMip( + nvrhi::ICommandList* cmd, + nvrhi::IDevice* nv, + IBLPrefilterPassState& state, + nvrhi::ITexture* src, + nvrhi::ITexture* dst, + u32 mip) +{ + if (!cmd || !src || !dst || !state.prefilterPipe || mip >= state.mipCount) + return; + + auto* loader = GEnv.Render->GetShaderLoader(); + auto* refl = loader->GetCachedReflection("ibl_prefilter_specular", ".cs"); + if (!refl) + return; + auto& cache = GetPassResourceCache(); + + cmd->setTextureState(dst, nvrhi::TextureSubresourceSet(mip, 1, 0, 6), nvrhi::ResourceStates::UnorderedAccess); + cmd->setTextureState(src, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + + const u32 size = std::max(1u, kIBLSpecSize >> mip); + const float roughness = state.mipCount > 1 ? float(mip) / float(state.mipCount - 1) : 0.f; + const u32 samples = roughness < 1e-3f ? 1u : (mip < 3 ? 16u : 8u); + for (u32 face = 0; face < 6; ++face) + { + PrefilterCB cb{}; + cb.size[0] = size; + cb.size[1] = size; + cb.face = face; + cb.sampleCount = samples; + cb.roughness = roughness; + cb.mipBias = 0.f; + cmd->writeBuffer(state.prefilterCB, &cb, sizeof(cb)); + + BindingSetBuilder bsb(*refl, nv, "IBLPrefilter"); + bsb.Texture("t_Source", src) + .TextureUAV("u_Dest", dst, nvrhi::Format::UNKNOWN, + nvrhi::TextureSubresourceSet(mip, 1, face, 1)) + .ConstantBuffer("PrefilterParams", state.prefilterCB); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), state.prefilterLayout, nv); + if (!set) + continue; + + nvrhi::ComputeState cs; + cs.pipeline = state.prefilterPipe; + cs.bindings = {set}; + cmd->setComputeState(cs); + cmd->dispatch((size + 7) / 8, (size + 7) / 8, 1); + } + + cmd->setTextureState(dst, nvrhi::TextureSubresourceSet(mip, 1, 0, 6), nvrhi::ResourceStates::ShaderResource); +} + +static void DispatchSH( + nvrhi::ICommandList* cmd, + nvrhi::IDevice* nv, + IBLPrefilterPassState& state, + nvrhi::ITexture* src, + u32 outOffset) +{ + if (!cmd || !src || !state.shPipe) + return; + auto* loader = GEnv.Render->GetShaderLoader(); + auto* refl = loader->GetCachedReflection("ibl_sh_project", ".cs"); + if (!refl) + return; + auto& cache = GetPassResourceCache(); + + cmd->setBufferState(state.shBuffer, nvrhi::ResourceStates::UnorderedAccess); + cmd->setTextureState(src, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + + SHProjectCB cb{}; + cb.size = 32; + cb.outOffset = outOffset; + cmd->writeBuffer(state.shCB, &cb, sizeof(cb)); + + BindingSetBuilder bsb(*refl, nv, "IBLSH"); + bsb.Texture("t_Source", src) + .BufferUAV("u_SH", state.shBuffer) + .ConstantBuffer("SHProjectParams", state.shCB); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), state.shLayout, nv); + if (!set) + return; + + nvrhi::ComputeState cs; + cs.pipeline = state.shPipe; + cs.bindings = {set}; + cmd->setComputeState(cs); + cmd->dispatch(1, 1, 1); + cmd->setBufferState(state.shBuffer, nvrhi::ResourceStates::ShaderResource); +} + +static void DispatchBRDF(nvrhi::ICommandList* cmd, nvrhi::IDevice* nv, IBLPrefilterPassState& state) +{ + if (!cmd || !state.brdfPipe || state.brdfReady) + return; + auto* loader = GEnv.Render->GetShaderLoader(); + auto* refl = loader->GetCachedReflection("ibl_brdf_lut", ".cs"); + if (!refl) + return; + auto& cache = GetPassResourceCache(); + + cmd->setTextureState(state.brdfLut, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + BRDFLUTCB cb{}; + cb.dims[0] = kIBLBrdfSize; + cb.dims[1] = kIBLBrdfSize; + cmd->writeBuffer(state.brdfCB, &cb, sizeof(cb)); + + BindingSetBuilder bsb(*refl, nv, "IBLBRDF"); + bsb.TextureUAV("u_BRDFLUT", state.brdfLut).ConstantBuffer("BRDFLUTParams", state.brdfCB); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), state.brdfLayout, nv); + if (!set) + return; + + nvrhi::ComputeState cs; + cs.pipeline = state.brdfPipe; + cs.bindings = {set}; + cmd->setComputeState(cs); + cmd->dispatch((kIBLBrdfSize + 7) / 8, (kIBLBrdfSize + 7) / 8, 1); + cmd->setTextureState(state.brdfLut, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + state.brdfReady = true; +} + +static void IBLUpdateBlends(IBLPrefilterPassState& state) +{ + const float dt = std::max(Device.fTimeDelta, 0.f); + const float prefSpeed = 1.25f; + const float probeSpeed = 1.75f; + const bool wantPref = ps_r_ibl_prefilter != 0 && state.pipelinesReady && state.hasBakedOnce + && !state.needsPrefilter && state.prefilterStep == 0; + const float prefTarget = wantPref ? 1.f : 0.f; + state.prefilterBlend += (prefTarget - state.prefilterBlend) + * std::min(1.f, dt * prefSpeed); + state.prefilterBlend = std::clamp(state.prefilterBlend, 0.f, 1.f); + + for (u32 i = 0; i < state.probeCount; ++i) + { + auto& p = state.probes[i]; + const float target = (p.ready && !p.dirty) ? p.intensity : 0.f; + p.blend += (target - p.blend) * std::min(1.f, dt * probeSpeed); + p.blend = std::clamp(p.blend, 0.f, 1.f); + } +} + +void IBLUploadProbeBuffer(IBLPrefilterPassState& state, nvrhi::ICommandList* cmd, nvrhi::IDevice* nv) +{ + if (!cmd || !state.probeBuffer) + return; + IBLUpdateBlends(state); + xr_vector data; + data.resize(1 + kIBLMaxProbes * kIBLProbeStride); + data[0].set(0, 0, 0, float(state.probeCount)); + for (u32 i = 0; i < state.probeCount; ++i) + { + const auto& p = state.probes[i]; + const u32 base = 1 + i * kIBLProbeStride; + data[base + 0].set(p.position.x, p.position.y, p.position.z, p.radius); + data[base + 1].set(p.boxMin.x, p.boxMin.y, p.boxMin.z, 0.f); + data[base + 2].set(p.boxMax.x, p.boxMax.y, p.boxMax.z, p.blend); + for (u32 s = 0; s < kIBLSHCount; ++s) + data[base + 3 + s].set(0, 0, 0, 0); + } + cmd->writeBuffer(state.probeBuffer, data.data(), data.size() * sizeof(Fvector4)); +} + +void IBLUpdateFrameMeta(IBLPrefilterPassState& state, nvrhi::ICommandList* cmd, float ssgiSkyScale) +{ + if (!cmd || !state.shBuffer) + return; + Fvector4 meta; + meta.set(float(state.mipCount), state.prefilterBlend, ssgiSkyScale, float(state.probeCount)); + const u32 metaOffset = (kIBLSHCount * 2) * sizeof(Fvector4); + cmd->setBufferState(state.shBuffer, nvrhi::ResourceStates::CopyDest); + cmd->writeBuffer(state.shBuffer, &meta, sizeof(meta), metaOffset); + cmd->setBufferState(state.shBuffer, nvrhi::ResourceStates::ShaderResource); +} + +static void IBLFillProbeBounds(IBLProbeCPU& p) +{ + p.boxMin.set(p.position.x - p.radius, p.position.y - p.radius * 0.5f, p.position.z - p.radius); + p.boxMax.set(p.position.x + p.radius, p.position.y + p.radius * 1.5f, p.position.z + p.radius); +} + +static void IBLMarkAllProbesDirty(IBLPrefilterPassState& state) +{ + for (u32 i = 0; i < state.probeCount; ++i) + state.probes[i].dirty = true; +} + +static void IBLScheduleNextDirtyCapture(IBLPrefilterPassState& state) +{ + if (state.pendingCapture >= 0) + return; + const Fvector& cam = Device.vCameraPosition; + int best = -1; + float bestD = flt_max; + for (u32 i = 0; i < state.probeCount; ++i) + { + if (!state.probes[i].dirty) + continue; + const float d = cam.distance_to(state.probes[i].position); + if (d < bestD) + { + bestD = d; + best = int(i); + } + } + if (best >= 0) + { + state.pendingCapture = best; + state.captureStep = 0; + } +} + +int IBLAddProbeAtCamera(IBLPrefilterPassState& state, float radius) +{ + if (state.probeCount >= kIBLMaxProbes) + return -1; + const int idx = int(state.probeCount++); + auto& p = state.probes[idx]; + p.position = Device.vCameraPosition; + p.radius = radius; + IBLFillProbeBounds(p); + p.intensity = 1.f; + p.blend = 0.f; + p.dirty = true; + p.ready = false; + state.pendingCapture = idx; + state.captureStep = 0; + return idx; +} + +void IBLClearProbes(IBLPrefilterPassState& state) +{ + state.probeCount = 0; + state.pendingCapture = -1; + state.captureStep = 0; +} + +void IBLRequestCapture(IBLPrefilterPassState& state, int probeIndex) +{ + if (probeIndex >= 0 && probeIndex < int(state.probeCount)) + { + state.probes[probeIndex].dirty = true; + state.pendingCapture = probeIndex; + state.captureStep = 0; + } +} + +void IBLUpdateAutoProbes(IBLPrefilterPassState& state, nvrhi::ITexture* srcSky0, nvrhi::ITexture* srcSky1) +{ + if (ps_r_ibl_probe_auto == 0 || !state.resourcesReady || !state.pipelinesReady) + return; + if (ps_r_ibl_prefilter == 0 || ps_r_sky_ibl == 0) + return; + if (!g_pGameLevel || !srcSky0) + return; + + const Fvector& cam = Device.vCameraPosition; + if (cam.square_magnitude() < 1.f) + return; + + const bool skyChanged = (srcSky0 != state.lastAutoSky0) || (srcSky1 != state.lastAutoSky1); + if (skyChanged) + { + state.lastAutoSky0 = srcSky0; + state.lastAutoSky1 = srcSky1; + IBLMarkAllProbesDirty(state); + } + + const float radius = std::max(2.f, ps_r_ibl_probe_radius); + const float spacing = radius * 0.85f; + + int nearest = -1; + float nearestDist = flt_max; + int farthest = -1; + float farthestDist = -1.f; + for (u32 i = 0; i < state.probeCount; ++i) + { + const float d = cam.distance_to(state.probes[i].position); + if (d < nearestDist) + { + nearestDist = d; + nearest = int(i); + } + if (d > farthestDist) + { + farthestDist = d; + farthest = int(i); + } + } + + if (nearest < 0 || nearestDist > spacing) + { + int idx = -1; + if (state.probeCount < kIBLMaxProbes) + idx = int(state.probeCount++); + else if (farthest >= 0) + idx = farthest; + + if (idx >= 0) + { + auto& p = state.probes[idx]; + p.position = cam; + p.radius = radius; + IBLFillProbeBounds(p); + p.intensity = 1.f; + p.blend = 0.f; + p.dirty = true; + p.ready = false; + static u32 s_placeLog = 0; + if (s_placeLog < 8) + { + Msg("* [IBL] auto probe %d at (%.1f,%.1f,%.1f) r=%.1f", idx, cam.x, cam.y, cam.z, radius); + ++s_placeLog; + } + } + } + + IBLScheduleNextDirtyCapture(state); +} + +static bool CaptureProbeStep( + nvrhi::ICommandList* cmd, + nvrhi::IDevice* nv, + IBLPrefilterPassState& state, + nvrhi::ITexture* sky0, + nvrhi::ITexture* sky1, + nvrhi::ITexture* sceneColor, + nvrhi::ITexture* sceneDepth, + int probeIndex) +{ + if (!cmd || !state.probeCapturePipe || probeIndex < 0 || probeIndex >= int(state.probeCount)) + return true; + if (!sky0 || !sceneColor || !sceneDepth || !state.probeCaptureTemp || !state.probeCubeArray) + return true; + + auto* loader = GEnv.Render->GetShaderLoader(); + auto& cache = GetPassResourceCache(); + auto& p = state.probes[probeIndex]; + + if (state.captureStep < 6) + { + auto* refl = loader->GetCachedReflection("ibl_probe_capture", ".cs"); + if (!refl) + return true; + + float skyLerp = 0.f; + if (g_pGamePersistent) + skyLerp = g_pGamePersistent->Environment().CurrentEnv.weight; + + const u32 face = state.captureStep; + cmd->setTextureState(state.probeCaptureTemp, + nvrhi::TextureSubresourceSet(0, 1, face, 1), nvrhi::ResourceStates::UnorderedAccess); + + ProbeCaptureCB cb{}; + cb.vp = Device.mFullTransform; + cb.invVP = Device.mInvFullTransform; + cb.probePos = p.position; + cb.skyLerp = skyLerp; + cb.eyePos = Device.vCameraPosition; + cb.maxDistance = p.radius * 2.f; + cb.size[0] = kIBLProbeSize; + cb.size[1] = kIBLProbeSize; + cb.face = face; + cmd->writeBuffer(state.probeCaptureCB, &cb, sizeof(cb)); + + BindingSetBuilder bsb(*refl, nv, "IBLProbeCap"); + bsb.Texture("t_Sky0", sky0) + .Texture("t_Sky1", sky1 ? sky1 : sky0) + .Texture("g_SceneColor", sceneColor) + .Texture("g_SceneDepth", sceneDepth) + .TextureUAV("u_ProbeFaces", state.probeCaptureTemp, nvrhi::Format::UNKNOWN, + nvrhi::TextureSubresourceSet(0, 1, face, 1)) + .ConstantBuffer("ProbeCaptureParams", state.probeCaptureCB); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), state.probeCaptureLayout, nv); + if (set) + { + nvrhi::ComputeState cs; + cs.pipeline = state.probeCapturePipe; + cs.bindings = {set}; + cmd->setComputeState(cs); + cmd->dispatch((kIBLProbeSize + 7) / 8, (kIBLProbeSize + 7) / 8, 1); + } + cmd->setTextureState(state.probeCaptureTemp, + nvrhi::TextureSubresourceSet(0, 1, face, 1), nvrhi::ResourceStates::ShaderResource); + ++state.captureStep; + return false; + } + + if (state.prefilterPipe) + { + auto* prefRefl = loader->GetCachedReflection("ibl_prefilter_specular", ".cs"); + if (prefRefl) + { + cmd->setTextureState(state.probeCaptureTemp, nvrhi::AllSubresources, + nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(state.probeCubeArray, + nvrhi::TextureSubresourceSet(0, 1, probeIndex * 6, 6), + nvrhi::ResourceStates::UnorderedAccess); + + for (u32 face = 0; face < 6; ++face) + { + PrefilterCB cb{}; + cb.size[0] = kIBLProbeSize; + cb.size[1] = kIBLProbeSize; + cb.face = face; + cb.sampleCount = 1; + cb.roughness = 0.f; + cb.mipBias = 0.f; + cmd->writeBuffer(state.prefilterCB, &cb, sizeof(cb)); + + const u32 arraySlice = probeIndex * 6 + face; + BindingSetBuilder bsb(*prefRefl, nv, "IBLProbePref"); + bsb.Texture("t_Source", state.probeCaptureTemp) + .TextureUAV("u_Dest", state.probeCubeArray, nvrhi::Format::UNKNOWN, + nvrhi::TextureSubresourceSet(0, 1, arraySlice, 1)) + .ConstantBuffer("PrefilterParams", state.prefilterCB); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), state.prefilterLayout, nv); + if (!set) + continue; + nvrhi::ComputeState cs; + cs.pipeline = state.prefilterPipe; + cs.bindings = {set}; + cmd->setComputeState(cs); + cmd->dispatch((kIBLProbeSize + 7) / 8, (kIBLProbeSize + 7) / 8, 1); + } + cmd->setTextureState(state.probeCubeArray, + nvrhi::TextureSubresourceSet(0, 1, probeIndex * 6, 6), + nvrhi::ResourceStates::ShaderResource); + } + } + + p.dirty = false; + p.ready = true; + state.captureStep = 0; + Msg("* [IBL] Captured probe %d at (%.1f,%.1f,%.1f)", probeIndex, p.position.x, p.position.y, p.position.z); + return true; +} + +IBLBindResources setupIBLPrefilterPass( + FrameGraph& fg, + fg::RenderDevice* device, + nvrhi::ITexture* srcSky0, + nvrhi::ITexture* srcSky1, + IBLPrefilterPassState& state) +{ + g_ActiveIBLState = &state; + + IBLBindResources out{}; + out.prefilterEnabled = false; + out.mipCount = state.mipCount; + + auto& cache = GetPassResourceCache(); + nvrhi::IDevice* nv = device ? device->GetNVRHIDevice() : nullptr; + nvrhi::ITexture* dummyCube = nv ? cache.GetDummyCubeMap(nv) : nullptr; + const bool hasRealSky = srcSky0 && srcSky0 != dummyCube; + const bool iblActive = ps_r_ibl_prefilter != 0 && ps_r_sky_ibl != 0 && g_pGameLevel && hasRealSky + && !Device.dwPrecacheFrame + && !(g_pGamePersistent && g_pGamePersistent->IsLoadingScreenShown()); + + if (!nv || !iblActive) + { + out.brdfLut = nv ? cache.GetDummyContactHistory(nv) : nullptr; + out.shBuffer = nv ? cache.GetDummyStructuredBuffer(nv) : nullptr; + out.probeBuffer = nv ? cache.GetDummyStructuredBuffer(nv) : nullptr; + out.probeCubeArray = nv ? cache.GetDummyCubeArray(nv) : nullptr; + out.spec0 = srcSky0 ? srcSky0 : dummyCube; + out.spec1 = srcSky1 ? srcSky1 : out.spec0; + SetCurrentIBLBindResources(out); + return out; + } + + InitializeIBLPrefilter(device, state); + if (!state.resourcesReady || !state.pipelinesReady) + { + out.brdfLut = cache.GetDummyContactHistory(nv); + out.shBuffer = cache.GetDummyStructuredBuffer(nv); + out.probeBuffer = cache.GetDummyStructuredBuffer(nv); + out.probeCubeArray = cache.GetDummyCubeArray(nv); + out.spec0 = srcSky0 ? srcSky0 : dummyCube; + out.spec1 = srcSky1 ? srcSky1 : out.spec0; + SetCurrentIBLBindResources(out); + return out; + } + + out.brdfLut = state.brdfLut; + out.shBuffer = state.shBuffer; + out.probeBuffer = state.probeBuffer; + out.probeCubeArray = state.probeCubeArray; + + if (srcSky0 != state.lastSrc0 || srcSky1 != state.lastSrc1) + { + state.lastSrc0 = srcSky0; + state.lastSrc1 = srcSky1; + state.needsPrefilter = true; + state.prefilterStep = 1; + } + else if (state.needsPrefilter && state.prefilterStep == 0) + { + state.prefilterStep = 1; + } + + if (state.hasBakedOnce) + { + out.spec0 = state.spec0; + out.spec1 = state.spec1; + out.prefilterEnabled = true; + } + else + { + out.spec0 = srcSky0; + out.spec1 = srcSky1 ? srcSky1 : srcSky0; + out.prefilterEnabled = false; + } + SetCurrentIBLBindResources(out); + + fg.addCallbackPass( + "IBL Prefilter", + [&](FrameGraph& builder, PassHandle passHandle, int&) { + RenderPassBuilder pb(builder, passHandle); + pb.sideEffects(); + }, + [&state, srcSky0, srcSky1, nv](const int&, const FrameGraph&, fg::RenderContext* ctx) { + nvrhi::ICommandList* cmd = ctx ? ctx->GetCommandList() : nullptr; + if (!cmd || !nv) + return; + + const u32 stepSpec0 = 2; + const u32 stepSpec1 = 2 + state.mipCount; + const u32 stepSH = 2 + 2 * state.mipCount; + + if (state.prefilterStep == 1) + { + DispatchBRDF(cmd, nv, state); + state.prefilterStep = stepSpec0; + } + else if (state.prefilterStep >= stepSpec0 && state.prefilterStep < stepSpec1) + { + DispatchPrefilterMip(cmd, nv, state, srcSky0, state.spec0, + state.prefilterStep - stepSpec0); + ++state.prefilterStep; + } + else if (state.prefilterStep >= stepSpec1 && state.prefilterStep < stepSH) + { + nvrhi::ITexture* src1 = (srcSky1 && srcSky1 != srcSky0) ? srcSky1 : srcSky0; + DispatchPrefilterMip(cmd, nv, state, src1, state.spec1, + state.prefilterStep - stepSpec1); + ++state.prefilterStep; + } + else if (state.prefilterStep == stepSH) + { + Fvector4 zeros[kIBLSHCount * 2 + 1] = {}; + cmd->setBufferState(state.shBuffer, nvrhi::ResourceStates::CopyDest); + cmd->writeBuffer(state.shBuffer, zeros, sizeof(zeros)); + DispatchSH(cmd, nv, state, srcSky0, 0); + DispatchSH(cmd, nv, state, srcSky1 ? srcSky1 : srcSky0, kIBLSHCount); + state.needsPrefilter = false; + state.prefilterStep = 0; + state.hasBakedOnce = true; + Msg("* [IBL] Prefiltered sky cubes + SH"); + } + + IBLUploadProbeBuffer(state, cmd, nv); + IBLUpdateFrameMeta(state, cmd, 1.f); + }); + + return out; +} + +void setupIBLProbeCapturePass( + FrameGraph& fg, + fg::RenderDevice* device, + nvrhi::ITexture* srcSky0, + nvrhi::ITexture* srcSky1, + VirtualResourceHandle sceneColor, + VirtualResourceHandle sceneDepth, + IBLPrefilterPassState& state) +{ + IBLUpdateAutoProbes(state, srcSky0, srcSky1); + + if (state.needsPrefilter || state.prefilterStep != 0) + return; + if (state.pendingCapture < 0 || !sceneColor.is_valid() || !sceneDepth.is_valid()) + return; + if (!srcSky0 || !state.pipelinesReady) + return; + + if (!state.probeCapturePipe) + { + static bool s_logged = false; + if (!s_logged) + { + Msg("! [IBL] probe capture pipeline missing — auto probes disabled"); + s_logged = true; + } + state.pendingCapture = -1; + return; + } + + fg.addCallbackPass( + "IBL Probe Capture", + [&](FrameGraph& builder, PassHandle passHandle, int&) { + RenderPassBuilder passBuilder(builder, passHandle); + passBuilder.read(sceneColor, ResourceState::ShaderResource); + passBuilder.read(sceneDepth, ResourceState::ShaderResource); + passBuilder.sideEffects(); + }, + [&state, srcSky0, srcSky1, sceneColor, sceneDepth, device](const int&, const FrameGraph& graph, fg::RenderContext* ctx) { + nvrhi::ICommandList* cmd = ctx ? ctx->GetCommandList() : nullptr; + nvrhi::IDevice* nvDev = device ? device->GetNVRHIDevice() : nullptr; + if (!cmd || !nvDev || state.pendingCapture < 0) + return; + nvrhi::ITexture* colorTex = graph.GetPhysicalTexture(sceneColor); + nvrhi::ITexture* depthTex = graph.GetPhysicalTexture(sceneDepth); + if (!colorTex || !depthTex) + return; + const int idx = state.pendingCapture; + const bool done = CaptureProbeStep(cmd, nvDev, state, srcSky0, + srcSky1 ? srcSky1 : srcSky0, colorTex, depthTex, idx); + if (done) + { + state.pendingCapture = -1; + IBLUploadProbeBuffer(state, cmd, nvDev); + IBLScheduleNextDirtyCapture(state); + } + }); +} + +void BindIBLResources(BindingSetBuilder& bsb, const IBLBindResources& ibl, nvrhi::IDevice* nv) +{ + auto& cache = GetPassResourceCache(); + nvrhi::ITexture* brdf = ibl.brdfLut ? ibl.brdfLut : cache.GetDummyContactHistory(nv); + nvrhi::IBuffer* sh = ibl.shBuffer ? ibl.shBuffer : cache.GetDummyStructuredBuffer(nv); + nvrhi::IBuffer* probeMeta = ibl.probeBuffer ? ibl.probeBuffer : cache.GetDummyStructuredBuffer(nv); + nvrhi::ITexture* probes = ibl.probeCubeArray + ? ibl.probeCubeArray + : cache.GetDummyCubeArray(nv); + + if (brdf) + bsb.Texture("s_envBRDF", brdf); + if (sh) + bsb.BufferSRV("g_IBLSkySH", sh); + if (probeMeta) + bsb.BufferSRV("g_IBLProbes", probeMeta); + if (probes) + bsb.Texture("s_probeSpec", probes); +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/IBLPrefilterPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/IBLPrefilterPassSetup.h new file mode 100644 index 00000000000..cbfbfc28246 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/IBLPrefilterPassSetup.h @@ -0,0 +1,126 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include + +namespace xray::render { +namespace fg { +class RenderDevice; +} +} + +namespace xray::render::framegraph { +class FrameGraph; +} + +namespace xray::render::fg::passes { + +constexpr u32 kIBLSpecSize = 128; +constexpr u32 kIBLBrdfSize = 128; +constexpr u32 kIBLSHCount = 9; +constexpr u32 kIBLMaxProbes = 16; +constexpr u32 kIBLProbeStride = 16; +constexpr u32 kIBLProbeSize = 64; +constexpr u32 kIBLSlotEnvBRDF = 52; +constexpr u32 kIBLSlotSkySH = 53; +constexpr u32 kIBLSlotProbes = 54; +constexpr u32 kIBLSlotProbeSpec = 55; + +struct IBLProbeCPU +{ + Fvector position; + float radius = 8.f; + Fvector boxMin; + Fvector boxMax; + float intensity = 1.f; + float blend = 0.f; + bool dirty = true; + bool ready = false; +}; + +struct IBLPrefilterPassState +{ + nvrhi::ComputePipelineHandle prefilterPipe; + nvrhi::BindingLayoutHandle prefilterLayout; + nvrhi::ComputePipelineHandle shPipe; + nvrhi::BindingLayoutHandle shLayout; + nvrhi::ComputePipelineHandle brdfPipe; + nvrhi::BindingLayoutHandle brdfLayout; + nvrhi::ComputePipelineHandle probeCapturePipe; + nvrhi::BindingLayoutHandle probeCaptureLayout; + + nvrhi::TextureHandle spec0; + nvrhi::TextureHandle spec1; + nvrhi::TextureHandle brdfLut; + nvrhi::TextureHandle probeCubeArray; + nvrhi::TextureHandle probeCaptureTemp; + nvrhi::BufferHandle shBuffer; + nvrhi::BufferHandle probeBuffer; + nvrhi::BufferHandle prefilterCB; + nvrhi::BufferHandle shCB; + nvrhi::BufferHandle brdfCB; + nvrhi::BufferHandle probeCaptureCB; + + nvrhi::ITexture* lastSrc0 = nullptr; + nvrhi::ITexture* lastSrc1 = nullptr; + bool brdfReady = false; + bool resourcesReady = false; + bool pipelinesReady = false; + bool needsPrefilter = true; + bool hasBakedOnce = false; + float prefilterBlend = 0.f; + u32 prefilterStep = 0; + u32 mipCount = 1; + u32 probeCount = 0; + IBLProbeCPU probes[kIBLMaxProbes]; + int pendingCapture = -1; + u32 captureStep = 0; + nvrhi::ITexture* lastAutoSky0 = nullptr; + nvrhi::ITexture* lastAutoSky1 = nullptr; +}; + +struct IBLBindResources +{ + nvrhi::ITexture* spec0 = nullptr; + nvrhi::ITexture* spec1 = nullptr; + nvrhi::ITexture* brdfLut = nullptr; + nvrhi::IBuffer* shBuffer = nullptr; + nvrhi::IBuffer* probeBuffer = nullptr; + nvrhi::ITexture* probeCubeArray = nullptr; + bool prefilterEnabled = false; + u32 mipCount = 1; +}; + +void InitializeIBLPrefilter(fg::RenderDevice* device, IBLPrefilterPassState& state); + +IBLBindResources setupIBLPrefilterPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + nvrhi::ITexture* srcSky0, + nvrhi::ITexture* srcSky1, + IBLPrefilterPassState& state); + +void setupIBLProbeCapturePass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + nvrhi::ITexture* srcSky0, + nvrhi::ITexture* srcSky1, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle sceneDepth, + IBLPrefilterPassState& state); + +void BindIBLResources(framegraph::BindingSetBuilder& bsb, const IBLBindResources& ibl, nvrhi::IDevice* nv); +void SetCurrentIBLBindResources(const IBLBindResources& ibl); +const IBLBindResources& GetCurrentIBLBindResources(); +IBLPrefilterPassState* GetActiveIBLState(); + +int IBLAddProbeAtCamera(IBLPrefilterPassState& state, float radius = 8.f); +void IBLClearProbes(IBLPrefilterPassState& state); +void IBLRequestCapture(IBLPrefilterPassState& state, int probeIndex); +void IBLUpdateAutoProbes(IBLPrefilterPassState& state, nvrhi::ITexture* srcSky0, nvrhi::ITexture* srcSky1); +void IBLUploadProbeBuffer(IBLPrefilterPassState& state, nvrhi::ICommandList* cmd, nvrhi::IDevice* nv); +void IBLUpdateFrameMeta(IBLPrefilterPassState& state, nvrhi::ICommandList* cmd, float ssgiSkyScale); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.cpp index 5833b427e9d..dd1f495f976 100644 --- a/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.cpp @@ -1,4 +1,5 @@ #include "stdafx.h" +#include #include "LocalShadowPassSetup.h" #include "PassCommon.h" #include "ShaderConstants.h" @@ -28,38 +29,55 @@ namespace xray::render::fg::passes { -namespace -{ - -u32 LocalShadowResolution() -{ - // Prefer sharper long floor shadows (beds/tables). Cap at 2048. - const u32 half = std::max(ps_r2_smapsize / 2u, 512u); - return std::min(half, 2048u); -} - -} // namespace - void InitializeLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& state) { if (!device) return; - const u32 res = LocalShadowResolution(); - const u32 slices = MAX_LOCAL_SHADOW_TILES; + const u32 res = xray::render::fg::LocalShadowAtlasSize(); + const u32 pages = xray::render::fg::LocalShadowPageCount(); + constexpr u32 kLocalCascadeCBVersions = 8192u; + if (state.initialized && state.enabled && state.resolution == res && - state.sliceCount == slices && state.atlas) + state.sliceCount == pages && state.atlas && state.staticCacheTex && + state.cascadeCB && state.cascadeCBMaxVersions >= kLocalCascadeCBVersions && + state.clearDepthPipeline && state.localSkinIndirectArgs && state.esmCB && + state.esmConvertPipeline && state.esmBlurPipeline) return; if (state.initialized) ShutdownLocalShadowPass(device, state); - // Pipelines are resolution-independent but clear so bias-tuned ones rebuild cleanly. state.opaquePipeline = nullptr; state.foliagePipeline = nullptr; + state.esmConvertPipeline = nullptr; + state.esmBlurPipeline = nullptr; + state.esmConvertLayout = nullptr; + state.esmBlurLayout = nullptr; + state.skin1w = {}; + state.skinHq = {}; + state.skin2w = {}; + state.skin3w = {}; + state.skin4w = {}; + state.localSkinInstanceSSBO = nullptr; + state.localSkinInstanceCapacity = 0; + state.localSkinIndirectArgs = nullptr; + state.localSkinIndirectArgsCapacity = 0; + state.cullDoneMarker = nullptr; + state.clearDepthPipeline = nullptr; + state.staticCache = nullptr; + state.staticCacheTex = nullptr; + state.esmAtlas = nullptr; + state.esmAtlasTex = nullptr; + state.esmTemp = nullptr; + state.esmTempTex = nullptr; + state.esmCB = nullptr; + for (u32 i = 0; i < MAX_LOCAL_SHADOW_PAGES; ++i) + state.pageFramebuffers[i] = nullptr; auto* resMgr = device->GetFGResourceManager(); - if (!resMgr || !resMgr->GetRTFactory() || !resMgr->GetTextureManager()) + nvrhi::IDevice* nv = device->GetNVRHIDevice(); + if (!resMgr || !resMgr->GetRTFactory() || !resMgr->GetTextureManager() || !nv) { state.initialized = true; state.enabled = false; @@ -67,16 +85,195 @@ void InitializeLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& s } state.resolution = res; - state.sliceCount = slices; + state.sliceCount = pages; state.atlasHandle = resMgr->GetRTFactory()->CreateCascadedShadowMap( - res, slices, true, "rt_LocalShadowAtlas"); + res, pages, true, "rt_LocalShadowAtlas"); state.atlas = resMgr->GetTextureManager()->GetNVRHITexture(state.atlasHandle); + + { + nvrhi::TextureDesc td; + td.width = res; + td.height = res; + td.depth = 1; + td.arraySize = pages; + td.mipLevels = 1; + td.format = nvrhi::Format::D32; + td.dimension = nvrhi::TextureDimension::Texture2DArray; + td.isRenderTarget = false; + td.isTypeless = false; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "rt_LocalShadowStaticCache"; + td.isUAV = false; + state.staticCache = nv->createTexture(td); + state.staticCacheTex = state.staticCache; + } + { + nvrhi::TextureDesc td; + td.width = res; + td.height = res; + td.depth = 1; + td.arraySize = pages; + td.mipLevels = 1; + td.format = nvrhi::Format::R16_FLOAT; + td.dimension = nvrhi::TextureDimension::Texture2DArray; + td.isUAV = true; + td.initialState = nvrhi::ResourceStates::UnorderedAccess; + td.keepInitialState = true; + td.debugName = "rt_LocalShadowESM"; + state.esmAtlas = nv->createTexture(td); + state.esmAtlasTex = state.esmAtlas; + td.debugName = "rt_LocalShadowESMTemp"; + state.esmTemp = nv->createTexture(td); + state.esmTempTex = state.esmTemp; + } + + { + nvrhi::BufferDesc cbDesc; + cbDesc.byteSize = sizeof(ShadowCascadeCB); + cbDesc.isConstantBuffer = true; + cbDesc.isVolatile = true; + cbDesc.maxVersions = kLocalCascadeCBVersions; + cbDesc.debugName = "LocalShadowCascadeCB"; + state.cascadeCB = nv->createBuffer(cbDesc); + state.cascadeCBMaxVersions = state.cascadeCB ? kLocalCascadeCBVersions : 0; + } + { + nvrhi::BufferDesc cbDesc; + cbDesc.byteSize = 32; + cbDesc.isConstantBuffer = true; + cbDesc.isVolatile = false; + cbDesc.initialState = nvrhi::ResourceStates::ConstantBuffer; + cbDesc.keepInitialState = true; + cbDesc.debugName = "LocalShadowEsmCB"; + state.esmCB = nv->createBuffer(cbDesc); + } + { + nvrhi::BufferDesc md; + md.byteSize = sizeof(u32); + md.canHaveUAVs = true; + md.canHaveRawViews = true; + md.initialState = nvrhi::ResourceStates::UnorderedAccess; + md.keepInitialState = true; + md.debugName = "LocalShadowCullDone"; + state.cullDoneMarker = nv->createBuffer(md); + } + { + constexpr u32 kLocalSkinIndirectCap = 4096u; + nvrhi::BufferDesc desc; + desc.byteSize = u64(kLocalSkinIndirectCap) * sizeof(IndirectDrawArgs); + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::IndirectArgument; + desc.keepInitialState = true; + desc.debugName = "LocalSkinIndirectArgs"; + state.localSkinIndirectArgs = nv->createBuffer(desc); + state.localSkinIndirectArgsCapacity = + state.localSkinIndirectArgs ? kLocalSkinIndirectCap : 0; + } + + { + auto* shaderLoader = GEnv.Render ? GEnv.Render->GetShaderLoader() : nullptr; + if (shaderLoader) + { + auto vsClear = shaderLoader->LoadVertexShader("shadow\\local_shadow_clear", "main"); + if (vsClear.handle) + { + nvrhi::GraphicsPipelineDesc d; + d.VS = vsClear.handle; + d.PS = nullptr; + d.primType = nvrhi::PrimitiveType::TriangleList; + d.renderState.depthStencilState.setDepthTestEnable(true); + d.renderState.depthStencilState.setDepthWriteEnable(true); + d.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::Always); + d.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.depthFormat = nvrhi::Format::D32; + state.clearDepthPipeline = GetPassResourceCache().GetOrCreatePipeline( + "LocalShadowClearDepth_v2", d, fbInfo, nv); + if (!state.clearDepthPipeline) + Msg("! [LocalShadow] clearDepth pipeline create failed"); + } + else + { + Msg("! [LocalShadow] shadow\\local_shadow_clear.vs missing"); + } + + auto& cache = GetPassResourceCache(); + auto makeEsmLayout = [&](const ExtractedReflection& refl, const char* name) { + nvrhi::BindingLayoutDesc desc; + desc.visibility = nvrhi::ShaderType::Compute; + for (const auto& cb : refl.constantLayout.constantBuffers.buffers) + desc.bindings.push_back(nvrhi::BindingLayoutItem::ConstantBuffer(cb.slot)); + for (const auto& tex : refl.rtBindings.inputTextures) + desc.bindings.push_back(nvrhi::BindingLayoutItem::Texture_SRV(tex.slot)); + for (const auto& uav : refl.rtBindings.uavBindings) + desc.bindings.push_back(nvrhi::BindingLayoutItem::Texture_UAV(uav.slot)); + return cache.GetOrCreateBindingLayout(name, desc, nv); + }; + + auto esmConv = shaderLoader->LoadComputeShader("local_shadow_esm_convert", "main"); + if (esmConv.handle && esmConv.reflection) + { + state.esmConvertLayout = makeEsmLayout(*esmConv.reflection, "LocalShadowEsmConvert_v3"); + if (state.esmConvertLayout) + { + nvrhi::ComputePipelineDesc pd; + pd.CS = esmConv.handle; + pd.bindingLayouts = {state.esmConvertLayout}; + state.esmConvertPipeline = cache.GetOrCreateComputePipeline( + "LocalShadowEsmConvert_v3", pd, nv); + } + } + else + { + Msg("! [LocalShadow] local_shadow_esm_convert.cs failed to load"); + } + auto esmBlur = shaderLoader->LoadComputeShader("local_shadow_esm_blur", "main"); + if (esmBlur.handle && esmBlur.reflection) + { + state.esmBlurLayout = makeEsmLayout(*esmBlur.reflection, "LocalShadowEsmBlur_v3"); + if (state.esmBlurLayout) + { + nvrhi::ComputePipelineDesc pd; + pd.CS = esmBlur.handle; + pd.bindingLayouts = {state.esmBlurLayout}; + state.esmBlurPipeline = cache.GetOrCreateComputePipeline( + "LocalShadowEsmBlur_v3", pd, nv); + } + } + else + { + Msg("! [LocalShadow] local_shadow_esm_blur.cs failed to load"); + } + } + } + + if (state.esmAtlasTex && nv) + { + nvrhi::CommandListHandle clearCmd = nv->createCommandList(); + if (clearCmd) + { + clearCmd->open(); + clearCmd->clearTextureFloat( + state.esmAtlasTex, nvrhi::AllSubresources, nvrhi::Color(1.f)); + if (state.esmTempTex) + clearCmd->clearTextureFloat( + state.esmTempTex, nvrhi::AllSubresources, nvrhi::Color(1.f)); + clearCmd->close(); + nv->executeCommandList(clearCmd); + } + } + state.initialized = true; - state.enabled = (state.atlas != nullptr); + state.enabled = (state.atlas != nullptr && state.cascadeCB != nullptr && state.cullDoneMarker != nullptr); if (state.enabled) - Msg("* [LocalShadow] Init: OK (%ux%u × %u slices)", res, res, slices); + Msg("* [LocalShadow] Init: OK (%ux%u × %u pages, CB versions %u, clear=%d, static=%d, esmTex=%d, esmPipe=%d/%d)", + res, res, pages, state.cascadeCBMaxVersions, state.clearDepthPipeline ? 1 : 0, + state.staticCacheTex ? 1 : 0, state.esmAtlasTex ? 1 : 0, + state.esmConvertPipeline ? 1 : 0, state.esmBlurPipeline ? 1 : 0); else - Msg("! [LocalShadow] Failed to create local shadow atlas"); + Msg("! [LocalShadow] Failed to create local shadow atlas/CB"); } void ShutdownLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& state) @@ -88,8 +285,34 @@ void ShutdownLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& sta } state.atlasHandle = {}; state.atlas = nullptr; + state.staticCache = nullptr; + state.staticCacheTex = nullptr; + state.esmAtlas = nullptr; + state.esmAtlasTex = nullptr; + state.esmTemp = nullptr; + state.esmTempTex = nullptr; + state.cascadeCB = nullptr; + state.esmCB = nullptr; + state.cascadeCBMaxVersions = 0; + state.cullDoneMarker = nullptr; state.opaquePipeline = nullptr; state.foliagePipeline = nullptr; + state.esmConvertPipeline = nullptr; + state.esmBlurPipeline = nullptr; + state.esmConvertLayout = nullptr; + state.esmBlurLayout = nullptr; + state.skin1w = {}; + state.skinHq = {}; + state.skin2w = {}; + state.skin3w = {}; + state.skin4w = {}; + state.localSkinInstanceSSBO = nullptr; + state.localSkinInstanceCapacity = 0; + state.localSkinIndirectArgs = nullptr; + state.localSkinIndirectArgsCapacity = 0; + for (u32 i = 0; i < MAX_LOCAL_SHADOW_PAGES; ++i) + state.pageFramebuffers[i] = nullptr; + state.clearDepthPipeline = nullptr; state.resolution = 0; state.sliceCount = 0; state.initialized = false; @@ -103,7 +326,8 @@ LocalShadowOutputs setupLocalShadowPass( ShadowPassState& shadowState, LocalShadowPassState& state, const xr_vector* worldSkinnedBatches, - fg::GPUCullingManager* gpuCulling) + fg::GPUCullingManager* gpuCulling, + framegraph::VirtualResourceHandle gpuCullDrawArgs) { using namespace framegraph; LocalShadowOutputs outputs; @@ -118,7 +342,23 @@ LocalShadowOutputs setupLocalShadowPass( return outputs; InitializeLocalShadowPass(device, state); - if (!state.enabled || !state.atlas || !shadowState.enabled || !shadowState.pipeline) + if (!state.enabled || !state.atlas || !state.cascadeCB || !state.cullDoneMarker || + !shadowState.enabled || !shadowState.pipeline) + return outputs; + + nvrhi::IDevice* nvInit = device->GetNVRHIDevice(); + if (!gpuCulling || !nvInit || !gpuCulling->EnsureLightShadowCullPipeline(nvInit)) + { + static bool s_once = false; + if (!s_once) + { + Msg("! [LocalShadow] GPU light_shadow_cull unavailable — skipping local shadows"); + s_once = true; + } + return outputs; + } + const u32 slotNeed = GPUCullingManager::kLocalShadowCullMaxSlots; + if (!gpuCulling->EnsureLocalShadowCullSlots(nvInit, slotNeed)) return outputs; ResourceDesc desc; @@ -137,31 +377,36 @@ LocalShadowOutputs setupLocalShadowPass( struct PassData { VirtualResourceHandle atlas; + VirtualResourceHandle gpuCullArgs; LocalShadowPassState* localState = nullptr; ShadowPassState* shadowState = nullptr; BindlessForwardConfig bindlessConfig; fg::RenderDevice* device = nullptr; fg::GPUCullingManager* gpuCulling = nullptr; const xr_vector* worldSkinnedBatches = nullptr; - xr_vector tiles; + const xr_vector* tiles = nullptr; }; auto& passData = fg.addCallbackPass( - "LocalShadows", - [&, atlasHandle, worldSkinnedBatches, gpuCulling](FrameGraph& builder, PassHandle passHandle, PassData& data) { + "LocalShadowDraw", + [&, atlasHandle, gpuCullDrawArgs, worldSkinnedBatches, gpuCulling](FrameGraph& builder, PassHandle passHandle, PassData& data) { RenderPassBuilder pb(builder, passHandle); data.atlas = pb.write(atlasHandle, ResourceState::DepthStencilWrite); + if (gpuCullDrawArgs.is_valid()) + data.gpuCullArgs = pb.read(gpuCullDrawArgs, ResourceState::IndirectArgument); + else + pb.sideEffects(); data.localState = &state; data.shadowState = &shadowState; data.bindlessConfig = bindlessConfig; data.device = device; data.gpuCulling = gpuCulling; data.worldSkinnedBatches = worldSkinnedBatches; - data.tiles = tiles; + data.tiles = &clm.GetLocalShadowTiles(); }, [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { - ZoneScopedN("LocalShadows::Execute"); - if (!data.localState || !data.shadowState || data.tiles.empty()) + ZoneScopedN("LocalShadowDraw::Execute"); + if (!data.localState || !data.shadowState || !data.tiles || data.tiles->empty()) return; auto* atlasTex = graph.GetPhysicalTexture(data.atlas); if (!atlasTex) @@ -185,11 +430,35 @@ LocalShadowOutputs setupLocalShadowPass( return; } + const u32 casterCount = data.gpuCulling + ? (data.gpuCulling->GetStaticObjectCount() + data.gpuCulling->GetDynamicObjectCount() + + data.gpuCulling->GetTransparentObjectCount()) + : 0; + if (casterCount == 0 || !ls.clearDepthPipeline) + return; + + bool anyRedraw = false; + for (const LocalShadowTile& tile : *data.tiles) + { + if (tile.needsRedraw && tile.size >= 8) + { + anyRedraw = true; + break; + } + } + if (!anyRedraw) + return; + + { + ZoneScopedN("LocalShadowDraw::LightCull"); + data.gpuCulling->DispatchAllLocalShadowTileCulls(cmd, nv, *data.tiles, 1.35f); + } + auto* shaderLoader = GEnv.Render->GetShaderLoader(); auto* vsRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".vs"); auto* psRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".ps"); auto drawIndexBuffer = GetOrCreateDrawIndexBuffer("LocalShadow", nv); - if (!vsRefl || !psRefl || !drawIndexBuffer || !st.pipeline || !st.layout || !st.cascadeCB) + if (!vsRefl || !psRefl || !drawIndexBuffer || !st.pipeline || !st.layout || !ls.cascadeCB) return; auto& cache = GetPassResourceCache(); @@ -234,105 +503,563 @@ LocalShadowOutputs setupLocalShadowPass( ls.foliagePipeline ? ls.foliagePipeline.Get() : st.foliagePipeline.Get(); const u32 cres = ls.resolution; - nvrhi::Viewport vp(0.f, float(cres), 0.f, float(cres), 0.f, 1.f); + const auto& tilesRef = *data.tiles; { static bool s_once = false; if (!s_once) { - Msg("* [LocalShadow] Drawing %u tiles @ %ux%u (skinned=%d)", - (u32)data.tiles.size(), cres, cres, - (ps_r_skinned_shadows != 0 && data.worldSkinnedBatches) ? 1 : 0); + Msg("* [LocalShadow] Drawing %u packed rects @ %ux%u × %u pages (skinned=SSBO)", + (u32)tilesRef.size(), cres, cres, ls.sliceCount); s_once = true; } } - cmd->clearDepthStencilTexture( - atlasTex, - nvrhi::TextureSubresourceSet(0, 1, 0, ls.sliceCount), - true, 1.0f, false, 0); - - // Restore identity caster maps (CSM light-cull overwrites with compact subset). - if (data.gpuCulling) - data.gpuCulling->EnsureShadowCasterIdentityBuffers(cmd, nv); - - const bool drawWorldSkinned = - (ps_r_skinned_shadows != 0) && data.worldSkinnedBatches && data.gpuCulling && - (st.skinnedPipeline || st.skinned4wPipeline || st.skinnedHqPipeline || - st.skinned2wPipeline || st.skinned3wPipeline); - nvrhi::IBuffer* skBoneBuf = - drawWorldSkinned ? data.gpuCulling->GetGlobalBoneBuffer() : nullptr; + if (!data.gpuCulling) + return; + const u32 maxSlots = data.gpuCulling->GetLocalShadowCullSlotCount(); + const u32 maxTilesThisFrame = static_cast(tilesRef.size()); + u32 tilesDrawn = 0; + + struct LocalSkinDraw + { + nvrhi::IBuffer* vb = nullptr; + nvrhi::IBuffer* ib = nullptr; + u32 indexCount = 0; + u32 startIndex = 0; + s32 baseVertex = 0; + u32 vertexStride = 0; + u16 skinningRenderMode = 0; + u32 instanceIndex = 0; + u32 skinnedPoolFormat = UINT32_MAX; + s32 skinnedPoolBaseVertex = 0; + u32 skinnedPoolFirstIndex = 0; + bool pooled = false; + Fvector center{}; + }; + + static thread_local xr_vector s_skinInstances; + static thread_local xr_vector s_skinDraws; + auto& skinInstances = s_skinInstances; + auto& skinDraws = s_skinDraws; + skinInstances.clear(); + skinDraws.clear(); + nvrhi::IBuffer* skBoneBuf = nullptr; auto* skPsRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".ps"); - auto* skVsReflNonHQ = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned", ".vs"); - auto* skVsRefl4W = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_4w", ".vs"); - auto* skVsReflHQ = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_hq", ".vs"); - auto* skVsRefl2W = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_2w", ".vs"); - auto* skVsRefl3W = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_3w", ".vs"); - nvrhi::IBuffer* skDynCB = nullptr; - nvrhi::IBuffer* skMatCB = nullptr; - const bool worldSkinnedReady = drawWorldSkinned && skBoneBuf && skPsRefl; - constexpr u32 kSkinnedVCBVersions = 2048; - constexpr u32 kMaxSkinnedPerTile = 128; - u32 skinnedWritesLeft = kSkinnedVCBVersions; - if (worldSkinnedReady) + + bool anySkinTile = false; + if ((ps_r_skinned_shadows != 0) && data.worldSkinnedBatches && data.gpuCulling && skPsRefl) { - skDynCB = cache.GetOrCreateVolatileCB( - "LocalShadow", "WorldSkinDyn2", sizeof(DynamicTransforms), data.device, - kSkinnedVCBVersions); - skMatCB = cache.GetOrCreateVolatileCB( - "LocalShadow", "WorldSkinMat2", sizeof(SkinnedMaterialCB), data.device, - kSkinnedVCBVersions); + for (const LocalShadowTile& tile : tilesRef) + { + if (tile.needsRedraw && tile.L) + { + anySkinTile = true; + break; + } + } } - for (const LocalShadowTile& tile : data.tiles) + const bool wantWorldSkinned = + anySkinTile && (ps_r_skinned_shadows != 0) && data.worldSkinnedBatches && + data.gpuCulling && skPsRefl; + if (wantWorldSkinned) { - ZoneScopedN("LocalShadows::Tile"); - if (tile.slice >= ls.sliceCount) - continue; + skBoneBuf = data.gpuCulling->GetGlobalBoneBuffer(); + auto* bindlessLayout = backend ? backend->GetBindlessLayout() : nullptr; + nvrhi::FramebufferInfoEx skinFbInfo; + skinFbInfo.depthFormat = nvrhi::Format::D32; - ShadowCascadeCB cbData{}; - cbData.lightVP = tile.clipVP; - cmd->writeBuffer(st.cascadeCB, &cbData, sizeof(cbData)); + auto ensureLocalSkinPipe = [&](LocalSkinPipeVariant& var, const char* vsName, const char* cacheName, + const nvrhi::VertexAttributeDesc* baseAttribs, u32 baseAttrCount) { + if (var.pipeline || !st.ps) + return; + auto vsResult = shaderLoader->LoadVertexShader(vsName, "main"); + if (!vsResult.handle || !vsResult.reflection) + return; + var.vs = vsResult.handle; + var.layout = cache.GetOrCreateBindingLayoutFromReflection( + cacheName, *vsResult.reflection, *skPsRefl, nv); + if (!var.layout) + { + vsResult.reflection = nullptr; + return; + } + nvrhi::VertexAttributeDesc attribs[8]; + for (u32 i = 0; i < baseAttrCount; ++i) + attribs[i] = baseAttribs[i]; + attribs[baseAttrCount] = nvrhi::VertexAttributeDesc() + .setName("DRAWINDEX").setFormat(nvrhi::Format::R32_UINT) + .setBufferIndex(1).setOffset(0).setElementStride(4).setIsInstanced(true); + var.inputLayout = nv->createInputLayout(attribs, baseAttrCount + 1, var.vs); + nvrhi::GraphicsPipelineDesc d; + d.VS = var.vs; + d.PS = st.ps; + d.inputLayout = var.inputLayout; + d.primType = nvrhi::PrimitiveType::TriangleList; + d.bindingLayouts = {var.layout}; + if (bindlessLayout) + d.bindingLayouts.push_back(bindlessLayout); + d.renderState.depthStencilState.setDepthTestEnable(true); + d.renderState.depthStencilState.setDepthWriteEnable(true); + d.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + d.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + d.renderState.rasterState.depthBias = 0; + d.renderState.rasterState.slopeScaledDepthBias = 0.5f; + d.renderState.rasterState.depthBiasClamp = 0.001f; + var.pipeline = cache.GetOrCreatePipeline(cacheName, d, skinFbInfo, nv); + vsResult.reflection = nullptr; + }; + + { + constexpr u32 stride = 24; + nvrhi::VertexAttributeDesc a[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA16_SNORM).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(8).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(12).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG16_SNORM).setOffset(20).setElementStride(stride), + }; + ensureLocalSkinPipe(ls.skin1w, "shadow\\shadow_cascade_skinned_local", + "LocalShadowSkin1W", a, 5); + } + { + constexpr u32 stride = 36; + nvrhi::VertexAttributeDesc a[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(20).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), + }; + ensureLocalSkinPipe(ls.skinHq, "shadow\\shadow_cascade_skinned_hq_local", + "LocalShadowSkinHQ", a, 5); + } + { + constexpr u32 stride = 44; + nvrhi::VertexAttributeDesc a[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(20).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), + }; + ensureLocalSkinPipe(ls.skin2w, "shadow\\shadow_cascade_skinned_2w_local", + "LocalShadowSkin2W", a, 5); + ensureLocalSkinPipe(ls.skin3w, "shadow\\shadow_cascade_skinned_3w_local", + "LocalShadowSkin3W", a, 5); + } + { + constexpr u32 stride = 40; + nvrhi::VertexAttributeDesc a[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(20).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BLENDINDICES").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(36).setElementStride(stride), + }; + ensureLocalSkinPipe(ls.skin4w, "shadow\\shadow_cascade_skinned_4w_local", + "LocalShadowSkin4W", a, 6); + } + + if (skBoneBuf && (ls.skin1w.pipeline || ls.skinHq.pipeline || ls.skin2w.pipeline || + ls.skin3w.pipeline || ls.skin4w.pipeline)) + { + const bool mdiEnabled = data.gpuCulling->IsSkinnedMDIEnabled(); + skinInstances.reserve(data.worldSkinnedBatches->size()); + skinDraws.reserve(data.worldSkinnedBatches->size()); + for (const auto& batch : *data.worldSkinnedBatches) + { + if (!batch.isSkinned || !batch.vertexBuffer || !batch.indexBuffer) + continue; + CKinematics* parent = nullptr; + const u32 visualType = batch.visual ? batch.visual->getType() : 0; + if (visualType == MT_SKELETON_GEOMDEF_ST) + parent = static_cast(batch.visual)->GetParent(); + else if (visualType == MT_SKELETON_GEOMDEF_PM) + parent = static_cast(batch.visual)->GetParent(); + if (!parent) + continue; + + const u32 boneOffset = data.gpuCulling->GetOrUploadSkeleton(cmd, parent); + LocalSkinInstanceGPU inst{}; + inst.world = batch.worldMatrix; + inst.materialID = batch.bindlessMaterialID; + inst.boneOffset = boneOffset; + const u32 instanceIndex = static_cast(skinInstances.size()); + skinInstances.push_back(inst); + + const u32 variantIdx = + bindless::MaterialBuffer::Instance().GetShaderVariant(batch.bindlessMaterialID); + LocalSkinDraw draw{}; + draw.vb = batch.vertexBuffer; + draw.ib = batch.indexBuffer; + draw.indexCount = batch.indexCount; + draw.startIndex = batch.startIndex; + draw.baseVertex = batch.baseVertex; + draw.vertexStride = batch.vertexStride; + draw.skinningRenderMode = batch.skinningRenderMode; + draw.instanceIndex = instanceIndex; + draw.skinnedPoolFormat = batch.skinnedPoolFormat; + draw.skinnedPoolBaseVertex = batch.skinnedPoolBaseVertex; + draw.skinnedPoolFirstIndex = batch.skinnedPoolFirstIndex; + draw.pooled = mdiEnabled && variantIdx == 0 + && batch.skinnedPoolFormat >= SkinnedGeometryPools::FIRST_FORMAT + && batch.skinnedPoolFormat < SkinnedGeometryPools::FORMAT_COUNT; + draw.center = batch.worldMatrix.c; + skinDraws.push_back(draw); + } + + if (!skinInstances.empty()) + { + const u32 needed = static_cast(skinInstances.size()); + if (!ls.localSkinInstanceSSBO || ls.localSkinInstanceCapacity < needed) + { + const u32 capacity = std::max(needed, 256u); + nvrhi::BufferDesc ssboDesc; + ssboDesc.byteSize = u64(capacity) * sizeof(LocalSkinInstanceGPU); + ssboDesc.structStride = sizeof(LocalSkinInstanceGPU); + ssboDesc.canHaveUAVs = false; + ssboDesc.initialState = nvrhi::ResourceStates::ShaderResource; + ssboDesc.keepInitialState = true; + ssboDesc.debugName = "LocalSkinInstanceSSBO"; + ls.localSkinInstanceSSBO = nv->createBuffer(ssboDesc); + ls.localSkinInstanceCapacity = ls.localSkinInstanceSSBO ? capacity : 0; + ls.skin1w.bindingSet = nullptr; + ls.skinHq.bindingSet = nullptr; + ls.skin2w.bindingSet = nullptr; + ls.skin3w.bindingSet = nullptr; + ls.skin4w.bindingSet = nullptr; + if (ls.localSkinInstanceSSBO) + { + static bool s_ssboOnce = false; + if (!s_ssboOnce) + { + Msg("* [LocalShadow] LocalSkin SSBO ready (cap %u)", capacity); + s_ssboOnce = true; + } + } + } + if (ls.localSkinInstanceSSBO) + { + cmd->writeBuffer( + ls.localSkinInstanceSSBO, + skinInstances.data(), + u64(skinInstances.size()) * sizeof(LocalSkinInstanceGPU)); + } + } + } + } + const bool worldSkinnedReady = + !skinDraws.empty() && ls.localSkinInstanceSSBO && skBoneBuf; + + if (worldSkinnedReady) + { + auto ensureSkinBinding = [&](LocalSkinPipeVariant& var, const char* vsName) { + if (!var.pipeline || !var.layout || var.bindingSet) + return; + auto* vsReflSk = shaderLoader->GetCachedReflection(vsName, ".vs"); + if (!vsReflSk || !skPsRefl) + return; + BindingSetBuilder bsb(*vsReflSk, *skPsRefl, nv, "LocalShadow.WorldSkinSSBO"); + bsb.ConstantBuffer("ShadowCascadeCB", ls.cascadeCB); + bsb.BufferSRV("g_BoneMatrices", skBoneBuf); + bsb.BufferSRV("g_LocalSkinInstances", ls.localSkinInstanceSSBO); + BindBindlessMaterialTables(bsb); + var.bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), var.layout, nv); + }; + ensureSkinBinding(ls.skin1w, "shadow\\shadow_cascade_skinned_local"); + ensureSkinBinding(ls.skinHq, "shadow\\shadow_cascade_skinned_hq_local"); + ensureSkinBinding(ls.skin2w, "shadow\\shadow_cascade_skinned_2w_local"); + ensureSkinBinding(ls.skin3w, "shadow\\shadow_cascade_skinned_3w_local"); + ensureSkinBinding(ls.skin4w, "shadow\\shadow_cascade_skinned_4w_local"); + } + for (u32 page = 0; page < ls.sliceCount && page < MAX_LOCAL_SHADOW_PAGES; ++page) + { + if (ls.pageFramebuffers[page]) + continue; nvrhi::FramebufferDesc fbDesc; nvrhi::TextureSubresourceSet depthSub; - depthSub.baseArraySlice = tile.slice; + depthSub.baseArraySlice = page; depthSub.numArraySlices = 1; fbDesc.setDepthAttachment(atlasTex, depthSub); - auto fb = cache.GetOrCreateFramebuffer( - make_string("LocalShadow_%u_%u", cres, tile.slice).c_str(), fbDesc, nv); + ls.pageFramebuffers[page] = cache.GetOrCreateFramebuffer( + make_string("LocalShadowP%u_%u", page, cres).c_str(), fbDesc, nv); + } + + bool needTerrainUpload = false; + if (st.terrainPipeline && st.terrainLayout && data.bindlessConfig.HasTerrain()) + { + for (const LocalShadowTile& tile : tilesRef) + { + if (tile.needsRedraw && tile.size >= 128) + { + needTerrainUpload = true; + break; + } + } + } + if (needTerrainUpload) + { + if (auto* matCache = GEnv.Render ? GEnv.Render->GetMaterialCache() : nullptr) + matCache->FinalizePendingTerrainMaterials(ctx); + bindless::TerrainMaterialBuffer::Instance().Upload(ctx); + } + + struct SkinCand + { + const LocalSkinDraw* draw = nullptr; + LocalSkinPipeVariant* var = nullptr; + float dist2 = 0.f; + }; + struct SkinMdiGroup + { + LocalSkinPipeVariant* var = nullptr; + u32 format = 0; + u32 start = 0; + u32 count = 0; + }; + struct TileSkinMdi + { + u32 groupBegin = 0; + u32 groupCount = 0; + bool ready = false; + }; + static thread_local xr_vector s_skinCands; + static thread_local xr_vector s_skinPooled; + static thread_local xr_vector s_skinResidual; + static thread_local xr_vector s_frameSkinArgs; + static thread_local xr_vector s_frameSkinGroups; + static thread_local xr_vector s_tileSkinMdi; + static thread_local xr_vector> s_tileSkinResidual; + auto& skinCands = s_skinCands; + auto& skinPooled = s_skinPooled; + auto& skinResidual = s_skinResidual; + auto& frameSkinArgs = s_frameSkinArgs; + auto& frameSkinGroups = s_frameSkinGroups; + auto& tileSkinMdi = s_tileSkinMdi; + auto& tileSkinResidual = s_tileSkinResidual; + + auto pickVar = [&](const LocalSkinDraw& draw) -> LocalSkinPipeVariant* { + const u16 rm = draw.skinningRenderMode; + const u32 stride = draw.vertexStride; + LocalSkinPipeVariant* var = nullptr; + if (rm == 7 || rm == 8) var = &ls.skin3w; + else if (rm == 5 || rm == 6) var = &ls.skin2w; + else if (rm == 9 || rm == 10) var = &ls.skin4w; + else if (rm == 4 || rm == 2) var = &ls.skinHq; + else if (rm == 3 || rm == 1) var = &ls.skin1w; + else if (stride == 36) var = &ls.skinHq; + else if (stride == 40) var = &ls.skin4w; + else if (stride == 44) var = &ls.skin2w; + else var = &ls.skin1w; + if (!var || !var->pipeline || !var->layout || !var->inputLayout || !var->bindingSet) + return nullptr; + return var; + }; + + frameSkinArgs.clear(); + frameSkinGroups.clear(); + tileSkinMdi.assign(tilesRef.size(), {}); + tileSkinResidual.assign(tilesRef.size(), {}); + const bool skinMdiFrame = + worldSkinnedReady && ps_r_local_shadow_skinned_max > 0; + if (skinMdiFrame) + { + const u32 skinCap = static_cast(ps_r_local_shadow_skinned_max); + for (u32 tileIdx = 0; tileIdx < tilesRef.size() && tileIdx < maxSlots; ++tileIdx) + { + const LocalShadowTile& tile = tilesRef[tileIdx]; + if (!tile.needsRedraw || !tile.L || tile.size < 8 || + tile.page >= ls.sliceCount || + tile.posX + tile.size > cres || tile.posY + tile.size > cres) + continue; + + const float maxR = std::max(tile.L->range * 2.0f, 4.f); + const float maxR2 = maxR * maxR; + const Fvector& lp = tile.L->position; + skinCands.clear(); + for (const LocalSkinDraw& draw : skinDraws) + { + const float d2 = lp.distance_to_sqr(draw.center); + if (d2 > maxR2) + continue; + LocalSkinPipeVariant* var = pickVar(draw); + if (!var) + continue; + skinCands.push_back({&draw, var, d2}); + } + if (skinCands.size() > skinCap) + { + std::nth_element( + skinCands.begin(), + skinCands.begin() + skinCap, + skinCands.end(), + [](const SkinCand& a, const SkinCand& b) { return a.dist2 < b.dist2; }); + skinCands.resize(skinCap); + } + + skinPooled.clear(); + skinResidual.clear(); + for (const SkinCand& cand : skinCands) + { + if (cand.draw->pooled) + skinPooled.push_back(cand); + else + skinResidual.push_back(cand); + } + tileSkinResidual[tileIdx] = skinResidual; + + if (skinPooled.empty()) + continue; + + std::sort( + skinPooled.begin(), + skinPooled.end(), + [](const SkinCand& a, const SkinCand& b) { + if (a.draw->skinnedPoolFormat != b.draw->skinnedPoolFormat) + return a.draw->skinnedPoolFormat < b.draw->skinnedPoolFormat; + return a.var < b.var; + }); + + TileSkinMdi& tmdi = tileSkinMdi[tileIdx]; + tmdi.groupBegin = static_cast(frameSkinGroups.size()); + for (const SkinCand& cand : skinPooled) + { + const LocalSkinDraw& draw = *cand.draw; + if (frameSkinGroups.size() == tmdi.groupBegin || + frameSkinGroups.back().var != cand.var || + frameSkinGroups.back().format != draw.skinnedPoolFormat) + { + frameSkinGroups.push_back( + {cand.var, draw.skinnedPoolFormat, + static_cast(frameSkinArgs.size()), 0}); + } + IndirectDrawArgs args{}; + args.indexCountPerInstance = draw.indexCount; + args.instanceCount = 1; + args.startIndexLocation = draw.skinnedPoolFirstIndex; + args.baseVertexLocation = draw.skinnedPoolBaseVertex; + args.startInstanceLocation = draw.instanceIndex; + frameSkinArgs.push_back(args); + ++frameSkinGroups.back().count; + } + tmdi.groupCount = + static_cast(frameSkinGroups.size()) - tmdi.groupBegin; + } + + if (!frameSkinArgs.empty()) + { + const u32 needed = static_cast(frameSkinArgs.size()); + if (!ls.localSkinIndirectArgs || ls.localSkinIndirectArgsCapacity < needed) + { + const u32 capacity = std::max(needed, 4096u); + nvrhi::BufferDesc desc; + desc.byteSize = u64(capacity) * sizeof(IndirectDrawArgs); + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::IndirectArgument; + desc.keepInitialState = true; + desc.debugName = "LocalSkinIndirectArgs"; + ls.localSkinIndirectArgs = nv->createBuffer(desc); + ls.localSkinIndirectArgsCapacity = + ls.localSkinIndirectArgs ? capacity : 0; + } + if (ls.localSkinIndirectArgs && ls.localSkinIndirectArgsCapacity >= needed) + { + cmd->writeBuffer( + ls.localSkinIndirectArgs, + frameSkinArgs.data(), + u64(needed) * sizeof(IndirectDrawArgs)); + cmd->setBufferState( + ls.localSkinIndirectArgs, + nvrhi::ResourceStates::IndirectArgument); + for (TileSkinMdi& tmdi : tileSkinMdi) + tmdi.ready = tmdi.groupCount > 0; + } + } + } + + for (u32 tileIdx = 0; tileIdx < tilesRef.size(); ++tileIdx) + { + if (tilesDrawn >= maxTilesThisFrame) + break; + if (tileIdx >= maxSlots) + break; + const LocalShadowTile& tile = tilesRef[tileIdx]; + if (!tile.needsRedraw || tile.size < 8 || tile.page >= ls.sliceCount || + tile.posX + tile.size > cres || tile.posY + tile.size > cres) + continue; + + const float x0 = float(tile.posX); + const float y0 = float(tile.posY); + const float x1 = float(tile.posX + tile.size); + const float y1 = float(tile.posY + tile.size); + nvrhi::Viewport vp(x0, x1, y0, y1, 0.f, 1.f); + nvrhi::Rect scissor(tile.posX, tile.posX + tile.size, tile.posY, tile.posY + tile.size); + + auto fb = (tile.page < MAX_LOCAL_SHADOW_PAGES) ? ls.pageFramebuffers[tile.page] : nullptr; if (!fb) continue; - auto drawSet = [&](const BindlessDrawSet& set, nvrhi::IGraphicsPipeline* pipe) { - if (!pipe) + if (!ls.clearDepthPipeline) + continue; + + { + nvrhi::GraphicsState clearGs; + clearGs.pipeline = ls.clearDepthPipeline; + clearGs.framebuffer = fb; + clearGs.viewport.addViewport(vp); + clearGs.viewport.addScissorRect(scissor); + cmd->setGraphicsState(clearGs); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); + } + + ShadowCascadeCB cbData{}; + cbData.lightVP = tile.clipVP; + cmd->writeBuffer(ls.cascadeCB, &cbData, sizeof(cbData)); + + auto drawSet = [&](const BindlessDrawSet& set, nvrhi::IGraphicsPipeline* pipe, u32 setIdx) { + if (!pipe || !set.instanceBuffer || set.totalObjectCount == 0) + return; + + nvrhi::IBuffer* batchIndices = + data.gpuCulling->GetLocalShadowSlotIndices(tileIdx, setIdx); + nvrhi::IBuffer* materialIDs = + data.gpuCulling->GetLocalShadowSlotMats(tileIdx, setIdx); + nvrhi::IBuffer* drawArgs = + data.gpuCulling->GetLocalShadowSlotDrawArgs(tileIdx, setIdx); + nvrhi::IBuffer* countBuffer = + data.gpuCulling->GetLocalShadowSlotCount(tileIdx, setIdx); + if (!batchIndices || !materialIDs || !drawArgs || !countBuffer) return; - if (!set.IsCastAllValid() && !set.IsValid()) + + auto* cachedDraw = data.gpuCulling->MutLocalShadowSlotDrawBinding(tileIdx, setIdx); + auto* cachedSrc = data.gpuCulling->MutLocalShadowSlotDrawSrcInstance(tileIdx, setIdx); + if (!cachedDraw || !cachedSrc) return; - const bool castAll = set.IsCastAllValid(); - nvrhi::IBuffer* batchIndices = castAll - ? set.castAllBatchIndicesBuffer : set.compactBatchIndicesBuffer; - nvrhi::IBuffer* materialIDs = castAll - ? set.castAllMaterialIDBuffer : set.compactMaterialIDBuffer; - nvrhi::IBuffer* drawArgs = castAll - ? set.castAllDrawArgsBuffer : set.compactDrawArgsBuffer; - nvrhi::IBuffer* countBuffer = castAll - ? set.castAllCountBuffer : set.compactCountBuffer; - - BindingSetBuilder bsb(*vsRefl, *psRefl, nv, "LocalShadow"); - bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); - bsb.BufferSRV("g_InstanceData", set.instanceBuffer); - bsb.BufferSRV("g_CompactBatchIndices", batchIndices); - bsb.BufferSRV("g_CompactMaterialIDs", materialIDs); - auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), st.layout, nv); - if (!bindingSet) + + nvrhi::IBuffer* inst = set.instanceBuffer; + if (!*cachedDraw || *cachedSrc != inst) + { + BindingSetBuilder bsb(*vsRefl, *psRefl, nv, "LocalShadow"); + bsb.ConstantBuffer("ShadowCascadeCB", ls.cascadeCB); + BindBindlessMaterialTables(bsb); + bsb.BufferSRV("g_InstanceData", inst); + bsb.BufferSRV("g_CompactBatchIndices", batchIndices); + bsb.BufferSRV("g_CompactMaterialIDs", materialIDs); + *cachedDraw = cache.GetOrCreateBindingSet(bsb.Build(), st.layout, nv); + *cachedSrc = (*cachedDraw) ? inst : nullptr; + } + if (!*cachedDraw) return; nvrhi::GraphicsState gs; gs.pipeline = pipe; gs.framebuffer = fb; - gs.bindings = {bindingSet}; + gs.bindings = {*cachedDraw}; if (bindlessTable) gs.addBindingSet(bindlessTable); gs.vertexBuffers = { @@ -340,24 +1067,19 @@ LocalShadowOutputs setupLocalShadowPass( {drawIndexBuffer, 1, 0}}; gs.indexBuffer = {data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; gs.viewport.addViewport(vp); - gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.viewport.addScissorRect(scissor); gs.indirectParams = drawArgs; gs.indirectCountBuffer = countBuffer; cmd->setGraphicsState(gs); - DrawIndexedIndirectCountOrFallback(cmd, 0, 0, set.totalObjectCount); + DrawIndexedIndirectCountOrFallback( + cmd, 0, 0, GPUCullingManager::kLocalShadowCullSlotCap); }; - drawSet(data.bindlessConfig.staticSet, opaquePipe); - drawSet(data.bindlessConfig.dynamicSet, opaquePipe); - if (foliagePipe) - drawSet(data.bindlessConfig.transparentCasterSet, foliagePipe); + drawSet(data.bindlessConfig.staticSet, opaquePipe, 0); - if (st.terrainPipeline && st.terrainLayout && data.bindlessConfig.HasTerrain()) + if (tile.size >= 128 && st.terrainPipeline && st.terrainLayout && data.bindlessConfig.HasTerrain()) { - if (auto* matCache = GEnv.Render ? GEnv.Render->GetMaterialCache() : nullptr) - matCache->FinalizePendingTerrainMaterials(ctx); auto& terrainMatBuffer = bindless::TerrainMaterialBuffer::Instance(); - terrainMatBuffer.Upload(ctx); auto* terrainPsRefl = shaderLoader->GetCachedReflection( "shadow\\shadow_cascade_terrain", ".ps"); const bool useCompact = data.bindlessConfig.UseTerrainCompaction(); @@ -377,7 +1099,8 @@ LocalShadowOutputs setupLocalShadowPass( if (terrainPsRefl && terrainMatBuffer.GetBuffer() && tInstance && tBatch && tMat && tArgs) { BindingSetBuilder tbsb(*vsRefl, *terrainPsRefl, nv, "LocalShadow.Terrain"); - tbsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + tbsb.ConstantBuffer("ShadowCascadeCB", ls.cascadeCB); + BindBindlessMaterialTables(tbsb); tbsb.BufferSRV("g_InstanceData", tInstance); tbsb.BufferSRV("g_CompactBatchIndices", tBatch); tbsb.BufferSRV("g_CompactMaterialIDs", tMat); @@ -396,7 +1119,7 @@ LocalShadowOutputs setupLocalShadowPass( gs.indexBuffer = { data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; gs.viewport.addViewport(vp); - gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.viewport.addScissorRect(scissor); gs.indirectParams = tArgs; if (tCount) gs.indirectCountBuffer = tCount; @@ -407,107 +1130,196 @@ LocalShadowOutputs setupLocalShadowPass( } } - if (worldSkinnedReady && skDynCB && skMatCB && skinnedWritesLeft > 0 && tile.L) + drawSet(data.bindlessConfig.dynamicSet, opaquePipe, 1); + if (foliagePipe) + drawSet(data.bindlessConfig.transparentCasterSet, foliagePipe, 2); + + if (skinMdiFrame && tile.L && tileIdx < tileSkinMdi.size()) { - // Cast beyond light range a bit so NPC/body shadows reach across the floor. - const float maxR = std::max(tile.L->range * 2.0f, 4.f); - const float maxR2 = maxR * maxR; - const Fvector& lp = tile.L->position; - u32 drawnThisTile = 0; + const TileSkinMdi& tmdi = tileSkinMdi[tileIdx]; + if (tmdi.ready && ls.localSkinIndirectArgs) + { + auto& pools = data.gpuCulling->GetSkinnedPools(); + const u32 groupEnd = tmdi.groupBegin + tmdi.groupCount; + for (u32 gi = tmdi.groupBegin; gi < groupEnd && gi < frameSkinGroups.size(); ++gi) + { + const SkinMdiGroup& g = frameSkinGroups[gi]; + if (!g.count || !g.var) + continue; + nvrhi::IBuffer* poolVB = pools.GetVertexBuffer(g.format); + nvrhi::IBuffer* poolIB = pools.GetIndexBuffer(g.format); + if (!poolVB || !poolIB) + continue; - for (const auto& batch : *data.worldSkinnedBatches) + nvrhi::GraphicsState gs; + gs.pipeline = g.var->pipeline; + gs.framebuffer = fb; + gs.bindings = {g.var->bindingSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {poolVB, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = {poolIB, nvrhi::Format::R16_UINT, 0}; + gs.viewport.addViewport(vp); + gs.viewport.addScissorRect(scissor); + gs.indirectParams = ls.localSkinIndirectArgs; + cmd->setGraphicsState(gs); + cmd->drawIndexedIndirect( + g.start * sizeof(IndirectDrawArgs), g.count); + } + } + + if (tileIdx < tileSkinResidual.size() && !tileSkinResidual[tileIdx].empty()) { - if (skinnedWritesLeft == 0 || drawnThisTile >= kMaxSkinnedPerTile) - break; - if (!batch.isSkinned || !batch.vertexBuffer || !batch.indexBuffer) - continue; - if (lp.distance_to_sqr(batch.worldMatrix.c) > maxR2) - continue; + auto& residual = tileSkinResidual[tileIdx]; + std::sort( + residual.begin(), + residual.end(), + [](const SkinCand& a, const SkinCand& b) { + if (a.var != b.var) + return a.var < b.var; + if (a.draw->vb != b.draw->vb) + return a.draw->vb < b.draw->vb; + return a.draw->ib < b.draw->ib; + }); - CKinematics* parent = nullptr; - const u32 visualType = batch.visual ? batch.visual->getType() : 0; - if (visualType == MT_SKELETON_GEOMDEF_ST) - parent = static_cast(batch.visual)->GetParent(); - else if (visualType == MT_SKELETON_GEOMDEF_PM) - parent = static_cast(batch.visual)->GetParent(); - if (!parent) - continue; + LocalSkinPipeVariant* curVar = nullptr; + nvrhi::IBuffer* curVb = nullptr; + nvrhi::IBuffer* curIb = nullptr; + for (const SkinCand& cand : residual) + { + const LocalSkinDraw& draw = *cand.draw; + if (cand.var != curVar || draw.vb != curVb || draw.ib != curIb) + { + curVar = cand.var; + curVb = draw.vb; + curIb = draw.ib; + nvrhi::GraphicsState gs; + gs.pipeline = curVar->pipeline; + gs.framebuffer = fb; + gs.bindings = {curVar->bindingSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {draw.vb, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = {draw.ib, nvrhi::Format::R16_UINT, 0}; + gs.viewport.addViewport(vp); + gs.viewport.addScissorRect(scissor); + cmd->setGraphicsState(gs); + } + cmd->drawIndexed( + nvrhi::DrawArguments() + .setVertexCount(draw.indexCount) + .setInstanceCount(1) + .setStartIndexLocation(draw.startIndex) + .setStartVertexLocation(draw.baseVertex) + .setStartInstanceLocation(draw.instanceIndex)); + } + } + } - const u16 rm = batch.skinningRenderMode; - const u32 stride = batch.vertexStride; - nvrhi::IGraphicsPipeline* pipe = nullptr; - nvrhi::IBindingLayout* layout = nullptr; - nvrhi::IInputLayout* il = nullptr; - auto* vsReflSk = skVsReflNonHQ; - auto pick3w = [&] { - pipe = st.skinned3wPipeline.Get(); layout = st.skinned3wLayout.Get(); - il = st.skinned3wInputLayout.Get(); vsReflSk = skVsRefl3W; - }; - auto pick2w = [&] { - pipe = st.skinned2wPipeline.Get(); layout = st.skinned2wLayout.Get(); - il = st.skinned2wInputLayout.Get(); vsReflSk = skVsRefl2W; - }; - auto pick4w = [&] { - pipe = st.skinned4wPipeline.Get(); layout = st.skinned4wLayout.Get(); - il = st.skinned4wInputLayout.Get(); vsReflSk = skVsRefl4W; - }; - auto pickHq = [&] { - pipe = st.skinnedHqPipeline.Get(); layout = st.skinnedHqLayout.Get(); - il = st.skinnedHqInputLayout.Get(); vsReflSk = skVsReflHQ; - }; - auto pickNonHq = [&] { - pipe = st.skinnedPipeline.Get(); layout = st.skinnedLayout.Get(); - il = st.skinnedInputLayout.Get(); vsReflSk = skVsReflNonHQ; - }; - if (rm == 7 || rm == 8) pick3w(); - else if (rm == 5 || rm == 6) pick2w(); - else if (rm == 9 || rm == 10) pick4w(); - else if (rm == 4 || rm == 2) pickHq(); - else if (rm == 3 || rm == 1) pickNonHq(); - else if (stride == 36) pickHq(); - else if (stride == 40) pick4w(); - else if (stride == 44) pick2w(); - else pickNonHq(); - if (!pipe || !layout || !il || !vsReflSk) - continue; + ++tilesDrawn; + } - const u32 boneOffset = data.gpuCulling->GetOrUploadSkeleton(cmd, parent); - DynamicTransforms dyn{}; - FillDynamicTransforms(dyn, batch.worldMatrix); - cmd->writeBuffer(skDynCB, &dyn, sizeof(dyn)); - SkinnedMaterialCB matId{}; - matId.materialID = batch.bindlessMaterialID; - matId.skeletonBoneOffset = boneOffset; - cmd->writeBuffer(skMatCB, &matId, sizeof(matId)); - - BindingSetBuilder bsb(*vsReflSk, *skPsRefl, nv, "LocalShadow.WorldSkin"); - bsb.ConstantBuffer("dynamic_transforms", skDynCB); - bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); - bsb.ConstantBuffer("SkinnedMaterialCB", skMatCB); - bsb.BufferSRV("g_BoneMatrices", skBoneBuf); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); - auto set = cache.GetOrCreateBindingSet(bsb.Build(), layout, nv); - if (!set) + if (ps_r_local_shadow_filter != 0 && ls.esmConvertPipeline && ls.esmAtlasTex && + ls.esmCB && ls.esmConvertLayout && atlasTex) + { + struct EsmCBData + { + u32 page, x0, y0, size; + float k; + float pad0, pad1, pad2; + }; + struct EsmBlurCB + { + u32 page, x0, y0, size; + u32 horizontal; + u32 pad0, pad1, pad2; + }; + + auto* convRefl = shaderLoader->GetCachedReflection("local_shadow_esm_convert", ".cs"); + auto* blurRefl = shaderLoader->GetCachedReflection("local_shadow_esm_blur", ".cs"); + if (convRefl) + { + for (u32 tileIdx = 0; tileIdx < tilesRef.size(); ++tileIdx) + { + const LocalShadowTile& tile = tilesRef[tileIdx]; + if (!tile.needsRedraw || tile.size < 8 || tile.page >= ls.sliceCount || + tile.posX + tile.size > cres || tile.posY + tile.size > cres) continue; - nvrhi::GraphicsState gs; - gs.pipeline = pipe; - gs.framebuffer = fb; - gs.bindings = {set}; - if (bindlessTable) - gs.addBindingSet(bindlessTable); - gs.vertexBuffers = {{batch.vertexBuffer, 0, 0}}; - gs.indexBuffer = {batch.indexBuffer, nvrhi::Format::R16_UINT, 0}; - gs.viewport.addViewport(vp); - gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); - cmd->setGraphicsState(gs); - cmd->drawIndexed( - nvrhi::DrawArguments() - .setVertexCount(batch.indexCount) - .setStartIndexLocation(batch.startIndex) - .setStartVertexLocation(batch.baseVertex)); - ++drawnThisTile; - --skinnedWritesLeft; + EsmCBData conv{}; + conv.page = tile.page; + conv.x0 = tile.posX; + conv.y0 = tile.posY; + conv.size = tile.size; + conv.k = 80.f; + cmd->writeBuffer(ls.esmCB, &conv, sizeof(conv)); + + BindingSetBuilder cbsb(*convRefl, nv, "LocalShadowEsmConvert"); + cbsb.ConstantBuffer("LocalShadowEsmCB", ls.esmCB); + cbsb.Texture("g_DepthAtlas", atlasTex, nvrhi::Format::R32_FLOAT); + cbsb.TextureUAV("g_EsmAtlas", ls.esmAtlasTex); + auto convSet = cache.GetOrCreateBindingSet(cbsb.Build(), ls.esmConvertLayout, nv); + if (!convSet) + { + static bool s_once = false; + if (!s_once) + { + Msg("! [LocalShadow] ESM convert binding set failed"); + s_once = true; + } + break; + } + + nvrhi::ComputeState cs; + cs.pipeline = ls.esmConvertPipeline; + cs.bindings = {convSet}; + cmd->setComputeState(cs); + cmd->dispatch((tile.size + 7) / 8, (tile.size + 7) / 8, 1); + + if (ls.esmBlurPipeline && ls.esmBlurLayout && ls.esmTempTex && blurRefl) + { + EsmBlurCB blurH{}; + blurH.page = tile.page; + blurH.x0 = tile.posX; + blurH.y0 = tile.posY; + blurH.size = tile.size; + blurH.horizontal = 1; + cmd->writeBuffer(ls.esmCB, &blurH, sizeof(blurH)); + BindingSetBuilder hbsb(*blurRefl, nv, "LocalShadowEsmBlurH"); + hbsb.ConstantBuffer("LocalShadowEsmBlurCB", ls.esmCB); + hbsb.Texture("g_Input", ls.esmAtlasTex); + hbsb.TextureUAV("g_Output", ls.esmTempTex); + auto hSet = cache.GetOrCreateBindingSet(hbsb.Build(), ls.esmBlurLayout, nv); + if (hSet) + { + nvrhi::ComputeState bcs; + bcs.pipeline = ls.esmBlurPipeline; + bcs.bindings = {hSet}; + cmd->setComputeState(bcs); + cmd->dispatch((tile.size + 7) / 8, (tile.size + 7) / 8, 1); + } + + EsmBlurCB blurV = blurH; + blurV.horizontal = 0; + cmd->writeBuffer(ls.esmCB, &blurV, sizeof(blurV)); + BindingSetBuilder vbsb(*blurRefl, nv, "LocalShadowEsmBlurV"); + vbsb.ConstantBuffer("LocalShadowEsmBlurCB", ls.esmCB); + vbsb.Texture("g_Input", ls.esmTempTex); + vbsb.TextureUAV("g_Output", ls.esmAtlasTex); + auto vSet = cache.GetOrCreateBindingSet(vbsb.Build(), ls.esmBlurLayout, nv); + if (vSet) + { + nvrhi::ComputeState bcs; + bcs.pipeline = ls.esmBlurPipeline; + bcs.bindings = {vSet}; + cmd->setComputeState(bcs); + cmd->dispatch((tile.size + 7) / 8, (tile.size + 7) / 8, 1); + } + } } } } @@ -515,6 +1327,7 @@ LocalShadowOutputs setupLocalShadowPass( outputs.atlas = passData.atlas; outputs.atlasTex = state.atlas; + outputs.esmTex = state.esmConvertPipeline ? state.esmAtlasTex : nullptr; outputs.valid = true; return outputs; } diff --git a/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.h index 4b4b625daa1..48c548b13ff 100644 --- a/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.h @@ -23,13 +23,56 @@ class GPUCullingManager; namespace xray::render::fg::passes { +struct alignas(16) LocalSkinInstanceGPU +{ + Fmatrix world; + u32 materialID = 0; + u32 boneOffset = 0; + u32 pad0 = 0; + u32 pad1 = 0; +}; +static_assert(sizeof(LocalSkinInstanceGPU) == 80); + +struct LocalSkinPipeVariant +{ + nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::BindingLayoutHandle layout; + nvrhi::InputLayoutHandle inputLayout; + nvrhi::ShaderHandle vs; + nvrhi::BindingSetHandle bindingSet; +}; + struct LocalShadowPassState { xray::render::fg::TextureHandle atlasHandle; nvrhi::ITexture* atlas = nullptr; - // Low slope-bias pipelines (CSM's 2.75 slope eats thin furniture/NPC contact shadows). + nvrhi::TextureHandle staticCache; + nvrhi::ITexture* staticCacheTex = nullptr; + nvrhi::TextureHandle esmAtlas; + nvrhi::ITexture* esmAtlasTex = nullptr; + nvrhi::TextureHandle esmTemp; + nvrhi::ITexture* esmTempTex = nullptr; + nvrhi::BufferHandle cascadeCB; + nvrhi::BufferHandle esmCB; + u32 cascadeCBMaxVersions = 0; nvrhi::GraphicsPipelineHandle opaquePipeline; nvrhi::GraphicsPipelineHandle foliagePipeline; + nvrhi::ComputePipelineHandle esmConvertPipeline; + nvrhi::ComputePipelineHandle esmBlurPipeline; + nvrhi::BindingLayoutHandle esmConvertLayout; + nvrhi::BindingLayoutHandle esmBlurLayout; + LocalSkinPipeVariant skin1w; + LocalSkinPipeVariant skinHq; + LocalSkinPipeVariant skin2w; + LocalSkinPipeVariant skin3w; + LocalSkinPipeVariant skin4w; + nvrhi::BufferHandle localSkinInstanceSSBO; + u32 localSkinInstanceCapacity = 0; + nvrhi::BufferHandle localSkinIndirectArgs; + u32 localSkinIndirectArgsCapacity = 0; + nvrhi::BufferHandle cullDoneMarker; + nvrhi::FramebufferHandle pageFramebuffers[MAX_LOCAL_SHADOW_PAGES]; + nvrhi::GraphicsPipelineHandle clearDepthPipeline; u32 resolution = 0; u32 sliceCount = 0; bool initialized = false; @@ -40,6 +83,7 @@ struct LocalShadowOutputs { framegraph::VirtualResourceHandle atlas; nvrhi::ITexture* atlasTex = nullptr; + nvrhi::ITexture* esmTex = nullptr; bool valid = false; }; @@ -53,6 +97,7 @@ LocalShadowOutputs setupLocalShadowPass( ShadowPassState& shadowState, LocalShadowPassState& state, const xr_vector* worldSkinnedBatches = nullptr, - fg::GPUCullingManager* gpuCulling = nullptr); + fg::GPUCullingManager* gpuCulling = nullptr, + framegraph::VirtualResourceHandle gpuCullDrawArgs = {}); } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/LodPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/LodPassSetup.cpp new file mode 100644 index 00000000000..aaaee9efa2a --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/LodPassSetup.cpp @@ -0,0 +1,335 @@ +#include "stdafx.h" +#include "LodPassSetup.h" +#include "ShaderConstants.h" +#include "Layers/xrRender/FLOD.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/ResourceManager/FGResourceManager.h" +#include "Layers/xrRender/ResourceManager/TextureManager.h" +#include "Layers/xrRender/TextureDescrManager.h" + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +#pragma pack(push, 1) +struct LodVertex +{ + Fvector p0, p1, n0, n1; + u32 sun_af; + u32 rgbh0, rgbh1; + Fvector2 t0, t1; +}; +#pragma pack(pop) + +void EnsureLodPipeline(nvrhi::IDevice* nv, LodPassState& st) +{ + if (st.initialized) + return; + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + { + st.initialized = true; + return; + } + auto vs = loader->LoadVertexShader("bindless_lod"); + auto ps = loader->LoadPixelShader("bindless_lod"); + if (!vs.handle || !ps.handle) + { + Msg("! [LodPass] bindless_lod shaders missing"); + st.initialized = true; + return; + } + st.vs = vs.handle; + st.ps = ps.handle; + auto& cache = GetPassResourceCache(); + st.layout = cache.GetOrCreateBindingLayoutFromReflection("LodPass_v2", *vs.reflection, *ps.reflection, nv); + + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGB32_FLOAT) + .setArraySize(2).setOffset(offsetof(LodVertex, p0)).setElementStride(sizeof(LodVertex)), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::RGB32_FLOAT) + .setArraySize(2).setOffset(offsetof(LodVertex, n0)).setElementStride(sizeof(LodVertex)), + nvrhi::VertexAttributeDesc().setName("COLOR").setFormat(nvrhi::Format::RGBA8_UNORM) + .setArraySize(3).setOffset(offsetof(LodVertex, sun_af)).setElementStride(sizeof(LodVertex)), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT) + .setArraySize(2).setOffset(offsetof(LodVertex, t0)).setElementStride(sizeof(LodVertex)), + }; + st.inputLayout = cache.GetOrCreateInputLayout("LodPass", attribs, std::size(attribs), st.vs, nv); + + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(st.vs); + desc.setPixelShader(st.ps); + desc.setInputLayout(st.inputLayout); + desc.addBindingLayout(st.layout); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.depthStencilState.setDepthTestEnable(true); + desc.renderState.depthStencilState.setDepthWriteEnable(true); + desc.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + desc.renderState.blendState.targets[0].setBlendEnable(false); + + nvrhi::FramebufferInfoEx fb; + fb.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fb.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fb.colorFormats.push_back(nvrhi::Format::RGBA8_UNORM); + fb.colorFormats.push_back(nvrhi::Format::RGBA32_FLOAT); + fb.depthFormat = nvrhi::Format::D32; + st.pipeline = cache.GetOrCreatePipeline("LodPass_v2", desc, fb, nv); + st.initialized = true; +} + +void FillFacet(LodVertex* V, const FLOD::_face& F, float factor) +{ + for (int i = 0; i < 4; ++i) + { + V[i].p0 = F.v[i].v; + V[i].p1 = F.v[i].v; + V[i].n0 = F.N; + V[i].n1 = F.N; + V[i].t0 = F.v[i].t; + V[i].t1 = F.v[i].t; + V[i].rgbh0 = F.v[i].c_rgb_hemi; + V[i].rgbh1 = F.v[i].c_rgb_hemi; + u32 sun = F.v[i].c_sun; + V[i].sun_af = color_rgba(sun, sun, 255, u8(clampr(iFloor(factor * 255.f), 0, 255))); + } +} +} // namespace + +DefaultOutputLayout setupLodPass( + FrameGraph& fg, + fg::RenderDevice* device, + const DefaultOutputLayout& inputs, + const xr_vector* impostors, + u32 width, + u32 height, + LodPassState& state) +{ + if (!device || !device->GetNVRHIDevice() || !impostors || impostors->empty()) + return inputs; + + EnsureLodPipeline(device->GetNVRHIDevice(), state); + if (!state.pipeline || !state.layout) + return inputs; + + struct PassData + { + VirtualResourceHandle color, normal, baseColor, worldPos, depth; + DefaultOutputLayout outputs; + const xr_vector* impostors = nullptr; + LodPassState* st = nullptr; + fg::RenderDevice* device = nullptr; + u32 width = 0, height = 0; + }; + + auto& pd = fg.addCallbackPass( + "LodImpostors", + [&](FrameGraph& b, PassHandle ph, PassData& data) { + RenderPassBuilder pb(b, ph); + data.impostors = impostors; + data.st = &state; + data.device = device; + data.width = width; + data.height = height; + data.color = pb.readWrite(inputs.albedo, ResourceState::RenderTarget); + data.normal = pb.readWrite(inputs.normal, ResourceState::RenderTarget); + if (inputs.baseColor.is_valid()) + data.baseColor = pb.readWrite(inputs.baseColor, ResourceState::RenderTarget); + if (inputs.worldPos.is_valid()) + data.worldPos = pb.readWrite(inputs.worldPos, ResourceState::RenderTarget); + data.depth = pb.readWrite(inputs.depth, ResourceState::DepthStencilWrite); + data.outputs.albedo = data.color; + data.outputs.normal = data.normal; + data.outputs.baseColor = data.baseColor; + data.outputs.worldPos = data.worldPos; + data.outputs.depth = data.depth; + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd ? cmd->getDevice() : nullptr; + if (!cmd || !nv || !data.st || !data.impostors) + return; + + auto* colorRT = graph.GetPhysicalTexture(data.color); + auto* normalRT = graph.GetPhysicalTexture(data.normal); + auto* baseRT = data.baseColor.is_valid() ? graph.GetPhysicalTexture(data.baseColor) : nullptr; + auto* worldRT = data.worldPos.is_valid() ? graph.GetPhysicalTexture(data.worldPos) : nullptr; + auto* depthRT = graph.GetPhysicalTexture(data.depth); + if (!colorRT || !depthRT) + return; + + const u32 maxVerts = u32(data.impostors->size()) * 4; + if (!data.st->dynamicVB || data.st->dynamicVBCapacity < maxVerts) + { + nvrhi::BufferDesc bd; + bd.byteSize = maxVerts * sizeof(LodVertex); + bd.debugName = "LodPass_DynamicVB"; + bd.isVertexBuffer = true; + bd.initialState = nvrhi::ResourceStates::VertexBuffer; + bd.keepInitialState = true; + data.st->dynamicVB = nv->createBuffer(bd); + data.st->dynamicVBCapacity = maxVerts; + } + + xr_vector verts; + verts.reserve(maxVerts); + xr_vector drawCounts; + drawCounts.reserve(data.impostors->size()); + + for (const auto& inst : *data.impostors) + { + if (!inst.lod) + continue; + Fvector Ldir; + Fvector center; + inst.world.transform_tiny(center, inst.lod->vis.sphere.P); + Ldir.sub(center, Device.vCameraPosition); + Ldir.normalize_safe(); + + int best_id = 0; + float best_dot = Ldir.dotproduct(inst.lod->facets[0].N); + for (int i = 1; i < 8; ++i) + { + float d = Ldir.dotproduct(inst.lod->facets[i].N); + if (d > best_dot) { best_id = i; best_dot = d; } + } + + LodVertex V[4]; + FillFacet(V, inst.lod->facets[best_id], 0.f); + for (int i = 0; i < 4; ++i) + { + inst.world.transform_tiny(V[i].p0, V[i].p0); + V[i].p1 = V[i].p0; + inst.world.transform_dir(V[i].n0, V[i].n0); + V[i].n1 = V[i].n0; + verts.push_back(V[i]); + } + drawCounts.push_back(1); + } + + if (verts.empty()) + return; + + cmd->writeBuffer(data.st->dynamicVB, verts.data(), verts.size() * sizeof(LodVertex)); + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(colorRT); + if (normalRT) fbDesc.addColorAttachment(normalRT); + if (baseRT) fbDesc.addColorAttachment(baseRT); + if (worldRT) fbDesc.addColorAttachment(worldRT); + fbDesc.setDepthAttachment(depthRT); + auto& cache = GetPassResourceCache(); + auto fb = cache.GetOrCreateFramebuffer("LodPass", fbDesc, nv); + if (!fb) + return; + + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsR = loader->GetCachedReflection("bindless_lod", ".vs"); + auto* psR = loader->GetCachedReflection("bindless_lod", ".ps"); + if (!vsR || !psR) + return; + + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + { + StaticGlobals sg = BuildStaticGlobals(); + cmd->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + + nvrhi::ITexture* baseTex = cache.GetDummyContactHistory(nv); + nvrhi::ITexture* hemiTex = baseTex; + nvrhi::ITexture* pbrTex = baseTex; + auto* resMgr = data.device->GetFGResourceManager(); + auto* texMgr = resMgr ? resMgr->GetTextureManager() : nullptr; + + u32 vertOffset = 0; + u32 instIdx = 0; + for (const auto& inst : *data.impostors) + { + if (!inst.lod) + continue; + + if (texMgr && inst.lod->textureName.size()) + { + auto h = texMgr->LoadTexture(inst.lod->textureName.c_str()); + if (auto* t = texMgr->GetNVRHITexture(h)) + baseTex = t; + xr_string hemiName = inst.lod->textureName.c_str(); + hemiName += "_nm"; + auto hh = texMgr->LoadTexture(hemiName.c_str()); + if (auto* t = texMgr->GetNVRHITexture(hh)) + hemiTex = t; + else + hemiTex = baseTex; + + pbrTex = baseTex; + shared_str pbrName = TextureDescr.GetPBRName(inst.lod->textureName); + if (pbrName.size()) + { + auto hp = texMgr->LoadTexture(pbrName.c_str()); + if (auto* t = texMgr->GetNVRHITexture(hp)) + pbrTex = t; + } + else + { + xr_string fallback = inst.lod->textureName.c_str(); + fallback += "_pbr"; + auto hp = texMgr->LoadTexture(fallback.c_str()); + if (auto* t = texMgr->GetNVRHITexture(hp)) + pbrTex = t; + } + } + + BindingSetBuilder bsb(*vsR, *psR, nv, "LodPass"); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.Texture("s_base", baseTex); + bsb.Texture("s_hemi", hemiTex); + bsb.Texture("s_pbr", pbrTex); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->layout, nv); + if (!set) + { + vertOffset += 4; + ++instIdx; + continue; + } + + static const u16 kQuadIdx[6] = {0, 1, 2, 0, 2, 3}; + if (!data.st->quadIB) + { + nvrhi::BufferDesc ibDesc; + ibDesc.byteSize = sizeof(kQuadIdx); + ibDesc.debugName = "LodPass_QuadIB"; + ibDesc.isIndexBuffer = true; + ibDesc.initialState = nvrhi::ResourceStates::IndexBuffer; + ibDesc.keepInitialState = true; + data.st->quadIB = nv->createBuffer(ibDesc); + cmd->writeBuffer(data.st->quadIB, kQuadIdx, sizeof(kQuadIdx)); + } + + nvrhi::GraphicsState gs; + gs.pipeline = data.st->pipeline; + gs.framebuffer = fb; + gs.bindings = {set}; + gs.vertexBuffers = {{data.st->dynamicVB, 0, vertOffset * sizeof(LodVertex)}}; + gs.indexBuffer = {data.st->quadIB, nvrhi::Format::R16_UINT, 0}; + gs.viewport.addViewportAndScissorRect(nvrhi::Viewport(float(data.width), float(data.height))); + cmd->setGraphicsState(gs); + cmd->drawIndexed(nvrhi::DrawArguments{6, 1, 0, 0, 0}); + + vertOffset += 4; + ++instIdx; + } + }); + + return pd.outputs; +} + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/LodPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/LodPassSetup.h new file mode 100644 index 00000000000..e1477db43ab --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/LodPassSetup.h @@ -0,0 +1,45 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include "Layers/xrRender/FrameGraph/IPass.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { +class RenderDevice; +class FLOD; +} + +namespace xray::render::fg::passes +{ + +struct LodImpostorInstance +{ + FLOD* lod = nullptr; + Fmatrix world; +}; + +struct LodPassState +{ + nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::BindingLayoutHandle layout; + nvrhi::InputLayoutHandle inputLayout; + nvrhi::ShaderHandle vs; + nvrhi::ShaderHandle ps; + nvrhi::BufferHandle dynamicVB; + nvrhi::BufferHandle quadIB; + u32 dynamicVBCapacity = 0; + bool initialized = false; +}; + +framegraph::DefaultOutputLayout setupLodPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + const framegraph::DefaultOutputLayout& inputs, + const xr_vector* impostors, + u32 width, + u32 height, + LodPassState& state); + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/MotionVectorPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/MotionVectorPassSetup.cpp index 730c0491b5a..bb0cbb5b8b0 100644 --- a/src/Layers/xrRender/FrameGraphPasses/MotionVectorPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/MotionVectorPassSetup.cpp @@ -8,48 +8,60 @@ #include "Layers/xrRender/RenderContext/RenderDevice.h" #include "Layers/xrRender/FrameGraph/ShaderLoader.h" -namespace fg -{ - extern xray::render::FrameGraphRenderer RImplementation; -} - namespace xray::render::fg::passes { using namespace framegraph; +namespace +{ +constexpr u32 kMotionVectorPipeVersion = 6; +} + static void InitializeResources(fg::RenderDevice* device, MotionVectorPassState& state) { - if (state.initialized) return; + if (state.initialized && state.pipeVersion == kMotionVectorPipeVersion && state.pipeline) + return; + + state.initialized = false; + state.pipeVersion = 0; + state.pipeline = nullptr; + state.layout = nullptr; + state.cb = nullptr; auto& cache = GetPassResourceCache(); nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); + BindingSetBuilder::InvalidateReflectionCache(); auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_motion_vectors"); - if (!csResult.handle) return; + if (!csResult.handle || !csResult.reflection) + return; - state.layout = cache.GetOrCreateBindingLayoutFromReflection("MotionVector", *csResult.reflection, nvDevice); + state.layout = cache.GetOrCreateBindingLayoutFromReflection( + "MotionVector_v6", *csResult.reflection, nvDevice); nvrhi::ComputePipelineDesc pipeDesc; pipeDesc.CS = csResult.handle; pipeDesc.bindingLayouts = { state.layout }; - state.pipeline = cache.GetOrCreateComputePipeline("MotionVector", pipeDesc, nvDevice); + state.pipeline = cache.GetOrCreateComputePipeline("MotionVector_v6", pipeDesc, nvDevice); - state.cb = cache.GetOrCreateVolatileCB("MotionVector", "MotionVectorCB", 160, device); + state.cb = cache.GetOrCreateVolatileCB("MotionVector", "MotionVectorCB_v6", 256, device); state.initialized = true; + state.pipeVersion = kMotionVectorPipeVersion; } MotionVectorOutput setupMotionVectorPass( FrameGraph& fg, fg::RenderDevice* device, VirtualResourceHandle depthInput, - const Fmatrix& invViewProj, + VirtualResourceHandle worldPosInput, + const Fmatrix& viewProj, const Fmatrix& prevViewProj, u32 width, u32 height, MotionVectorPassState& state) { InitializeResources(device, state); - if (!state.pipeline) + if (!state.pipeline || !depthInput.is_valid() || !worldPosInput.is_valid()) return {}; ResourceDesc mvDesc; @@ -62,14 +74,24 @@ MotionVectorOutput setupMotionVectorPass( mvDesc.isTransient = true; auto mvHandle = fg.CreateTexture("rt_MotionVectors", mvDesc); + const Fvector cameraPos = Device.vCameraPosition; + const bool hasPrevCamera = state.hasPrevCamera; + const Fvector prevCameraPos = state.prevCameraPos; + state.prevCameraPos = cameraPos; + state.hasPrevCamera = true; + struct PassData { VirtualResourceHandle depth; + VirtualResourceHandle worldPos; VirtualResourceHandle motionVectors; fg::RenderDevice* device; MotionVectorPassState* state; - Fmatrix invViewProj; + Fmatrix viewProj; Fmatrix prevViewProj; + Fvector cameraPos; + Fvector prevCameraPos; u32 width, height; + u32 hasPrevCamera; }; auto& passData = fg.addCallbackPass( @@ -77,31 +99,46 @@ MotionVectorOutput setupMotionVectorPass( [&, mvHandle](FrameGraph& builder, PassHandle pass, PassData& data) { RenderPassBuilder pb(builder, pass); data.depth = pb.read(depthInput, ResourceState::ShaderResource); + data.worldPos = pb.read(worldPosInput, ResourceState::ShaderResource); data.motionVectors = pb.write(mvHandle, ResourceState::UnorderedAccess); data.device = device; data.state = &state; - data.invViewProj = invViewProj; + data.viewProj = viewProj; data.prevViewProj = prevViewProj; + data.cameraPos = cameraPos; + data.prevCameraPos = prevCameraPos; data.width = width; data.height = height; + data.hasPrevCamera = hasPrevCamera ? 1u : 0u; }, - [](const PassData& data, const FrameGraph& fg, fg::RenderContext* ctx) { - auto* depthTex = fg.GetPhysicalTexture(data.depth); - auto* mvTex = fg.GetPhysicalTexture(data.motionVectors); - if (!depthTex || !mvTex) return; - - struct { - Fmatrix invViewProj; + [](const PassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + auto* depthTex = fgGraph.GetPhysicalTexture(data.depth); + auto* worldPosTex = fgGraph.GetPhysicalTexture(data.worldPos); + auto* mvTex = fgGraph.GetPhysicalTexture(data.motionVectors); + if (!depthTex || !worldPosTex || !mvTex) return; + + struct alignas(16) { + Fmatrix viewProj; Fmatrix prevViewProj; + Fmatrix invViewProj; float screenW, screenH; float invScreenW, invScreenH; + Fvector4 cameraPos; + Fvector4 prevCameraPos; + u32 hasPrevCamera; + u32 pad0, pad1, pad2; } cb; - cb.invViewProj = data.invViewProj; + cb.viewProj = data.viewProj; cb.prevViewProj = data.prevViewProj; + cb.invViewProj.invert(data.viewProj); cb.screenW = (float)data.width; cb.screenH = (float)data.height; cb.invScreenW = 1.0f / data.width; cb.invScreenH = 1.0f / data.height; + cb.cameraPos.set(data.cameraPos.x, data.cameraPos.y, data.cameraPos.z, 0.f); + cb.prevCameraPos.set(data.prevCameraPos.x, data.prevCameraPos.y, data.prevCameraPos.z, 0.f); + cb.hasPrevCamera = data.hasPrevCamera; + cb.pad0 = cb.pad1 = cb.pad2 = 0; nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); nvrhi::ICommandList* cmdList = ctx->GetCommandList(); @@ -112,6 +149,7 @@ MotionVectorOutput setupMotionVectorPass( BindingSetBuilder bsb(*mvRefl, nvDevice, "MotionVector"); bsb.ConstantBuffer("MotionVectorParams", data.state->cb) .Texture("t_Depth", depthTex) + .Texture("t_WorldPos", worldPosTex) .TextureUAV("u_MotionVectors", mvTex); auto bindDesc = bsb.Build(); auto& cache = GetPassResourceCache(); diff --git a/src/Layers/xrRender/FrameGraphPasses/MotionVectorPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/MotionVectorPassSetup.h index 250a8103b58..da1fb1e35bb 100644 --- a/src/Layers/xrRender/FrameGraphPasses/MotionVectorPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/MotionVectorPassSetup.h @@ -13,6 +13,9 @@ struct MotionVectorPassState { nvrhi::BindingLayoutHandle layout; nvrhi::IBuffer* cb = nullptr; bool initialized = false; + u32 pipeVersion = 0; + Fvector prevCameraPos = {0, 0, 0}; + bool hasPrevCamera = false; }; struct MotionVectorOutput { @@ -23,7 +26,8 @@ MotionVectorOutput setupMotionVectorPass( framegraph::FrameGraph& fg, fg::RenderDevice* device, framegraph::VirtualResourceHandle depthInput, - const Fmatrix& invViewProj, + framegraph::VirtualResourceHandle worldPosInput, + const Fmatrix& viewProj, const Fmatrix& prevViewProj, u32 width, u32 height, MotionVectorPassState& state); diff --git a/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp index c39d1df3478..debe9f31f43 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp @@ -20,6 +20,7 @@ #include "Layers/xrRender/Bindless/MaterialBuffer.h" #include "Layers/xrRender/FrameGraph/PassResourceCache.h" #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/ClusteredLightManager.h" #include "xrParticles/psystem.h" #include "xrCDB/Frustum.h" // For CFrustum (frustum plane extraction) @@ -482,8 +483,49 @@ void InitializeParticleResources(fg::RenderDevice* device, const nvrhi::Framebuf } } + auto makeLitLike = [&](const char* psName, nvrhi::ShaderHandle& outPS, + nvrhi::BindingLayoutHandle& outLayout, nvrhi::GraphicsPipelineHandle* outPipes, const char* layoutKey) + { + auto litPs = shaderLoader->LoadPixelShader(psName, "main"); + if (!litPs.handle || !litPs.reflection) + return; + outPS = litPs.handle; + outLayout = cache.GetOrCreateBindingLayoutFromReflection( + layoutKey, *vsResult.reflection, *litPs.reflection, nvDevice); + for (u32 i = 0; i < PARTICLE_BLEND_COUNT; i++) { + const auto& bd = s_blendDescs[i]; + nvrhi::GraphicsPipelineDesc pipeDesc; + pipeDesc.VS = state.vs; + pipeDesc.PS = outPS; + pipeDesc.inputLayout = state.inputLayout; + pipeDesc.bindingLayouts.push_back(outLayout); + if (bindlessLayout) + pipeDesc.bindingLayouts.push_back(bindlessLayout); + pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; + pipeDesc.renderState.depthStencilState.depthTestEnable = true; + pipeDesc.renderState.depthStencilState.depthWriteEnable = bd.depthWrite; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + if (bd.blendEnable) { + pipeDesc.renderState.blendState.targets[0].enableBlend(); + pipeDesc.renderState.blendState.targets[0].srcBlend = bd.srcBlend; + pipeDesc.renderState.blendState.targets[0].destBlend = bd.destBlend; + pipeDesc.renderState.blendState.targets[0].srcBlendAlpha = bd.srcBlendAlpha; + pipeDesc.renderState.blendState.targets[0].destBlendAlpha = bd.destBlendAlpha; + for (u32 rt = 1; rt < 4; ++rt) + pipeDesc.renderState.blendState.targets[rt].setColorWriteMask(nvrhi::ColorMask(0)); + } + string64 pipeName; + xr_sprintf(pipeName, "%s_%s", layoutKey, bd.name); + outPipes[i] = cache.GetOrCreatePipeline(pipeName, pipeDesc, fbInfo, nvDevice); + } + }; + + makeLitLike("bindless_particle_lit", state.litPS, state.litLayout, state.litPipelines, "ParticlePass_Lit"); + makeLitLike("bindless_particle_sixway", state.sixWayPS, state.sixWayLayout, state.sixWayPipelines, "ParticlePass_SixWay"); + state.initialized = true; - Msg("* [ParticlePass] Pipeline initialization complete (6 blend modes + distortion)"); + Msg("* [ParticlePass] Pipeline initialization complete (emissive/lit/sixway + distortion)"); if (!state.gpuCullingManager) { state.gpuCullingManager = xr_make_unique(); @@ -598,9 +640,16 @@ ParticlePassOutput setupParticlePass( if (b.shaderVariant == ParticleShaderVariant::Distort) return true; return false; }; - data.hasDistortion = hasDistortBatch(worldParticleBatches) || hasDistortBatch(hudParticleBatches); - - if (data.hasDistortion) { + const bool hasParticleDistort = + hasDistortBatch(worldParticleBatches) || hasDistortBatch(hudParticleBatches); + data.hasDistortion = hasParticleDistort || forwardInputs.distortion.is_valid(); + data.importedDistortion = false; + + if (forwardInputs.distortion.is_valid()) { + data.distortionRT = passBuilder.readWrite( + forwardInputs.distortion, ResourceState::RenderTarget); + data.importedDistortion = true; + } else if (hasParticleDistort) { framegraph::ResourceDesc distDesc; distDesc.type = framegraph::ResourceDesc::Type::Texture2D; distDesc.width = width; @@ -694,23 +743,47 @@ ParticlePassOutput setupParticlePass( const auto& rtDesc = colorRT->getDesc(); auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); auto* shaderLoader = GEnv.Render->GetShaderLoader(); auto* vsReflection = shaderLoader->GetCachedReflection("bindless_particle", ".vs"); auto* psReflection = shaderLoader->GetCachedReflection("bindless_particle", ".ps"); BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Particle"); - bsb.ConstantBuffer("static_globals", staticGlobalsCB) - .BufferSRV("g_Materials", matBuffer.GetBuffer()) - .Texture("g_SceneWorldPos", worldPosCopyRT); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + BindBindlessMaterialTables(bsb); + bsb.Texture("g_SceneWorldPos", worldPosCopyRT); auto bindDesc = bsb.Build(); auto bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bindDesc, data.passState->layout, nvDevice); + auto bindLitLike = [&](const char* psName, nvrhi::IBindingLayout* layout) -> nvrhi::BindingSetHandle + { + if (!layout) + return nullptr; + auto* litPsRefl = shaderLoader->GetCachedReflection(psName, ".ps"); + if (!vsReflection || !litPsRefl) + return nullptr; + BindingSetBuilder lbsb(*vsReflection, *litPsRefl, nvDevice, psName); + lbsb.ConstantBuffer("static_globals", staticGlobalsCB); + BindBindlessMaterialTables(lbsb); + lbsb.Texture("g_SceneWorldPos", worldPosCopyRT); + auto& clm = ClusteredLightManager::Instance(); + if (clm.GetLightDataBuffer()) + lbsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); + if (clm.GetClusterGridBuffer()) + lbsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); + if (clm.GetLightIndexListBuffer()) + lbsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); + return framegraph::GetPassResourceCache().GetOrCreateBindingSet(lbsb.Build(), layout, nvDevice); + }; + auto litBindingSet = bindLitLike("bindless_particle_lit", data.passState->litLayout); + auto sixWayBindingSet = bindLitLike("bindless_particle_sixway", data.passState->sixWayLayout); + auto* backend = data.device->GetBackend(); nvrhi::IDescriptorTable* bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; nvrhi::Rect scissor(rtDesc.width, rtDesc.height); - // Render order: SET first (opaque w/ depth write), then transparent modes static const u8 s_renderOrder[] = { PARTICLE_BLEND_SET, PARTICLE_BLEND_MUL, @@ -720,13 +793,25 @@ ParticlePassOutput setupParticlePass( PARTICLE_BLEND_ALPHA_ADD, }; + enum class ParticleDrawPath : u8 { Emissive, Lit, SixWay }; + auto renderBatchGroup = [&](const xr_vector& allBatches, u8 blendMode, - float depthMin, float depthMax) + ParticleDrawPath path, float depthMin, float depthMax) { xr_vector filtered; for (const auto& b : allBatches) { - if (b.blendMode == blendMode && b.shaderVariant == ParticleShaderVariant::Standard) - filtered.push_back(b); + if (b.blendMode != blendMode || b.shaderVariant == ParticleShaderVariant::Distort) + continue; + if (b.lightingMode == PS::ParticleLightingMode::Volumetric) + continue; + if (path == ParticleDrawPath::Lit && b.lightingMode != PS::ParticleLightingMode::Lit) + continue; + if (path == ParticleDrawPath::SixWay && b.lightingMode != PS::ParticleLightingMode::SixWay) + continue; + if (path == ParticleDrawPath::Emissive && + b.lightingMode != PS::ParticleLightingMode::Emissive) + continue; + filtered.push_back(b); } if (filtered.empty()) return; @@ -743,10 +828,20 @@ ParticlePassOutput setupParticlePass( cmdList->writeBuffer(data.passState->particleVB, vertices.data(), vertices.size() * sizeof(ParticleVertex)); - auto pipeline = data.passState->pipelines[blendMode]; + nvrhi::IGraphicsPipeline* pipeline = nullptr; + nvrhi::IBindingSet* bs = bindingSet; + if (path == ParticleDrawPath::Lit) { + pipeline = data.passState->litPipelines[blendMode]; + bs = litBindingSet; + } else if (path == ParticleDrawPath::SixWay) { + pipeline = data.passState->sixWayPipelines[blendMode]; + bs = sixWayBindingSet; + } else { + pipeline = data.passState->pipelines[blendMode]; + } if (!pipeline) pipeline = data.passState->pipelines[PARTICLE_BLEND_BLEND]; - if (!pipeline) + if (!pipeline || !bs) return; nvrhi::Viewport viewport( @@ -758,7 +853,7 @@ ParticlePassOutput setupParticlePass( nvrhi::GraphicsState gfxState; gfxState.pipeline = pipeline; gfxState.framebuffer = framebuffer; - gfxState.bindings = { bindingSet }; + gfxState.bindings = { bs }; if (bindlessTable) gfxState.addBindingSet(bindlessTable); gfxState.vertexBuffers = { {data.passState->particleVB, 0, 0} }; @@ -775,15 +870,19 @@ ParticlePassOutput setupParticlePass( ); }; - if (totalWorld > 0) { - for (u8 mode : s_renderOrder) - renderBatchGroup(*data.worldParticleBatches, mode, 0.0f, 1.0f); - } + auto renderAllPaths = [&](const xr_vector& batches, float z0, float z1) + { + for (u8 mode : s_renderOrder) { + renderBatchGroup(batches, mode, ParticleDrawPath::Emissive, z0, z1); + renderBatchGroup(batches, mode, ParticleDrawPath::Lit, z0, z1); + renderBatchGroup(batches, mode, ParticleDrawPath::SixWay, z0, z1); + } + }; - if (totalHUD > 0) { - for (u8 mode : s_renderOrder) - renderBatchGroup(*data.hudParticleBatches, mode, 0.0f, 0.1f); - } + if (totalWorld > 0) + renderAllPaths(*data.worldParticleBatches, 0.0f, 1.0f); + if (totalHUD > 0) + renderAllPaths(*data.hudParticleBatches, 0.0f, 0.1f); if (!data.hasDistortion || !data.distortionRT.is_valid()) return; @@ -802,13 +901,14 @@ ParticlePassOutput setupParticlePass( if (!data.passState->distortPipeline) return; - cmdList->clearTextureFloat(distortRT, nvrhi::AllSubresources, nvrhi::Color(0.f, 0.f, 0.f, 0.f)); + if (!data.importedDistortion) + cmdList->clearTextureFloat(distortRT, nvrhi::AllSubresources, nvrhi::Color(0.f, 0.f, 0.f, 0.f)); auto* distortVsReflection = shaderLoader->GetCachedReflection("bindless_particle", ".vs"); auto* distortPsReflection = shaderLoader->GetCachedReflection("bindless_particle_distort", ".ps"); BindingSetBuilder distortBsb(*distortVsReflection, *distortPsReflection, nvDevice, "Particle.Distort"); - distortBsb.ConstantBuffer("static_globals", staticGlobalsCB) - .BufferSRV("g_Materials", matBuffer.GetBuffer()); + distortBsb.ConstantBuffer("static_globals", staticGlobalsCB); + BindBindlessMaterialTables(distortBsb); auto distortBindDesc = distortBsb.Build(); auto distortBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet( distortBindDesc, data.passState->distortLayout, nvDevice); @@ -874,6 +974,7 @@ ParticlePassOutput setupParticlePass( output.layout.baseColor = passData.baseColor; output.layout.worldPos = passData.worldPos; output.layout.depth = passData.depth; + output.layout.distortion = passData.distortionRT; output.distortionRT = passData.distortionRT; return output; } diff --git a/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.h index 3c6776ef144..6bc5d5329cb 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.h @@ -5,6 +5,7 @@ #include "Layers/xrRender/FrameGraph/FGResource.h" #include "Layers/xrRender/FrameGraph/IPass.h" #include "Layers/xrRender/FBasicVisual.h" +#include "Layers/xrRender/ParticleEffectDef.h" #include "PassVertexFormats.h" #include "ParticleGPUCullingManager.h" @@ -58,16 +59,23 @@ struct ParticleBatch { u32 bindlessMaterialID = 0; u8 blendMode = PARTICLE_BLEND_BLEND; ParticleShaderVariant shaderVariant = ParticleShaderVariant::Standard; + PS::ParticleLightingMode lightingMode = PS::ParticleLightingMode::Emissive; }; struct ParticlePassState { nvrhi::GraphicsPipelineHandle pipelines[PARTICLE_BLEND_COUNT]; + nvrhi::GraphicsPipelineHandle litPipelines[PARTICLE_BLEND_COUNT]; + nvrhi::GraphicsPipelineHandle sixWayPipelines[PARTICLE_BLEND_COUNT]; nvrhi::GraphicsPipelineHandle distortPipeline; nvrhi::BindingLayoutHandle layout; + nvrhi::BindingLayoutHandle litLayout; + nvrhi::BindingLayoutHandle sixWayLayout; nvrhi::BindingLayoutHandle distortLayout; nvrhi::InputLayoutHandle inputLayout; nvrhi::ShaderHandle vs; nvrhi::ShaderHandle ps; + nvrhi::ShaderHandle litPS; + nvrhi::ShaderHandle sixWayPS; nvrhi::ShaderHandle distortPS; nvrhi::SamplerHandle sampler; bool initialized = false; @@ -101,6 +109,7 @@ struct ParticlePassData { u32 hiZMipLevels; ParticlePassState* passState; bool hasDistortion; + bool importedDistortion; }; struct ParticlePassOutput { diff --git a/src/Layers/xrRender/FrameGraphPasses/PassCommon.cpp b/src/Layers/xrRender/FrameGraphPasses/PassCommon.cpp index 5a65a7404df..996b28c80f9 100644 --- a/src/Layers/xrRender/FrameGraphPasses/PassCommon.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/PassCommon.cpp @@ -9,15 +9,46 @@ #include "xrEngine/device.h" #include "xrEngine/IRenderBackend.h" #include "xrCDB/Frustum.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/Bindless/MaterialBuffer.h" +#include "Layers/xrRender/Bindless/TerrainMaterialBuffer.h" +#include "Layers/xrRender/Bindless/VariantTextureBuffer.h" +#include "Layers/xrRender/Bindless/BindlessTypes.h" +#include namespace xray::render::fg::passes { +namespace +{ +std::atomic g_mdiDrawCalls{0}; +std::atomic g_mdiMaxDrawCountSum{0}; +} + +void ResetMdiDrawCounters() +{ + g_mdiDrawCalls.store(0, std::memory_order_relaxed); + g_mdiMaxDrawCountSum.store(0, std::memory_order_relaxed); +} + +u32 GetMdiDrawCallCount() +{ + return g_mdiDrawCalls.load(std::memory_order_relaxed); +} + +u32 GetMdiMaxDrawCountSum() +{ + return g_mdiMaxDrawCountSum.load(std::memory_order_relaxed); +} + void DrawIndexedIndirectCountOrFallback( nvrhi::ICommandList* cmdList, uint32_t paramOffsetBytes, uint32_t countOffsetBytes, uint32_t maxDrawCount) { + g_mdiDrawCalls.fetch_add(1, std::memory_order_relaxed); + g_mdiMaxDrawCountSum.fetch_add(maxDrawCount, std::memory_order_relaxed); + // MoltenVK / older GPUs may lack drawIndirectCount. Callers that take the // fallback path must clear unused compact draw-arg slots (instanceCount=0). const bool useCount = GEnv.Backend && GEnv.Backend->GetCapabilities().drawIndirectCount; @@ -141,4 +172,74 @@ void ResolveEnvSkyCubes(fg::RenderDevice* device, nvrhi::ITexture*& outSky0, nvr outSky1 = cache.GetDummyCubeMap(nvDevice); } +void BindBindlessMaterialTables(framegraph::BindingSetBuilder& bsb) +{ + // bindless_common.h declares t8/t9/t10 even when a pass only samples g_Materials. + // Skipping any of them → NVRHI null binding set → invisible geometry. + // Always bind something: early frames / shadow-only paths may lack real tables. + static nvrhi::BufferHandle s_dummyMat; + static nvrhi::BufferHandle s_dummyTerrain; + static nvrhi::BufferHandle s_dummyVariant; + + auto ensureDummy = [](nvrhi::BufferHandle& slot, const char* name, u32 stride) -> nvrhi::IBuffer* { + if (!slot) + { + auto* backend = GEnv.Render ? GEnv.Render->GetRenderDevice() : nullptr; + nvrhi::IDevice* dev = backend ? backend->GetNVRHIDevice() : nullptr; + if (!dev) + return nullptr; + nvrhi::BufferDesc desc; + desc.byteSize = stride * 4; + desc.structStride = stride; + desc.debugName = name; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + slot = dev->createBuffer(desc); + } + return slot.Get(); + }; + + nvrhi::IBuffer* mats = bindless::MaterialBuffer::Instance().GetBuffer(); + if (!mats) + mats = ensureDummy(s_dummyMat, "DummyMaterials", sizeof(bindless::MaterialData)); + if (mats) + bsb.BufferSRV("g_Materials", mats); + + nvrhi::IBuffer* terrain = bindless::TerrainMaterialBuffer::Instance().GetBuffer(); + if (!terrain) + terrain = ensureDummy(s_dummyTerrain, "DummyTerrainMaterials", sizeof(bindless::TerrainMaterialData)); + if (terrain) + bsb.BufferSRV("g_TerrainMaterials", terrain); + + nvrhi::IBuffer* variants = bindless::VariantTextureBuffer::Instance().GetBuffer(); + if (!variants) + variants = ensureDummy(s_dummyVariant, "DummyVariantTextures", sizeof(bindless::VariantTextureData)); + if (variants) + bsb.BufferSRV("g_VariantTextures", variants); +} + +void BindPaintSplatBuffer(framegraph::BindingSetBuilder& bsb, nvrhi::IDevice* device, + nvrhi::IBuffer* splatBuffer) +{ + nvrhi::IBuffer* buf = splatBuffer; + if (!buf) + { + // skinned_common.h always declares t11 — shadow / HUD may not have overlays. + static nvrhi::BufferHandle s_dummyPaint; + if (!s_dummyPaint && device) + { + nvrhi::BufferDesc desc; + desc.byteSize = 256; + desc.structStride = 64; + desc.debugName = "DummyPaintSplats"; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + s_dummyPaint = device->createBuffer(desc); + } + buf = s_dummyPaint.Get(); + } + if (buf) + bsb.BufferSRV("g_PaintSplats", buf); +} + } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/PassCommon.h b/src/Layers/xrRender/FrameGraphPasses/PassCommon.h index 0abe13320a6..50002102f32 100644 --- a/src/Layers/xrRender/FrameGraphPasses/PassCommon.h +++ b/src/Layers/xrRender/FrameGraphPasses/PassCommon.h @@ -15,6 +15,10 @@ void DrawIndexedIndirectCountOrFallback( uint32_t countOffsetBytes, uint32_t maxDrawCount); +void ResetMdiDrawCounters(); +u32 GetMdiDrawCallCount(); +u32 GetMdiMaxDrawCountSum(); + struct LightingConstants { Fvector4 sunDirection; Fvector4 sunColor; @@ -72,4 +76,12 @@ u32 ExtractFrustumPlanes(Fvector4 outPlanes[6]); // Resolve $user$sky0/$user$sky1 cubemaps for water reflections (falls back to dummy cubes). void ResolveEnvSkyCubes(fg::RenderDevice* device, nvrhi::ITexture*& outSky0, nvrhi::ITexture*& outSky1); +/// Bind g_Materials / g_TerrainMaterials / g_VariantTextures required by bindless_common.h +/// (t8/t9/t10). Always call when the PS includes bindless_common.h. +void BindBindlessMaterialTables(framegraph::BindingSetBuilder& bsb); + +/// Bind g_PaintSplats (t11) required by skinned_common.h. Uses overlay buffer or a dummy. +void BindPaintSplatBuffer(framegraph::BindingSetBuilder& bsb, nvrhi::IDevice* device, + nvrhi::IBuffer* splatBuffer = nullptr); + } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/PassVertexFormats.h b/src/Layers/xrRender/FrameGraphPasses/PassVertexFormats.h index 65e939bca97..edd2dd144f7 100644 --- a/src/Layers/xrRender/FrameGraphPasses/PassVertexFormats.h +++ b/src/Layers/xrRender/FrameGraphPasses/PassVertexFormats.h @@ -17,6 +17,12 @@ struct SunVertex { u32 color; float u, v; }; + +struct CloudVertex { + Fvector3 position; + u32 dir; + u32 color; +}; #pragma pack(pop) struct TextVertex { @@ -67,7 +73,15 @@ struct AdaptCB { float middleGrayX; float middleGrayY; float middleGrayZ; - float middleGrayW; // adaptation blend + float pad0; + float lowPercentile; + float highPercentile; + float adaptUp; + float adaptDown; + float evBias; + float deltaTime; + float pad1; + float pad2; }; } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/PathTracerPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/PathTracerPassSetup.cpp index 3f26329313c..45249776e2b 100644 --- a/src/Layers/xrRender/FrameGraphPasses/PathTracerPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/PathTracerPassSetup.cpp @@ -266,14 +266,14 @@ PathTracerOutput setupPathTracerPass( cbData.skinnedBatchStart = batchCounts.identityStatic + batchCounts.terrain + batchCounts.transparent + batchCounts.instancedTotal; else - cbData.skinnedBatchStart = 0; + cbData.skinnedBatchStart = 0xFFFFFFFFu; if (batchCounts.grass > 0) cbData.grassBatchStart = batchCounts.identityStatic + batchCounts.terrain + batchCounts.transparent + batchCounts.instancedTotal + batchCounts.skinned; else - cbData.grassBatchStart = 0; + cbData.grassBatchStart = 0xFFFFFFFFu; cbData.detailAtlasIndex = accelMgr->GetDetailAtlasIndex(); cbData.pad[0] = 0; diff --git a/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.cpp new file mode 100644 index 00000000000..6b3f4654c16 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.cpp @@ -0,0 +1,300 @@ +#include "stdafx.h" +#include "PostProcessPassSetup.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/ResourceManager/FGResourceManager.h" +#include "Layers/xrRender/ResourceManager/TextureManager.h" +#include "Layers/xrRender/r4_rendertarget.h" +#include "Layers/xrRender/FrameGraphPasses/ShaderConstants.h" +#include "xrEngine/IGame_Persistent.h" +#include "xrEngine/ShadersExternalData.h" + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +struct alignas(16) PostProcessCB +{ + Fvector4 dual_r0; + Fvector4 dual_r1; + Fvector4 dual_l0; + Fvector4 dual_l1; + Fvector4 noise0; + Fvector4 noise1; + Fvector4 color; + Fvector4 gray; + Fvector4 brightness; + Fvector4 colormap; + Fvector4 mode; +}; + +void EnsurePPPipeline(nvrhi::IDevice* nv, PostProcessPassState& st, nvrhi::Format outFmt) +{ + if (!nv) + return; + if (st.initialized && st.pipeline && st.pipelineFormat == outFmt) + return; + + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + { + st.initialized = true; + return; + } + auto vs = loader->LoadVertexShader("fullscreen"); + auto ps = loader->LoadPixelShader("postprocess"); + if (!vs.handle || !ps.handle) + { + Msg("! [PostProcess] shaders missing — PP disabled"); + st.initialized = true; + return; + } + auto& cache = GetPassResourceCache(); + if (!st.layout) + st.layout = cache.GetOrCreateBindingLayoutFromReflection( + "PostProcess_v3", *vs.reflection, *ps.reflection, nv); + if (st.layout) + { + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs.handle); + desc.setPixelShader(ps.handle); + desc.addBindingLayout(st.layout); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.blendState.targets[0].setBlendEnable(false); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + nvrhi::FramebufferInfoEx fb; + fb.addColorFormat(outFmt); + char pipeName[64]; + xr_sprintf(pipeName, "PostProcess_v3_fmt%u", (u32)outFmt); + st.pipeline = cache.GetOrCreatePipeline(pipeName, desc, fb, nv); + st.pipelineFormat = outFmt; + } + + if (!st.noisePlaceholder) + { + nvrhi::TextureDesc td; + td.width = 1; + td.height = 1; + td.format = nvrhi::Format::RGBA8_UNORM; + td.debugName = "PP_NoisePlaceholder"; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + st.noisePlaceholder = nv->createTexture(td); + nvrhi::CommandListHandle cmd = nv->createCommandList(); + cmd->open(); + u32 gray = 0xFF808080; + cmd->writeTexture(st.noisePlaceholder, 0, 0, &gray, sizeof(gray)); + cmd->close(); + nv->executeCommandList(cmd); + } + + st.initialized = true; +} +} // namespace + +VirtualResourceHandle setupPostProcessPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle ldrInput, + VirtualResourceHandle outputTarget, + u32 width, + u32 height, + CRenderTarget* target, + PostProcessPassState& state) +{ + if (!device || !device->GetNVRHIDevice() || !ldrInput.is_valid() || !target) + return ldrInput; + if (!target->u_need_PP()) + { + if (g_pGamePersistent && g_pGamePersistent->m_pGShaderConstants) + { + if (g_pGamePersistent->m_pGShaderConstants->m_blender_mode.x < 0.5f) + return ldrInput; + } + else + return ldrInput; + } + + nvrhi::Format outFmt = nvrhi::Format::RGBA8_UNORM; + if (GEnv.Backend && GEnv.Backend->GetBackBuffer()) + outFmt = GEnv.Backend->GetBackBuffer()->getDesc().format; + + EnsurePPPipeline(device->GetNVRHIDevice(), state, outFmt); + if (!state.pipeline || !state.layout) + return ldrInput; + + const bool hasOutputTarget = outputTarget.is_valid(); + + struct PassData + { + VirtualResourceHandle input, output; + u32 width, height; + PostProcessPassState* st = nullptr; + fg::RenderDevice* device = nullptr; + CRenderTarget* target = nullptr; + }; + + auto& pd = fg.addCallbackPass( + "PostProcess", + [&](FrameGraph& b, PassHandle ph, PassData& data) { + RenderPassBuilder pb(b, ph); + data.st = &state; + data.device = device; + data.target = target; + data.width = width; + data.height = height; + data.input = pb.read(ldrInput, ResourceState::ShaderResource); + + if (hasOutputTarget) + { + data.output = pb.write(outputTarget, ResourceState::RenderTarget); + } + else + { + ResourceDesc td; + td.type = ResourceDesc::Type::Texture2D; + td.width = width; + td.height = height; + td.format = outFmt; + td.isRenderTarget = true; + td.isTransient = false; + td.debugName = "rt_PostProcess"; + data.output = pb.createTexture("rt_PostProcess", td); + } + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd ? cmd->getDevice() : nullptr; + auto* in = graph.GetPhysicalTexture(data.input); + auto* out = graph.GetPhysicalTexture(data.output); + if (!cmd || !nv || !in || !out || !data.st || !data.target) + return; + + nvrhi::Format fmt = out->getDesc().format; + EnsurePPPipeline(nv, *data.st, fmt); + if (!data.st->pipeline || !data.st->layout) + return; + + auto& cache = GetPassResourceCache(); + auto* loader = GEnv.Render->GetShaderLoader(); + auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psR = loader->GetCachedReflection("postprocess", ".ps"); + if (!vsR || !psR) + return; + + Fvector2 n0, n1, r0, r1, l0, l1; + data.target->u_calc_tc_duality_ss(r0, r1, l0, l1); + data.target->u_calc_tc_noise(n0, n1); + + int gblend = clampr(iFloor((1.f - data.target->get_gray()) * 255.f), 0, 255); + int nblend = clampr(iFloor((1.f - data.target->get_noise()) * 255.f), 0, 255); + u32 p_color = subst_alpha(data.target->get_color_base(), nblend); + u32 p_gray = subst_alpha(data.target->get_color_gray(), gblend); + const Fvector& bright = data.target->get_color_add(); + + PostProcessCB cb{}; + cb.dual_r0.set(r0.x, r0.y, 0.f, 0.f); + cb.dual_r1.set(r1.x, r1.y, 0.f, 0.f); + cb.dual_l0.set(l0.x, l0.y, 0.f, 0.f); + cb.dual_l1.set(l1.x, l1.y, 0.f, 0.f); + cb.noise0.set(n0.x, n0.y, 0.f, 0.f); + cb.noise1.set(n1.x, n1.y, 0.f, 0.f); + cb.color.set( + float(color_get_R(p_color)) / 255.f, + float(color_get_G(p_color)) / 255.f, + float(color_get_B(p_color)) / 255.f, + float(color_get_A(p_color)) / 255.f); + cb.gray.set( + float(color_get_R(p_gray)) / 255.f, + float(color_get_G(p_gray)) / 255.f, + float(color_get_B(p_gray)) / 255.f, + float(color_get_A(p_gray)) / 255.f); + cb.brightness.set(bright.x, bright.y, bright.z, 0.f); + cb.colormap.set( + data.target->get_cm_influence(), + data.target->get_cm_interpolate(), + 0.f, 0.f); + + float blenderMode = 0.f; + float nvIntensity = 1.f; + if (g_pGamePersistent && g_pGamePersistent->m_pGShaderConstants) + { + blenderMode = g_pGamePersistent->m_pGShaderConstants->m_blender_mode.x; + nvIntensity = g_pGamePersistent->m_pGShaderConstants->hud_params.z; + } + cb.mode.set(blenderMode, nvIntensity, 0.f, 0.f); + + auto* pcb = cache.GetOrCreateVolatileCB("PostProcess", "PostProcessParams", sizeof(PostProcessCB), data.device); + if (pcb) + cmd->writeBuffer(pcb, &cb, sizeof(cb)); + + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + { + StaticGlobals sg = BuildStaticGlobals(); + cmd->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + + nvrhi::ITexture* noiseTex = data.st->noisePlaceholder.Get(); + auto* resMgr = data.device->GetFGResourceManager(); + auto* texMgr = resMgr ? resMgr->GetTextureManager() : nullptr; + if (texMgr) + { + auto handle = texMgr->LoadTexture("fx\\fx_noise2"); + if (auto* t = texMgr->GetNVRHITexture(handle)) + noiseTex = t; + } + + nvrhi::ITexture* grad0 = data.st->noisePlaceholder.Get(); + nvrhi::ITexture* grad1 = data.st->noisePlaceholder.Get(); + if (data.target->u_need_CM()) + { + auto& cm0 = data.target->get_color_map_manager().get_cmap(0); + auto& cm1 = data.target->get_color_map_manager().get_cmap(1); + if (cm0 && cm0->surface_get_native()) + grad0 = static_cast(cm0->surface_get_native()); + if (cm1 && cm1->surface_get_native()) + grad1 = static_cast(cm1->surface_get_native()); + } + + BindingSetBuilder bsb(*vsR, *psR, nv, "PostProcess"); + if (pcb) + bsb.ConstantBuffer("PostProcessParams", pcb); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.Texture("s_base0", in); + bsb.Texture("s_base1", in); + bsb.Texture("s_noise", noiseTex); + bsb.Texture("s_grad0", grad0); + bsb.Texture("s_grad1", grad1); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->layout, nv); + if (!set) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(out); + auto fb = cache.GetOrCreateFramebuffer("PostProcess", fbDesc, nv); + if (!fb) + return; + + nvrhi::GraphicsState gs; + gs.pipeline = data.st->pipeline; + gs.framebuffer = fb; + gs.bindings = {set}; + gs.viewport.addViewportAndScissorRect(nvrhi::Viewport(float(data.width), float(data.height))); + cmd->setGraphicsState(gs); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); + }); + + return pd.output; +} + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.h new file mode 100644 index 00000000000..4ac001f1606 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.h @@ -0,0 +1,32 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; class CRenderTarget; } + +namespace xray::render::fg::passes +{ + +struct PostProcessPassState +{ + nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::BindingLayoutHandle layout; + nvrhi::Format pipelineFormat = nvrhi::Format::UNKNOWN; + nvrhi::TextureHandle noisePlaceholder; + bool initialized = false; +}; + +framegraph::VirtualResourceHandle setupPostProcessPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle ldrInput, + framegraph::VirtualResourceHandle outputTarget, + u32 width, + u32 height, + CRenderTarget* target, + PostProcessPassState& state); + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.cpp index 598ce3352f5..46f229b19f9 100644 --- a/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.cpp @@ -11,6 +11,9 @@ #include "Layers/xrRender/Geometry/MaterialCache.h" #include "Layers/xrRender/Bindless/MaterialBuffer.h" #include "Layers/xrRender/Bindless/TerrainMaterialBuffer.h" + +extern ENGINE_API int ps_r_rt_gi; +extern ENGINE_API int ps_r_path_tracer; #include "Layers/xrRender/ResourceManager/FGResourceManager.h" #include "Layers/xrRender/ResourceManager/TextureManager.h" #include "Layers/xrRender/ResourceManager/NativeRTFactory.h" @@ -31,21 +34,25 @@ namespace void ComputeTopDownRainMatrices(float extent, float height, u32 smapRes, Fmatrix& outClipVP, Fmatrix& outSampleVP) { const Fvector& C = Device.vCameraPosition; + const float e = std::max(extent, 8.f); + const float texel = (e * 2.f) / float(std::max(smapRes, 1u)); + Fvector eye; - eye.set(C.x, C.y + height, C.z); + eye.set( + floorf(C.x / texel + 0.5f) * texel, + C.y + height, + floorf(C.z / texel + 0.5f) * texel); Fvector dir(0.f, -1.f, 0.f); Fvector up(0.f, 0.f, 1.f); Fmatrix view; view.build_camera_dir(eye, dir, up); - const float e = std::max(extent, 8.f); Fmatrix proj; proj.build_projection_ortho(e * 2.f, e * 2.f, 1.f, height + 120.f); outClipVP.mul(proj, view); - // Clip → UV (same convention as CSM sample VP) Fmatrix toUV; toUV.identity(); toUV._11 = 0.5f; @@ -54,8 +61,6 @@ void ComputeTopDownRainMatrices(float extent, float height, u32 smapRes, Fmatrix toUV._41 = 0.5f; toUV._42 = 0.5f; outSampleVP.mul(toUV, outClipVP); - - (void)smapRes; } } // namespace @@ -66,7 +71,8 @@ void InitializeRainShadowPass(fg::RenderDevice* device, RainShadowPassState& sta return; const u32 res = std::clamp((u32)ps_r3_dyn_wet_surf_sm_res, 64u, 2048u); - if (state.initialized && state.enabled && state.resolution == res && state.rainSM) + if (state.initialized && state.enabled && state.resolution == res + && state.rainSM && state.rainPipeline && state.rainCB) return; if (state.initialized) @@ -83,12 +89,85 @@ void InitializeRainShadowPass(fg::RenderDevice* device, RainShadowPassState& sta state.resolution = res; state.rainSMHandle = resMgr->GetRTFactory()->CreateShadowMap(res, true, "rt_RainShadow"); state.rainSM = resMgr->GetTextureManager()->GetNVRHITexture(state.rainSMHandle); + + auto* loader = GEnv.Render ? GEnv.Render->GetShaderLoader() : nullptr; + nvrhi::IDevice* nv = device->GetNVRHIDevice(); + if (nv && !state.rainCB) + { + nvrhi::BufferDesc cbDesc; + cbDesc.byteSize = sizeof(ShadowCascadeCB); + cbDesc.isConstantBuffer = true; + cbDesc.isVolatile = true; + cbDesc.maxVersions = 64; + cbDesc.debugName = "RainShadowCB"; + state.rainCB = nv->createBuffer(cbDesc); + } + if (loader && nv) + { + auto vs = loader->LoadVertexShader("shadow\\shadow_cascade", "main"); + auto ps = loader->LoadPixelShader("shadow\\shadow_cascade_rain", "main"); + if (vs.handle && ps.handle && vs.reflection && ps.reflection) + { + auto& cache = GetPassResourceCache(); + state.rainLayout = cache.GetOrCreateBindingLayoutFromReflection( + "RainShadowSolid", *vs.reflection, *ps.reflection, nv); + if (state.rainLayout) + { + u32 attrCount = 0; + auto* attrs = GetUnifiedVertexAttributes(attrCount); + auto inputLayout = nv->createInputLayout(attrs, attrCount, vs.handle); + + nvrhi::GraphicsPipelineDesc pipeDesc; + pipeDesc.VS = vs.handle; + pipeDesc.PS = ps.handle; + pipeDesc.inputLayout = inputLayout; + pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; + pipeDesc.renderState.depthStencilState.setDepthTestEnable(true); + pipeDesc.renderState.depthStencilState.setDepthWriteEnable(true); + pipeDesc.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + pipeDesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + pipeDesc.renderState.rasterState.depthBias = 2; + pipeDesc.renderState.rasterState.slopeScaledDepthBias = 1.5f; + + auto* backend = device->GetBackend(); + nvrhi::IBindingLayout* bindlessLayout = backend ? backend->GetBindlessLayout() : nullptr; + if (bindlessLayout) + pipeDesc.bindingLayouts = {state.rainLayout, bindlessLayout}; + else + pipeDesc.bindingLayouts = {state.rainLayout}; + + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.depthFormat = nvrhi::Format::D32; + state.rainPipeline = cache.GetOrCreatePipeline("RainShadowSolid", pipeDesc, fbInfo, nv); + + auto terrainPs = loader->LoadPixelShader("shadow\\shadow_cascade_terrain", "main"); + if (terrainPs.handle && terrainPs.reflection) + { + state.terrainLayout = cache.GetOrCreateBindingLayoutFromReflection( + "RainShadowTerrain", *vs.reflection, *terrainPs.reflection, nv); + if (state.terrainLayout) + { + nvrhi::GraphicsPipelineDesc terrainDesc = pipeDesc; + terrainDesc.PS = terrainPs.handle; + if (bindlessLayout) + terrainDesc.bindingLayouts = {state.terrainLayout, bindlessLayout}; + else + terrainDesc.bindingLayouts = {state.terrainLayout}; + state.terrainPipeline = cache.GetOrCreatePipeline( + "RainShadowTerrain", terrainDesc, fbInfo, nv); + } + terrainPs.reflection = nullptr; + } + } + } + } + state.initialized = true; - state.enabled = (state.rainSM != nullptr); + state.enabled = (state.rainSM != nullptr && state.rainPipeline != nullptr && state.rainCB != nullptr); if (state.enabled) Msg("* [RainShadow] Init: OK (%ux%u)", res, res); else - Msg("! [RainShadow] Failed to create rain SM"); + Msg("! [RainShadow] Failed to create rain SM / solid pipeline"); } void ShutdownRainShadowPass(fg::RenderDevice* device, RainShadowPassState& state) @@ -100,6 +179,11 @@ void ShutdownRainShadowPass(fg::RenderDevice* device, RainShadowPassState& state } state.rainSMHandle = {}; state.rainSM = nullptr; + state.rainPipeline = nullptr; + state.rainLayout = nullptr; + state.terrainPipeline = nullptr; + state.terrainLayout = nullptr; + state.rainCB = nullptr; state.resolution = 0; state.initialized = false; state.enabled = false; @@ -109,7 +193,6 @@ RainShadowOutputs setupRainShadowPass( framegraph::FrameGraph& fg, fg::RenderDevice* device, const BindlessForwardConfig& bindlessConfig, - ShadowPassState& shadowState, RainShadowPassState& state) { using namespace framegraph; @@ -128,11 +211,26 @@ RainShadowOutputs setupRainShadowPass( return outputs; InitializeRainShadowPass(device, state); - if (!state.enabled || !state.rainSM || !shadowState.enabled || !shadowState.pipeline) + if (!state.enabled || !state.rainSM || !state.rainPipeline || !state.rainLayout || !state.rainCB) return outputs; - const float extent = std::max({ps_r3_dyn_wet_surf_far, ps_r3_dyn_wet_surf_near, 40.f}); - // Tall volume so multi-storey roofs above the camera still cast into the map. + const bool rtWet = (ps_r_rt_gi != 0) || (ps_r_path_tracer != 0); + float requested; + float maxExtent; + if (rtWet) + { + const float farPlane = g_pGamePersistent + ? g_pGamePersistent->Environment().CurrentEnv.far_plane + : 400.f; + requested = std::max(farPlane * 0.65f, 250.f); + maxExtent = float(state.resolution) * 0.35f; + } + else + { + requested = std::max({ps_r3_dyn_wet_surf_far, ps_r3_dyn_wet_surf_near, 40.f}); + maxExtent = float(state.resolution) * 0.05f; + } + const float extent = std::min(requested, std::max(maxExtent, 16.f)); ComputeTopDownRainMatrices(extent, 200.f, state.resolution, state.clipVP, state.sampleVP); ResourceDesc desc; @@ -152,7 +250,6 @@ RainShadowOutputs setupRainShadowPass( { VirtualResourceHandle rainSM; RainShadowPassState* rainState = nullptr; - ShadowPassState* shadowState = nullptr; BindlessForwardConfig bindlessConfig; fg::RenderDevice* device = nullptr; }; @@ -163,12 +260,11 @@ RainShadowOutputs setupRainShadowPass( RenderPassBuilder pb(builder, passHandle); data.rainSM = pb.write(rainHandle, ResourceState::DepthStencilWrite); data.rainState = &state; - data.shadowState = &shadowState; data.bindlessConfig = bindlessConfig; data.device = device; }, [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { - if (!data.rainState || !data.shadowState) + if (!data.rainState) return; auto* rainTex = graph.GetPhysicalTexture(data.rainSM); if (!rainTex) @@ -178,7 +274,6 @@ RainShadowOutputs setupRainShadowPass( if (!cmd || !nv || !rainTex) return; - ShadowPassState& st = *data.shadowState; RainShadowPassState& rs = *data.rainState; cmd->clearDepthStencilTexture( @@ -187,7 +282,7 @@ RainShadowOutputs setupRainShadowPass( if (!data.bindlessConfig.UseGPUCulling() || !data.bindlessConfig.UseMegaBuffers()) { static bool s_warned = false; - if (!s_warned) + if (!s_warned && g_pGameLevel) { Msg("! [RainShadow] GPU bindless culling unavailable — rain SM empty (wet cover broken)"); s_warned = true; @@ -197,17 +292,15 @@ RainShadowOutputs setupRainShadowPass( auto* shaderLoader = GEnv.Render->GetShaderLoader(); auto* vsRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".vs"); - auto* psRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade", ".ps"); + auto* psRefl = shaderLoader->GetCachedReflection("shadow\\shadow_cascade_rain", ".ps"); auto drawIndexBuffer = GetOrCreateDrawIndexBuffer("RainShadow", nv); - if (!vsRefl || !psRefl || !drawIndexBuffer || !st.pipeline || !st.layout || !st.cascadeCB) + if (!vsRefl || !psRefl || !drawIndexBuffer || !rs.rainPipeline || !rs.rainLayout || !rs.rainCB) return; ShadowCascadeCB cbData{}; cbData.lightVP = rs.clipVP; - cmd->writeBuffer(st.cascadeCB, &cbData, sizeof(cbData)); + cmd->writeBuffer(rs.rainCB, &cbData, sizeof(cbData)); - // Rebuild light-frustum caster lists for the rain ortho volume so roofs - // outside the camera view still occlude wet surfaces. if (auto* fgr = dynamic_cast(GEnv.Render)) { if (auto* cull = fgr->GetGPUCullingManager()) @@ -234,11 +327,10 @@ RainShadowOutputs setupRainShadowPass( auto drawSet = [&](const BindlessDrawSet& set) { if (!set.IsValid() && !set.IsCastAllValid() && !set.IsLightCullValid()) return; - // Rain/wet SM: prefer cast-all (full caster list) — roofs outside camera - // compact set must still cast into the top-down rain map. - const bool castAll = set.IsCastAllValid(); - const bool useLightCull = !castAll && (ps_r_shadow_light_cull != 0) && set.IsLightCullValid(); - if (!castAll && !useLightCull && !set.IsValid()) + + const bool useLightCull = set.IsLightCullValid(); + const bool castAll = !useLightCull && set.IsCastAllValid(); + if (!useLightCull && !castAll && !set.IsValid()) return; nvrhi::IBuffer* batchIndices = useLightCull ? set.lightCullBatchIndicesBuffer @@ -251,17 +343,17 @@ RainShadowOutputs setupRainShadowPass( : (castAll ? set.castAllCountBuffer : set.compactCountBuffer); BindingSetBuilder bsb(*vsRefl, *psRefl, nv, "RainShadow"); - bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + bsb.ConstantBuffer("ShadowCascadeCB", rs.rainCB); + BindBindlessMaterialTables(bsb); bsb.BufferSRV("g_InstanceData", set.instanceBuffer); bsb.BufferSRV("g_CompactBatchIndices", batchIndices); bsb.BufferSRV("g_CompactMaterialIDs", materialIDs); - auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), st.layout, nv); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), rs.rainLayout, nv); if (!bindingSet) return; nvrhi::GraphicsState gs; - gs.pipeline = st.pipeline; + gs.pipeline = rs.rainPipeline; gs.framebuffer = fb; gs.bindings = {bindingSet}; if (bindlessTable) @@ -280,11 +372,50 @@ RainShadowOutputs setupRainShadowPass( drawSet(data.bindlessConfig.staticSet); drawSet(data.bindlessConfig.dynamicSet); - if (st.foliagePipeline) - drawSet(data.bindlessConfig.transparentCasterSet); - // Terrain must cast into rain SM — otherwise hills/roofs of terrain never occlude wet. - if (st.terrainPipeline && st.terrainLayout && data.bindlessConfig.HasTerrain()) + if (auto* fgr = dynamic_cast(GEnv.Render)) + { + if (auto* cull = fgr->GetGPUCullingManager(); + cull && cull->GetTessObjectCount() > 0) + { + nvrhi::IBuffer* tessInst = cull->GetTessInstanceBuffer(); + nvrhi::IBuffer* tessMat = cull->GetTessMaterialIDBuffer(); + nvrhi::IBuffer* tessIdx = cull->GetTessBatchIndicesBuffer(); + nvrhi::IBuffer* tessArgs = cull->GetTessDrawArgsBuffer(); + const u32 tessCount = cull->GetTessObjectCount(); + if (tessInst && tessMat && tessIdx && tessArgs && tessCount > 0) + { + BindingSetBuilder tsb(*vsRefl, *psRefl, nv, "RainShadow.Tess"); + tsb.ConstantBuffer("ShadowCascadeCB", rs.rainCB); + BindBindlessMaterialTables(tsb); + tsb.BufferSRV("g_InstanceData", tessInst); + tsb.BufferSRV("g_CompactBatchIndices", tessIdx); + tsb.BufferSRV("g_CompactMaterialIDs", tessMat); + auto tessSet = cache.GetOrCreateBindingSet(tsb.Build(), rs.rainLayout, nv); + if (tessSet) + { + nvrhi::GraphicsState gs; + gs.pipeline = rs.rainPipeline; + gs.framebuffer = fb; + gs.bindings = {tessSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {data.bindlessConfig.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = { + data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + gs.viewport.addViewport(vp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.indirectParams = tessArgs; + cmd->setGraphicsState(gs); + cmd->drawIndexedIndirect(0, tessCount); + } + } + } + } + + if (rs.terrainPipeline && rs.terrainLayout && data.bindlessConfig.HasTerrain()) { if (auto* matCache = GEnv.Render ? GEnv.Render->GetMaterialCache() : nullptr) matCache->FinalizePendingTerrainMaterials(ctx); @@ -312,15 +443,16 @@ RainShadowOutputs setupRainShadowPass( if (terrainPsRefl && terrainMatBuffer.GetBuffer() && tInstance && tBatch && tMat && tArgs) { BindingSetBuilder tbsb(*vsRefl, *terrainPsRefl, nv, "RainShadow.Terrain"); - tbsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + tbsb.ConstantBuffer("ShadowCascadeCB", rs.rainCB); + BindBindlessMaterialTables(tbsb); tbsb.BufferSRV("g_InstanceData", tInstance); tbsb.BufferSRV("g_CompactBatchIndices", tBatch); tbsb.BufferSRV("g_CompactMaterialIDs", tMat); - auto terrainSet = cache.GetOrCreateBindingSet(tbsb.Build(), st.terrainLayout, nv); + auto terrainSet = cache.GetOrCreateBindingSet(tbsb.Build(), rs.terrainLayout, nv); if (terrainSet) { nvrhi::GraphicsState gs; - gs.pipeline = st.terrainPipeline; + gs.pipeline = rs.terrainPipeline; gs.framebuffer = fb; gs.bindings = {terrainSet}; if (bindlessTable) diff --git a/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.h index d68e0b86f0a..6d033800a23 100644 --- a/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.h @@ -24,6 +24,11 @@ struct RainShadowPassState { xray::render::fg::TextureHandle rainSMHandle; nvrhi::ITexture* rainSM = nullptr; + nvrhi::GraphicsPipelineHandle rainPipeline; + nvrhi::BindingLayoutHandle rainLayout; + nvrhi::GraphicsPipelineHandle terrainPipeline; + nvrhi::BindingLayoutHandle terrainLayout; + nvrhi::BufferHandle rainCB; u32 resolution = 0; Fmatrix clipVP; Fmatrix sampleVP; @@ -46,7 +51,6 @@ RainShadowOutputs setupRainShadowPass( framegraph::FrameGraph& fg, fg::RenderDevice* device, const BindlessForwardConfig& bindlessConfig, - ShadowPassState& shadowState, RainShadowPassState& state); } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.cpp index 8410d27074c..b614140c6a7 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.cpp @@ -9,6 +9,11 @@ #include "Layers/xrRender/RenderContext/RenderContext.h" #include "Layers/xrRender/RenderContext/RenderDevice.h" #include "Layers/xrRender/RayTracing/RTAccelStructManager.h" +#include "Layers/xrRender/ClusteredLightManager.h" +#include "Layers/xrRender/FrameGraphPasses/PassCommon.h" +#include "Layers/xrRender/FrameGraphPasses/ShaderConstants.h" +#include "Layers/xrRender/xrRender_console.h" +#include "Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h" #if defined(XR_PLATFORM_WINDOWS) #include "Layers/xrRender/Backend/D3D12Backend.h" #endif @@ -18,6 +23,22 @@ #include "xrEngine/IGame_Persistent.h" #include +extern ENGINE_API int ps_r_rt_gi; +extern ENGINE_API int ps_r_rt_gi_spatial_samples; +extern ENGINE_API float ps_r_rt_gi_spatial_radius; +extern ENGINE_API int ps_r_rt_gi_m_max; +extern ENGINE_API int ps_r_rt_gi_local_samples; +extern ENGINE_API int ps_r_rt_di_candidates; +extern ENGINE_API int ps_r_rt_di_spatial_samples; +extern ENGINE_API float ps_r_rt_di_spatial_radius; +extern ENGINE_API int ps_r_rt_di_m_max; +extern ENGINE_API int ps_r_rt_gi_atrous_steps; +extern ENGINE_API float ps_r_rt_gi_temporal_alpha; +extern ENGINE_API int ps_r_rt_gi_bounces; +extern ENGINE_API int ps_r_rt_gi_cache_size; +extern ENGINE_API float ps_r_rt_gi_cache_cell; +extern ENGINE_API int ps_r_rt_vol_steps; + namespace fg { extern xray::render::FrameGraphRenderer RImplementation; @@ -33,6 +54,7 @@ static nvrhi::TextureHandle s_rtgiPlaceholderCube; struct ReSTIRGICB { Fmatrix invViewProj; Fmatrix prevViewProj; + Fmatrix worldToView; Fvector4 cameraPos; Fvector4 sunDir_intensity; Fvector4 sunColor_skyWeight; @@ -45,9 +67,19 @@ struct ReSTIRGICB { u32 skinnedBatchStart; u32 grassBatchStart; u32 detailAtlasIndex; - u32 pad[3]; + u32 localLightSamples; + u32 diCandidates; + u32 bounces; + Fvector4 diSampleParams; + Fvector4 clusterParams; + Fvector4 clusterScales; + Fvector4 hemiColor; + u32 cacheSize; + float cacheCellSize; + u32 cacheMaxAge; + u32 pad1; }; -static_assert(sizeof(ReSTIRGICB) == 224, "ReSTIRGICB must be 224 bytes"); +static_assert(sizeof(ReSTIRGICB) == 368, "ReSTIRGICB must be 368 bytes"); struct TemporalCB { Fmatrix invViewProj; @@ -57,9 +89,104 @@ struct TemporalCB { float invScreenWidth; float invScreenHeight; u32 frameIndex; + u32 identityStaticCount; + u32 terrainBatchCount; + u32 skinnedBatchStart; + u32 grassBatchStart; + u32 detailAtlasIndex; + u32 pad[2]; +}; +static_assert(sizeof(TemporalCB) == 128, "TemporalCB must be 128 bytes"); + +struct SpatialCB { + Fmatrix invViewProj; + Fvector4 cameraPos; + float screenWidth; + float screenHeight; + float invScreenWidth; + float invScreenHeight; + u32 frameIndex; + u32 spatialSamples; + float spatialRadius; + u32 mMax; + u32 identityStaticCount; + u32 terrainBatchCount; + u32 skinnedBatchStart; + u32 grassBatchStart; + u32 detailAtlasIndex; u32 pad[3]; }; -static_assert(sizeof(TemporalCB) == 112, "TemporalCB must be 112 bytes"); +static_assert(sizeof(SpatialCB) == 144, "SpatialCB must be 144 bytes"); + +struct DITemporalCB { + Fmatrix worldToView; + Fvector4 cameraPos; + float screenWidth; + float screenHeight; + float invScreenWidth; + float invScreenHeight; + u32 frameIndex; + u32 mMax; + float pad[2]; + Fvector4 clusterParams; + Fvector4 clusterScales; +}; +static_assert(sizeof(DITemporalCB) == 144, "DITemporalCB must be 144 bytes"); + +struct DISpatialCB { + Fmatrix worldToView; + Fvector4 cameraPos; + float screenWidth; + float screenHeight; + float invScreenWidth; + float invScreenHeight; + u32 frameIndex; + u32 spatialSamples; + float spatialRadius; + u32 mMax; + Fvector4 clusterParams; + Fvector4 clusterScales; + u32 grassBatchStart; + u32 detailAtlasIndex; + u32 identityStaticCount; + u32 terrainBatchCount; + u32 skinnedBatchStart; + u32 pad0; + u32 pad1; + u32 pad2; +}; +static_assert(sizeof(DISpatialCB) == 176, "DISpatialCB must be 176 bytes"); + +struct DIShadeCB { + Fvector4 cameraPos; + float screenWidth; + float screenHeight; + u32 grassBatchStart; + u32 detailAtlasIndex; + Fvector4 clusterParams; + u32 identityStaticCount; + u32 terrainBatchCount; + u32 skinnedBatchStart; + u32 pad; +}; +static_assert(sizeof(DIShadeCB) == 64, "DIShadeCB must be 64 bytes"); + +struct WaterCB { + Fmatrix invViewProj; + Fvector4 cameraPos; + Fvector4 sunDir_intensity; + Fvector4 sunColor_skyWeight; + float screenWidth; + float screenHeight; + float giIntensity; + u32 identityStaticCount; + u32 terrainBatchCount; + u32 skinnedBatchStart; + u32 grassBatchStart; + u32 detailAtlasIndex; + Fvector4 hemiColor; +}; +static_assert(sizeof(WaterCB) == 160, "WaterCB must be 160 bytes"); struct CompositeCB { Fmatrix invViewProj; @@ -68,8 +195,59 @@ struct CompositeCB { float screenHeight; float giIntensity; u32 pad; + Fvector4 fogParams; + Fvector4 fogColor; +}; +static_assert(sizeof(CompositeCB) == 128, "CompositeCB must be 128 bytes"); + +struct BlurCB { + float screenWidth; + float screenHeight; + float invScreenWidth; + float invScreenHeight; + float phiNormal; + float phiDepth; + u32 step; + u32 mode; +}; +static_assert(sizeof(BlurCB) == 32, "BlurCB must be 32 bytes"); + +struct TemporalFilterCB { + float screenWidth; + float screenHeight; + float invScreenWidth; + float invScreenHeight; + float alpha; + float pad0; + u32 enabled; + u32 pad1; +}; +static_assert(sizeof(TemporalFilterCB) == 32, "TemporalFilterCB must be 32 bytes"); + +struct RTVolCB { + Fmatrix invViewProj; + Fvector4 cameraPos; + Fvector4 sunDir_intensity; + Fvector4 sunColor_fog; + float screenWidth; + float screenHeight; + float fogDensity; + float fogHeight; + u32 steps; + float fogFar; + float heightFalloff; + float shaftIntensity; + u32 grassBatchStart; + u32 detailAtlasIndex; + u32 pad0; + u32 pad1; }; -static_assert(sizeof(CompositeCB) == 96, "CompositeCB must be 96 bytes"); +static_assert(sizeof(RTVolCB) == 160, "RTVolCB must be 160 bytes"); + +bool IsRTGIActive(const RTAccelStructManager* accelMgr) +{ + return ::ps_r_rt_gi && accelMgr && accelMgr->IsSupported() && accelMgr->IsReady(); +} static void CreatePlaceholders(nvrhi::IDevice* nvDevice) { @@ -96,61 +274,216 @@ static void CreatePlaceholders(nvrhi::IDevice* nvDevice) } } +static void EnsureVolumetricPipeline(fg::RenderDevice* device, ReSTIRGIPassState& state) +{ + constexpr u32 kVolPipeVersion = 4; + static u32 s_volPipeVersion = 0; + if (state.volPipeline && state.volLayout && s_volPipeVersion == kVolPipeVersion) + return; + + state.volPipeline = nullptr; + state.volLayout = nullptr; + s_volPipeVersion = 0; + + auto& cache = GetPassResourceCache(); + nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); + BindingSetBuilder::InvalidateReflectionCache(); + auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("rt_volumetric"); + if (!csResult.handle || !csResult.reflection) { + Msg("! [ReSTIR GI] rt_volumetric shader missing"); + return; + } + + nvrhi::IBindingLayout* bindlessLayout = nullptr; + if (auto* backend = device->GetBackend()) + bindlessLayout = backend->GetBindlessLayout(); + + state.volLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_VolShafts_v4", *csResult.reflection, nvDevice); + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = csResult.handle; + if (bindlessLayout) + pipeDesc.bindingLayouts = { state.volLayout, bindlessLayout }; + else + pipeDesc.bindingLayouts = { state.volLayout }; + state.volPipeline = nvDevice->createComputePipeline(pipeDesc); + if (state.volPipeline) + s_volPipeVersion = kVolPipeVersion; + Msg("* [ReSTIR GI] RT sunshafts pipeline %s", state.volPipeline ? "ready" : "FAILED"); +} + +static void EnsureWaterPipeline(fg::RenderDevice* device, ReSTIRGIPassState& state) +{ + constexpr u32 kWaterPipeVersion = 10; + static u32 s_waterPipeVersion = 0; + if (state.waterPipeline && state.waterLayout && s_waterPipeVersion == kWaterPipeVersion) + return; + + state.waterPipeline = nullptr; + state.waterLayout = nullptr; + s_waterPipeVersion = 0; + + auto& cache = GetPassResourceCache(); + nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); + BindingSetBuilder::InvalidateReflectionCache(); + auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_water_rt"); + if (!csResult.handle || !csResult.reflection) { + Msg("! [ReSTIR GI] restir_water_rt shader missing"); + return; + } + + nvrhi::IBindingLayout* bindlessLayout = nullptr; + if (auto* backend = device->GetBackend()) + bindlessLayout = backend->GetBindlessLayout(); + + state.waterLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Water_RT_v10", *csResult.reflection, nvDevice); + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = csResult.handle; + if (bindlessLayout) + pipeDesc.bindingLayouts = { state.waterLayout, bindlessLayout }; + else + pipeDesc.bindingLayouts = { state.waterLayout }; + state.waterPipeline = nvDevice->createComputePipeline(pipeDesc); + if (state.waterPipeline) + s_waterPipeVersion = kWaterPipeVersion; +} + static void InitializeResources(fg::RenderDevice* device, ReSTIRGIPassState& state) { - if (state.initialized) return; + if (state.initialized && state.enabled) { + EnsureVolumetricPipeline(device, state); + EnsureWaterPipeline(device, state); + return; + } auto& cache = GetPassResourceCache(); nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); CreatePlaceholders(nvDevice); - nvrhi::SamplerDesc samplerDesc; - samplerDesc.setAllFilters(true); - samplerDesc.setAllAddressModes(nvrhi::SamplerAddressMode::Repeat); - state.sampler = cache.GetOrCreateSampler("RTGI", samplerDesc, nvDevice); + if (!state.sampler) { + nvrhi::SamplerDesc samplerDesc; + samplerDesc.setAllFilters(true); + samplerDesc.setAllAddressModes(nvrhi::SamplerAddressMode::Repeat); + state.sampler = cache.GetOrCreateSampler("RTGI", samplerDesc, nvDevice); + } + + if (!state.cb) { + state.cb = cache.GetOrCreateVolatileCB("RTGI", "RTGI_CB", + (u32)std::max({ sizeof(ReSTIRGICB), sizeof(TemporalCB), sizeof(SpatialCB), sizeof(DITemporalCB), + sizeof(DISpatialCB), sizeof(DIShadeCB), sizeof(WaterCB), sizeof(CompositeCB), sizeof(BlurCB), + sizeof(TemporalFilterCB), sizeof(RTVolCB) }), device); + } - state.cb = cache.GetOrCreateVolatileCB("RTGI", "RTGI_CB", - (u32)std::max({ sizeof(ReSTIRGICB), sizeof(TemporalCB), sizeof(CompositeCB) }), device); + nvrhi::IBindingLayout* bindlessLayout = nullptr; + if (auto* backend = device->GetBackend()) + bindlessLayout = backend->GetBindlessLayout(); - // --- Initial pass layout (RT + bindless) --- - { + if (!state.initialPipeline && !state.initialLoadFailed) { auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_gi_initial"); - if (csResult.handle) { - state.initialLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Initial", *csResult.reflection, nvDevice); -#if defined(XR_PLATFORM_WINDOWS) - auto* backend = dynamic_cast(GEnv.Backend); - nvrhi::IBindingLayout* bindlessLayout = backend ? backend->GetBindlessLayout() : nullptr; -#else - nvrhi::IBindingLayout* bindlessLayout = nullptr; -#endif + if (csResult.handle && csResult.reflection) { + state.initialLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Initial_v4_Emissive", *csResult.reflection, nvDevice); + if (!state.initialLayout) { + Msg("! [ReSTIR GI] initial binding layout create failed"); + state.initialLoadFailed = true; + } else { + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = csResult.handle; + if (bindlessLayout) + pipeDesc.bindingLayouts = { state.initialLayout, bindlessLayout }; + else + pipeDesc.bindingLayouts = { state.initialLayout }; + state.initialPipeline = nvDevice->createComputePipeline(pipeDesc); + if (!state.initialPipeline) { + Msg("! [ReSTIR GI] initial createComputePipeline failed (bindless=%s)", + bindlessLayout ? "yes" : "no"); + state.initialLoadFailed = true; + } + } + } else { + state.initialLoadFailed = true; + Msg("! [ReSTIR GI] restir_gi_initial shader missing (handle=%s refl=%s)", + csResult.handle ? "ok" : "null", + csResult.reflection ? "ok" : "null"); + } + } + if (state.initialPipeline) + state.initialLoadFailed = false; + if (!state.temporalPipeline) { + auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_gi_temporal"); + if (csResult.handle && csResult.reflection) { + state.temporalLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Temporal_v2", *csResult.reflection, nvDevice); nvrhi::ComputePipelineDesc pipeDesc; pipeDesc.CS = csResult.handle; if (bindlessLayout) - pipeDesc.bindingLayouts = { state.initialLayout, bindlessLayout }; + pipeDesc.bindingLayouts = { state.temporalLayout, bindlessLayout }; else - pipeDesc.bindingLayouts = { state.initialLayout }; - state.initialPipeline = nvDevice->createComputePipeline(pipeDesc); + pipeDesc.bindingLayouts = { state.temporalLayout }; + state.temporalPipeline = nvDevice->createComputePipeline(pipeDesc); } } - // --- Temporal pass layout --- - { - auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_gi_temporal"); - if (csResult.handle) { - state.temporalLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Temporal", *csResult.reflection, nvDevice); + if (!state.spatialPipeline) { + auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_gi_spatial"); + if (csResult.handle && csResult.reflection) { + state.spatialLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Spatial_v2", *csResult.reflection, nvDevice); nvrhi::ComputePipelineDesc pipeDesc; pipeDesc.CS = csResult.handle; - pipeDesc.bindingLayouts = { state.temporalLayout }; - state.temporalPipeline = nvDevice->createComputePipeline(pipeDesc); + if (bindlessLayout) + pipeDesc.bindingLayouts = { state.spatialLayout, bindlessLayout }; + else + pipeDesc.bindingLayouts = { state.spatialLayout }; + state.spatialPipeline = nvDevice->createComputePipeline(pipeDesc); } } - // --- Composite pass layout --- - { + if (!state.diTemporalPipeline) { + auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_di_temporal"); + if (csResult.handle && csResult.reflection) { + state.diTemporalLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_DI_Temporal_v3", *csResult.reflection, nvDevice); + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = csResult.handle; + if (bindlessLayout) + pipeDesc.bindingLayouts = { state.diTemporalLayout, bindlessLayout }; + else + pipeDesc.bindingLayouts = { state.diTemporalLayout }; + state.diTemporalPipeline = nvDevice->createComputePipeline(pipeDesc); + } + } + + if (!state.diSpatialPipeline) { + auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_di_spatial"); + if (csResult.handle && csResult.reflection) { + state.diSpatialLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_DI_Spatial_v5", *csResult.reflection, nvDevice); + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = csResult.handle; + if (bindlessLayout) + pipeDesc.bindingLayouts = { state.diSpatialLayout, bindlessLayout }; + else + pipeDesc.bindingLayouts = { state.diSpatialLayout }; + state.diSpatialPipeline = nvDevice->createComputePipeline(pipeDesc); + } + } + + if (!state.diShadePipeline) { + auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_di_shade"); + if (csResult.handle && csResult.reflection) { + state.diShadeLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_DI_Shade_v6_EmitterSkip", *csResult.reflection, nvDevice); + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = csResult.handle; + if (bindlessLayout) + pipeDesc.bindingLayouts = { state.diShadeLayout, bindlessLayout }; + else + pipeDesc.bindingLayouts = { state.diShadeLayout }; + state.diShadePipeline = nvDevice->createComputePipeline(pipeDesc); + } + } + + EnsureWaterPipeline(device, state); + + if (!state.compositePipeline) { auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_gi_composite"); - if (csResult.handle) { - state.compositeLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Composite", *csResult.reflection, nvDevice); + if (csResult.handle && csResult.reflection) { + state.compositeLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Composite_v6", *csResult.reflection, nvDevice); nvrhi::ComputePipelineDesc pipeDesc; pipeDesc.CS = csResult.handle; pipeDesc.bindingLayouts = { state.compositeLayout }; @@ -158,62 +491,146 @@ static void InitializeResources(fg::RenderDevice* device, ReSTIRGIPassState& sta } } - state.enabled = state.initialPipeline && state.temporalPipeline && state.compositePipeline; + if (!state.blurPipeline) { + auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_gi_blur"); + if (csResult.handle && csResult.reflection) { + state.blurLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Blur4_GuideMark", *csResult.reflection, nvDevice); + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = csResult.handle; + pipeDesc.bindingLayouts = { state.blurLayout }; + state.blurPipeline = nvDevice->createComputePipeline(pipeDesc); + } + } + + if (!state.temporalFilterPipeline) { + auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_gi_temporal_filter"); + if (csResult.handle && csResult.reflection) { + state.temporalFilterLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_TempFilter", *csResult.reflection, nvDevice); + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = csResult.handle; + pipeDesc.bindingLayouts = { state.temporalFilterLayout }; + state.temporalFilterPipeline = nvDevice->createComputePipeline(pipeDesc); + } + } + + EnsureVolumetricPipeline(device, state); + + const bool wasEnabled = state.enabled; + state.enabled = state.initialPipeline && state.temporalPipeline && state.compositePipeline && state.diShadePipeline; state.initialized = true; - if (state.enabled) - Msg("* [ReSTIR GI] All pipelines created successfully"); - else - Msg("! [ReSTIR GI] Pipeline creation failed (initial=%s temporal=%s composite=%s)", - state.initialPipeline ? "ok" : "FAIL", - state.temporalPipeline ? "ok" : "FAIL", - state.compositePipeline ? "ok" : "FAIL"); + if (state.enabled && !wasEnabled) + Msg("* [ReSTIR GI] Pipelines ready (spatial=%s diTemp=%s diSpat=%s diShade=%s water=%s blur=%s vol=%s)", + state.spatialPipeline ? "ok" : "off", + state.diTemporalPipeline ? "ok" : "off", + state.diSpatialPipeline ? "ok" : "off", + state.diShadePipeline ? "ok" : "off", + state.waterPipeline ? "ok" : "off", + state.blurPipeline ? "ok" : "off", + state.volPipeline ? "ok" : "FAIL"); + else if (!state.enabled && !wasEnabled) + { + static bool s_loggedFail = false; + if (!s_loggedFail) + { + s_loggedFail = true; + Msg("! [ReSTIR GI] Pipeline creation failed (initial=%s temporal=%s composite=%s diShade=%s) — passthrough scene", + state.initialPipeline ? "ok" : "FAIL", + state.temporalPipeline ? "ok" : "FAIL", + state.compositePipeline ? "ok" : "FAIL", + state.diShadePipeline ? "ok" : "FAIL"); + } + } } -static void EnsurePersistentTextures(nvrhi::IDevice* nvDevice, ReSTIRGIPassState& state, u32 width, u32 height) +static bool EnsureIrradianceCache(nvrhi::IDevice* nvDevice, ReSTIRGIPassState& state) { - if (state.reservoirA[0] && state.texWidth == width && state.texHeight == height) - return; + const u32 wanted = (u32)std::max(0, ::ps_r_rt_gi_cache_size); + if (state.irradianceCache && state.irradianceCacheSize == wanted) + return true; + + state.irradianceCache = nullptr; + state.irradianceCacheSize = wanted; + + nvrhi::BufferDesc bufDesc; + bufDesc.debugName = "RTGI_IrradianceCache"; + const u32 entries = wanted > 0 ? wanted : 1u; + bufDesc.byteSize = (size_t)entries * 16u; + bufDesc.structStride = 16; + bufDesc.canHaveUAVs = true; + bufDesc.initialState = nvrhi::ResourceStates::UnorderedAccess; + bufDesc.keepInitialState = true; + state.irradianceCache = nvDevice->createBuffer(bufDesc); + if (!state.irradianceCache) { + Msg("! [ReSTIR GI] Irradiance cache buffer create failed"); + return false; + } + return true; +} - for (int i = 0; i < 2; i++) { - { - nvrhi::TextureDesc desc; - desc.debugName = i == 0 ? "RTGI_ReservoirA_0" : "RTGI_ReservoirA_1"; - desc.width = width; - desc.height = height; - desc.format = nvrhi::Format::RGBA32_FLOAT; - desc.isUAV = true; - desc.initialState = nvrhi::ResourceStates::UnorderedAccess; - desc.keepInitialState = true; - state.reservoirA[i] = nvDevice->createTexture(desc); - } - { - nvrhi::TextureDesc desc; - desc.debugName = i == 0 ? "RTGI_ReservoirB_0" : "RTGI_ReservoirB_1"; - desc.width = width; - desc.height = height; - desc.format = nvrhi::Format::RGBA32_FLOAT; - desc.isUAV = true; - desc.initialState = nvrhi::ResourceStates::UnorderedAccess; - desc.keepInitialState = true; - state.reservoirB[i] = nvDevice->createTexture(desc); - } +static bool EnsurePersistentTextures(nvrhi::IDevice* nvDevice, ReSTIRGIPassState& state, u32 width, u32 height) +{ + const bool sizeOk = state.reservoirA[0] && state.diReservoir[0] && state.reservoirC[0] && + state.specReservoirA[0] && state.texWidth == width && state.texHeight == height; + if (sizeOk) { + if (!EnsureIrradianceCache(nvDevice, state)) + return false; + return state.directLighting && state.noisyDiffuse && state.noisySpecular && + state.blurTemp && state.blurTempSpec && state.histDiffuse && state.histSpecular && state.hitDistance && + state.irradianceCache; } - { + auto makeTex = [&](const char* name, nvrhi::Format fmt) -> nvrhi::TextureHandle { nvrhi::TextureDesc desc; - desc.debugName = "RTGI_DirectLighting"; + desc.debugName = name; desc.width = width; desc.height = height; - desc.format = nvrhi::Format::RGBA16_FLOAT; + desc.format = fmt; desc.isUAV = true; desc.initialState = nvrhi::ResourceStates::UnorderedAccess; desc.keepInitialState = true; - state.directLighting = nvDevice->createTexture(desc); + return nvDevice->createTexture(desc); + }; + + for (int i = 0; i < 2; i++) { + state.reservoirA[i] = makeTex(i == 0 ? "RTGI_ReservoirA_0" : "RTGI_ReservoirA_1", nvrhi::Format::RGBA32_FLOAT); + state.reservoirB[i] = makeTex(i == 0 ? "RTGI_ReservoirB_0" : "RTGI_ReservoirB_1", nvrhi::Format::RGBA32_FLOAT); + state.reservoirC[i] = makeTex(i == 0 ? "RTGI_ReservoirC_0" : "RTGI_ReservoirC_1", nvrhi::Format::RG32_FLOAT); + state.specReservoirA[i] = makeTex(i == 0 ? "RTGI_SpecA_0" : "RTGI_SpecA_1", nvrhi::Format::RGBA32_FLOAT); + state.specReservoirB[i] = makeTex(i == 0 ? "RTGI_SpecB_0" : "RTGI_SpecB_1", nvrhi::Format::RGBA32_FLOAT); + state.diReservoir[i] = makeTex(i == 0 ? "RTGI_DIReservoir_0" : "RTGI_DIReservoir_1", nvrhi::Format::RGBA32_FLOAT); + if (!state.reservoirA[i] || !state.reservoirB[i] || !state.reservoirC[i] || + !state.specReservoirA[i] || !state.specReservoirB[i] || !state.diReservoir[i]) { + Msg("! [ReSTIR GI] Reservoir texture create failed"); + state.enabled = false; + return false; + } + } + + state.directLighting = makeTex("RTGI_DirectLighting", nvrhi::Format::RGBA16_FLOAT); + state.noisyDiffuse = makeTex("RTGI_NoisyDiffuse", nvrhi::Format::RGBA16_FLOAT); + state.noisySpecular = makeTex("RTGI_NoisySpecular", nvrhi::Format::RGBA16_FLOAT); + state.blurTemp = makeTex("RTGI_BlurTemp", nvrhi::Format::RGBA16_FLOAT); + state.blurTempSpec = makeTex("RTGI_BlurTempSpec", nvrhi::Format::RGBA16_FLOAT); + state.histDiffuse = makeTex("RTGI_HistDiffuse", nvrhi::Format::RGBA16_FLOAT); + state.histSpecular = makeTex("RTGI_HistSpecular", nvrhi::Format::RGBA16_FLOAT); + state.hitDistance = makeTex("RTGI_HitDistance", nvrhi::Format::R16_FLOAT); + + if (!EnsureIrradianceCache(nvDevice, state)) { + state.enabled = false; + return false; + } + + if (!state.directLighting || !state.noisyDiffuse || !state.noisySpecular || !state.blurTemp || + !state.blurTempSpec || !state.histDiffuse || !state.histSpecular || !state.hitDistance) { + Msg("! [ReSTIR GI] Persistent texture create failed — GI disabled"); + state.enabled = false; + return false; } state.texWidth = width; state.texHeight = height; + return true; } struct InitialPassData { @@ -233,6 +650,7 @@ struct InitialPassData { struct TemporalPassData { fg::RenderDevice* device; + RTAccelStructManager* accelMgr; ReSTIRGIPassState* state; VirtualResourceHandle depth; VirtualResourceHandle normal; @@ -254,6 +672,7 @@ struct CompositePassData { VirtualResourceHandle normal; VirtualResourceHandle baseColor; VirtualResourceHandle worldPos; + VirtualResourceHandle classifyWorldPos; VirtualResourceHandle sceneColorIn; VirtualResourceHandle sceneColor; CompositeCB cbData; @@ -279,7 +698,10 @@ ReSTIRGIOutput setupReSTIRGIPass( float giIntensity, u32 width, u32 height, ReSTIRGIPassState& state, - bool hasPrevFrameData) + bool hasPrevFrameData, + bool skipInTreeDenoise, + VirtualResourceHandle classifyWorldPos, + bool skipLightingTemporal) { InitializeResources(device, state); @@ -290,24 +712,19 @@ ReSTIRGIOutput setupReSTIRGIPass( outDesc.height = height; outDesc.format = nvrhi::Format::RGBA16_FLOAT; outDesc.isUAV = true; + outDesc.allowUAV = true; outDesc.isRenderTarget = true; outDesc.isTransient = true; VirtualResourceHandle outHandle = fg.CreateTexture("rtgi_SceneColor", outDesc); if (!state.enabled || !accelMgr || !accelMgr->IsReady()) { - auto& passData = fg.addCallbackPass( - "ReSTIR GI (disabled)", - [&](FrameGraph& builder, PassHandle passHandle, CompositePassData& data) { - RenderPassBuilder pb(builder, passHandle); - data.sceneColor = pb.write(outHandle, ResourceState::UnorderedAccess); - }, - [](const CompositePassData&, const FrameGraph&, fg::RenderContext*) {} - ); - return { passData.sceneColor }; + return { sceneColorIn, {}, {}, {} }; } nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); - EnsurePersistentTextures(nvDevice, state, width, height); + if (!EnsurePersistentTextures(nvDevice, state, width, height)) { + return { sceneColorIn, {}, {}, {} }; + } u32 writeIdx = state.currTemporalIdx; u32 readIdx = 1 - writeIdx; @@ -347,6 +764,7 @@ ReSTIRGIOutput setupReSTIRGIPass( ReSTIRGICB initialCB; initialCB.invViewProj = invViewProj; initialCB.prevViewProj = prevViewProj; + initialCB.worldToView = Device.mView; initialCB.cameraPos = { cameraPos.x, cameraPos.y, cameraPos.z, 0 }; initialCB.sunDir_intensity = { sunDir.x, sunDir.y, sunDir.z, sunIntensity }; initialCB.sunColor_skyWeight = { sunColor.x, sunColor.y, sunColor.z, skyWeight }; @@ -358,12 +776,40 @@ ReSTIRGIOutput setupReSTIRGIPass( initialCB.terrainBatchCount = batchCounts.terrain; initialCB.skinnedBatchStart = batchCounts.skinned > 0 ? batchCounts.identityStatic + batchCounts.terrain + batchCounts.transparent + batchCounts.instancedTotal - : 0; + : 0xFFFFFFFFu; initialCB.grassBatchStart = batchCounts.grass > 0 ? batchCounts.identityStatic + batchCounts.terrain + batchCounts.transparent + batchCounts.instancedTotal + batchCounts.skinned - : 0; + : 0xFFFFFFFFu; initialCB.detailAtlasIndex = accelMgr->GetDetailAtlasIndex(); - initialCB.pad[0] = initialCB.pad[1] = initialCB.pad[2] = 0; + initialCB.localLightSamples = (u32)std::max(0, ::ps_r_rt_gi_local_samples); + initialCB.bounces = (u32)std::clamp(::ps_r_rt_gi_bounces, 1, 2); + { + auto& clm = ClusteredLightManager::Instance(); + const u32 diCount = clm.GetDILightCount(); + const u32 baseCandidates = (u32)std::max(1, ::ps_r_rt_di_candidates); + const u32 adaptive = diCount > 0 + ? std::min(diCount, std::max(baseCandidates, (diCount + 3u) / 4u)) + : baseCandidates; + initialCB.diCandidates = std::min(adaptive, 32u); + initialCB.diSampleParams.set( + static_cast(diCount), + clm.GetDIPowerSum(), + 0.f, 0.f); + } + { + StaticGlobals clusterFill{}; + FillClusterParams(clusterFill); + initialCB.clusterParams = clusterFill.cluster_params; + initialCB.clusterScales = clusterFill.cluster_scales; + } + { + const auto& hemi = env.CurrentEnv.hemi_color; + initialCB.hemiColor.set(hemi.x, hemi.y, hemi.z, hemi.w); + } + initialCB.cacheSize = state.irradianceCacheSize; + initialCB.cacheCellSize = std::max(0.05f, ::ps_r_rt_gi_cache_cell); + initialCB.cacheMaxAge = 64u; + initialCB.pad1 = 0; ResourceDesc persistDesc; persistDesc.type = ResourceDesc::Type::Texture2D; @@ -381,17 +827,29 @@ ReSTIRGIOutput setupReSTIRGIPass( resDesc.format = nvrhi::Format::RGBA32_FLOAT; VirtualResourceHandle fgResA = fg.ImportTexture("rtgi_ResA_W", state.reservoirA[writeIdx].Get(), resDesc); VirtualResourceHandle fgResB = fg.ImportTexture("rtgi_ResB_W", state.reservoirB[writeIdx].Get(), resDesc); + auto resCDesc = persistDesc; + resCDesc.format = nvrhi::Format::RG32_FLOAT; + VirtualResourceHandle fgResC = fg.ImportTexture("rtgi_ResC_W", state.reservoirC[writeIdx].Get(), resCDesc); + VirtualResourceHandle fgSpecA = fg.ImportTexture("rtgi_SpecA_W", state.specReservoirA[writeIdx].Get(), resDesc); + VirtualResourceHandle fgSpecB = fg.ImportTexture("rtgi_SpecB_W", state.specReservoirB[writeIdx].Get(), resDesc); + VirtualResourceHandle fgDI = fg.ImportTexture("rtgi_DI_W", state.diReservoir[writeIdx].Get(), resDesc); + auto ndEarlyDesc = persistDesc; + ndEarlyDesc.format = nvrhi::Format::RGBA16_FLOAT; + VirtualResourceHandle fgNoisySpecEarly = fg.ImportTexture("rtgi_NoisySpecular_Init", state.noisySpecular.Get(), ndEarlyDesc); fg.GetRTRegistry().RegisterRT("rt_DirectLighting", fgDirectLighting); fg.GetRTRegistry().RegisterRT("rt_GI_ReservoirA", fgResA); fg.GetRTRegistry().RegisterRT("rt_GI_ReservoirB", fgResB); + fg.GetRTRegistry().RegisterRT("rt_GI_ReservoirC", fgResC); + fg.GetRTRegistry().RegisterRT("rt_GI_SpecA", fgSpecA); + fg.GetRTRegistry().RegisterRT("rt_GI_SpecB", fgSpecB); + fg.GetRTRegistry().RegisterRT("rt_DI_Reservoir", fgDI); + + u32 diIdx = writeIdx; - // ============================================ - // PASS 1: Initial Sample (RT shadow + bounce) - // ============================================ fg.addCallbackPass( "ReSTIR GI Initial", - [&, sky0Tex, sky1Tex, initialCB, writeIdx, fgDirectLighting, fgResA, fgResB](FrameGraph& builder, PassHandle passHandle, InitialPassData& data) { + [&, sky0Tex, sky1Tex, initialCB, writeIdx, fgDirectLighting, fgResA, fgResB, fgResC, fgSpecA, fgSpecB, fgDI, fgNoisySpecEarly](FrameGraph& builder, PassHandle passHandle, InitialPassData& data) { RenderPassBuilder pb(builder, passHandle); data.depth = pb.read(depth, ResourceState::ShaderResource); data.normal = pb.read(normal, ResourceState::ShaderResource); @@ -400,6 +858,11 @@ ReSTIRGIOutput setupReSTIRGIPass( pb.write(fgDirectLighting, ResourceState::UnorderedAccess); pb.write(fgResA, ResourceState::UnorderedAccess); pb.write(fgResB, ResourceState::UnorderedAccess); + pb.write(fgResC, ResourceState::UnorderedAccess); + pb.write(fgSpecA, ResourceState::UnorderedAccess); + pb.write(fgSpecB, ResourceState::UnorderedAccess); + pb.write(fgNoisySpecEarly, ResourceState::UnorderedAccess); + pb.write(fgDI, ResourceState::UnorderedAccess); pb.sideEffects(); data.device = device; data.accelMgr = accelMgr; @@ -426,6 +889,12 @@ ReSTIRGIOutput setupReSTIRGIPass( nvrhi::ITexture* directLit = data.state->directLighting.Get(); nvrhi::ITexture* resA = data.state->reservoirA[data.writeIdx].Get(); nvrhi::ITexture* resB = data.state->reservoirB[data.writeIdx].Get(); + nvrhi::ITexture* resC = data.state->reservoirC[data.writeIdx].Get(); + nvrhi::ITexture* specA = data.state->specReservoirA[data.writeIdx].Get(); + nvrhi::ITexture* specB = data.state->specReservoirB[data.writeIdx].Get(); + nvrhi::ITexture* diRes = data.state->diReservoir[data.writeIdx].Get(); + nvrhi::ITexture* noisySpec = data.state->noisySpecular.Get(); + nvrhi::IBuffer* irrCache = data.state->irradianceCache.Get(); auto* tlas = data.accelMgr->GetTLAS(); auto* batchInfo = data.accelMgr->GetBatchInfoBuffer(); auto* megaVB = data.accelMgr->GetMegaVB(); @@ -433,10 +902,10 @@ ReSTIRGIOutput setupReSTIRGIPass( auto* matBuf = data.accelMgr->GetMaterialBuffer(); auto* terrainBuf = data.accelMgr->GetTerrainMaterialBuffer(); - if (!sky0 || !sky1 || !directLit || !resA || !resB || - !tlas || !batchInfo || !megaVB || !megaIB || !matBuf || !terrainBuf) { - Msg("! [RTGI Initial] Null binding: sky0=%d sky1=%d directLit=%d resA=%d resB=%d tlas=%d batch=%d megaVB=%d megaIB=%d mat=%d terrain=%d", - !!sky0, !!sky1, !!directLit, !!resA, !!resB, !!tlas, !!batchInfo, !!megaVB, !!megaIB, !!matBuf, !!terrainBuf); + if (!sky0 || !sky1 || !directLit || !resA || !resB || !resC || !specA || !specB || !diRes || !noisySpec || + !irrCache || !tlas || !batchInfo || !megaVB || !megaIB || !matBuf || !terrainBuf) { + Msg("! [RTGI Initial] Null binding: sky0=%d sky1=%d directLit=%d resA=%d resB=%d di=%d tlas=%d batch=%d megaVB=%d megaIB=%d mat=%d terrain=%d", + !!sky0, !!sky1, !!directLit, !!resA, !!resB, !!diRes, !!tlas, !!batchInfo, !!megaVB, !!megaIB, !!matBuf, !!terrainBuf); return; } @@ -449,11 +918,19 @@ ReSTIRGIOutput setupReSTIRGIPass( cb.terrainBatchCount = bc.terrain; cb.skinnedBatchStart = bc.skinned > 0 ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal - : 0; + : 0xFFFFFFFFu; cb.grassBatchStart = bc.grass > 0 ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + bc.skinned - : 0; + : 0xFFFFFFFFu; cb.detailAtlasIndex = data.accelMgr->GetDetailAtlasIndex(); + cb.cacheSize = data.state->irradianceCacheSize; + { + auto& clm = ClusteredLightManager::Instance(); + cb.diSampleParams.set( + static_cast(clm.GetDILightCount()), + clm.GetDIPowerSum(), + 0.f, 0.f); + } cmdList->writeBuffer(data.state->cb, &cb, sizeof(ReSTIRGICB)); nvrhi::IBuffer* skinnedVB = data.accelMgr->GetSkinnedOutputVB(); @@ -478,8 +955,7 @@ ReSTIRGIOutput setupReSTIRGIPass( bsb.Texture("g_Sky0", sky0); bsb.Texture("g_Sky1", sky1); bsb.BufferSRV("g_SkinnedVB", skinnedVB); - bsb.BufferSRV("g_Materials", matBuf); - bsb.BufferSRV("g_TerrainMaterials", terrainBuf); + BindBindlessMaterialTables(bsb); bsb.BufferSRV("g_SkinnedIB", skinnedIB); bsb.BufferSRV("g_GrassVB", grassVB); bsb.BufferSRV("g_GrassIB", grassIB); @@ -487,9 +963,27 @@ ReSTIRGIOutput setupReSTIRGIPass( bsb.Texture("t_Normal", normalTex); bsb.Texture("t_BaseColor", baseColorTex); bsb.Texture("t_WorldPos", worldPosTex); + { + auto& clm = ClusteredLightManager::Instance(); + nvrhi::IBuffer* lightData = clm.GetLightDataBuffer(); + nvrhi::IBuffer* diIndices = clm.GetDILightIndicesBuffer(); + nvrhi::IBuffer* diCdf = clm.GetDILightCDFBuffer(); + if (!lightData) lightData = s_rtgiPlaceholderBuffer.Get(); + if (!diIndices) diIndices = s_rtgiPlaceholderBuffer.Get(); + if (!diCdf) diCdf = s_rtgiPlaceholderBuffer.Get(); + bsb.BufferSRV("g_LightData", lightData); + bsb.BufferSRV("g_DILightIndices", diIndices); + bsb.BufferSRV("g_DILightCDF", diCdf); + } bsb.TextureUAV("u_DirectLighting", directLit); bsb.TextureUAV("u_ReservoirA", resA); bsb.TextureUAV("u_ReservoirB", resB); + bsb.TextureUAV("u_NoisySpecular", noisySpec); + bsb.TextureUAV("u_DIReservoir", diRes); + bsb.TextureUAV("u_SpecReservoirA", specA); + bsb.TextureUAV("u_SpecReservoirB", specB); + bsb.TextureUAV("u_ReservoirC", resC); + bsb.BufferUAV("u_IrradianceCache", irrCache); auto& cache = GetPassResourceCache(); auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->initialLayout, nvDevice); if (!bindingSet) return; @@ -498,24 +992,17 @@ ReSTIRGIOutput setupReSTIRGIPass( cs.pipeline = data.state->initialPipeline; cs.bindings = { bindingSet }; -#if defined(XR_PLATFORM_WINDOWS) - auto* backend = dynamic_cast(GEnv.Backend); - if (backend) { - auto* bindlessTable = backend->GetBindlessDescriptorTable(); - if (bindlessTable) + if (auto* backend = data.device ? data.device->GetBackend() : nullptr) { + if (auto* bindlessTable = backend->GetBindlessDescriptorTable()) cs.addBindingSet(bindlessTable); } -#endif cmdList->setComputeState(cs); cmdList->dispatch((data.width + 7) / 8, (data.height + 7) / 8, 1); } ); - // ============================================ - // PASS 2: Temporal Resampling - // ============================================ - if (hasPrevFrameData && motionVectors.is_valid()) { + if (!skipLightingTemporal && hasPrevFrameData && motionVectors.is_valid()) { TemporalCB temporalCB; temporalCB.invViewProj = invViewProj; temporalCB.cameraPos = { cameraPos.x, cameraPos.y, cameraPos.z, 0 }; @@ -524,11 +1011,16 @@ ReSTIRGIOutput setupReSTIRGIPass( temporalCB.invScreenWidth = 1.0f / width; temporalCB.invScreenHeight = 1.0f / height; temporalCB.frameIndex = Device.dwFrame; - temporalCB.pad[0] = temporalCB.pad[1] = temporalCB.pad[2] = 0; + temporalCB.identityStaticCount = initialCB.identityStaticCount; + temporalCB.terrainBatchCount = initialCB.terrainBatchCount; + temporalCB.skinnedBatchStart = initialCB.skinnedBatchStart; + temporalCB.grassBatchStart = initialCB.grassBatchStart; + temporalCB.detailAtlasIndex = initialCB.detailAtlasIndex; + temporalCB.pad[0] = temporalCB.pad[1] = 0; fg.addCallbackPass( "ReSTIR GI Temporal", - [&, temporalCB, readIdx, writeIdx, fgResA, fgResB](FrameGraph& builder, PassHandle passHandle, TemporalPassData& data) { + [&, temporalCB, readIdx, writeIdx, fgResA, fgResB, fgResC, fgSpecA, fgSpecB](FrameGraph& builder, PassHandle passHandle, TemporalPassData& data) { RenderPassBuilder pb(builder, passHandle); data.depth = pb.read(depth, ResourceState::ShaderResource); data.normal = pb.read(normal, ResourceState::ShaderResource); @@ -541,8 +1033,12 @@ ReSTIRGIOutput setupReSTIRGIPass( data.motionVectors = pb.read(motionVectors, ResourceState::ShaderResource); pb.readWrite(fgResA, ResourceState::UnorderedAccess); pb.readWrite(fgResB, ResourceState::UnorderedAccess); + pb.readWrite(fgResC, ResourceState::UnorderedAccess); + pb.readWrite(fgSpecA, ResourceState::UnorderedAccess); + pb.readWrite(fgSpecB, ResourceState::UnorderedAccess); pb.sideEffects(); data.device = device; + data.accelMgr = accelMgr; data.state = &state; data.cbData = temporalCB; data.width = width; @@ -563,16 +1059,52 @@ ReSTIRGIOutput setupReSTIRGIPass( nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); nvrhi::ICommandList* cmdList = ctx->GetCommandList(); - cmdList->writeBuffer(data.state->cb, &data.cbData, sizeof(TemporalCB)); + TemporalCB cb = data.cbData; + if (data.accelMgr) { + const auto& bc = data.accelMgr->GetBatchCounts(); + cb.identityStaticCount = bc.identityStatic; + cb.terrainBatchCount = bc.terrain; + cb.skinnedBatchStart = bc.skinned > 0 + ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + : 0xFFFFFFFFu; + cb.grassBatchStart = bc.grass > 0 + ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + bc.skinned + : 0xFFFFFFFFu; + cb.detailAtlasIndex = data.accelMgr->GetDetailAtlasIndex(); + } + cmdList->writeBuffer(data.state->cb, &cb, sizeof(TemporalCB)); auto* shaderLoader = GEnv.Render->GetShaderLoader(); auto* csReflection = shaderLoader->GetCachedReflection("restir_gi_temporal", ".cs"); if (!csReflection) return; + auto* tlas = data.accelMgr ? data.accelMgr->GetTLAS() : nullptr; + if (!tlas) return; + nvrhi::IBuffer* batchInfo = data.accelMgr->GetBatchInfoBuffer(); + nvrhi::IBuffer* megaVB = data.accelMgr->GetMegaVB(); + nvrhi::IBuffer* megaIB = data.accelMgr->GetMegaIB(); + nvrhi::IBuffer* grassVB = data.accelMgr->GetGrassOutputVB(); + nvrhi::IBuffer* grassIB = data.accelMgr->GetGrassIB(); + if (!batchInfo) batchInfo = s_rtgiPlaceholderBuffer.Get(); + if (!megaVB) megaVB = s_rtgiPlaceholderBuffer.Get(); + if (!megaIB) megaIB = s_rtgiPlaceholderBuffer.Get(); + if (!grassVB) grassVB = s_rtgiPlaceholderBuffer.Get(); + if (!grassIB) grassIB = s_rtgiPlaceholderBuffer.Get(); + framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRGI.Temporal"); bsb.ConstantBuffer("ReSTIRTemporalParams", data.state->cb); + BindBindlessMaterialTables(bsb); + bsb.AccelStruct("g_SceneTLAS", tlas); + bsb.BufferSRV("g_BatchInfo", batchInfo); + bsb.BufferSRV("g_MegaVB", megaVB); + bsb.BufferSRV("g_MegaIB", megaIB); + bsb.BufferSRV("g_GrassVB", grassVB); + bsb.BufferSRV("g_GrassIB", grassIB); bsb.Texture("t_PrevReservoirA", data.state->reservoirA[data.readIdx]); bsb.Texture("t_PrevReservoirB", data.state->reservoirB[data.readIdx]); + bsb.Texture("t_PrevReservoirC", data.state->reservoirC[data.readIdx]); + bsb.Texture("t_PrevSpecA", data.state->specReservoirA[data.readIdx]); + bsb.Texture("t_PrevSpecB", data.state->specReservoirB[data.readIdx]); bsb.Texture("t_MotionVectors", mvTex); bsb.Texture("t_Depth", depthTex); bsb.Texture("t_Normal", normalTex); @@ -582,6 +1114,9 @@ ReSTIRGIOutput setupReSTIRGIPass( bsb.Texture("t_PrevWorldPos", prevWorldPosTex); bsb.TextureUAV("u_ReservoirA", data.state->reservoirA[data.writeIdx]); bsb.TextureUAV("u_ReservoirB", data.state->reservoirB[data.writeIdx]); + bsb.TextureUAV("u_ReservoirC", data.state->reservoirC[data.writeIdx]); + bsb.TextureUAV("u_SpecReservoirA", data.state->specReservoirA[data.writeIdx]); + bsb.TextureUAV("u_SpecReservoirB", data.state->specReservoirB[data.writeIdx]); auto& cache = GetPassResourceCache(); auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->temporalLayout, nvDevice); if (!bindingSet) return; @@ -589,6 +1124,10 @@ ReSTIRGIOutput setupReSTIRGIPass( nvrhi::ComputeState cs; cs.pipeline = data.state->temporalPipeline; cs.bindings = { bindingSet }; + if (auto* backend = data.device ? data.device->GetBackend() : nullptr) { + if (auto* bindlessTable = backend->GetBindlessDescriptorTable()) + cs.addBindingSet(bindlessTable); + } cmdList->setComputeState(cs); cmdList->dispatch((data.width + 7) / 8, (data.height + 7) / 8, 1); @@ -596,52 +1135,621 @@ ReSTIRGIOutput setupReSTIRGIPass( ); } - // ============================================ - // PASS 3: Composite (direct + indirect → scene) - // ============================================ - CompositeCB compositeCB; - compositeCB.invViewProj = invViewProj; - compositeCB.cameraPos = { cameraPos.x, cameraPos.y, cameraPos.z, 0 }; - compositeCB.screenWidth = (float)width; - compositeCB.screenHeight = (float)height; - compositeCB.giIntensity = giIntensity; - compositeCB.pad = 0; + if (!skipLightingTemporal && hasPrevFrameData && motionVectors.is_valid() && + state.diTemporalPipeline && state.diTemporalLayout) { + DITemporalCB diTempCB{}; + diTempCB.worldToView = Device.mView; + diTempCB.cameraPos = { cameraPos.x, cameraPos.y, cameraPos.z, 0 }; + diTempCB.screenWidth = (float)width; + diTempCB.screenHeight = (float)height; + diTempCB.invScreenWidth = 1.0f / width; + diTempCB.invScreenHeight = 1.0f / height; + diTempCB.frameIndex = Device.dwFrame; + diTempCB.mMax = (u32)std::max(1, ::ps_r_rt_di_m_max); + diTempCB.pad[0] = diTempCB.pad[1] = 0.f; + diTempCB.clusterParams = initialCB.clusterParams; + diTempCB.clusterScales = initialCB.clusterScales; + + VirtualResourceHandle fgDIPrev = fg.ImportTexture("rtgi_DI_Prev", state.diReservoir[readIdx].Get(), resDesc); + + struct DITemporalPassData { + fg::RenderDevice* device; + ReSTIRGIPassState* state; + VirtualResourceHandle depth; + VirtualResourceHandle normal; + VirtualResourceHandle prevNormals; + VirtualResourceHandle baseColor; + VirtualResourceHandle worldPos; + VirtualResourceHandle prevWorldPos; + VirtualResourceHandle motionVectors; + DITemporalCB cbData; + u32 width, height; + u32 readIdx; + u32 writeIdx; + }; + + fg.addCallbackPass( + "ReSTIR DI Temporal", + [&, diTempCB, readIdx, writeIdx, fgDI, fgDIPrev](FrameGraph& builder, PassHandle passHandle, DITemporalPassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.normal = pb.read(normal, ResourceState::ShaderResource); + if (prevNormals.is_valid()) + data.prevNormals = pb.read(prevNormals, ResourceState::ShaderResource); + data.baseColor = pb.read(baseColor, ResourceState::ShaderResource); + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); + if (prevWorldPos.is_valid()) + data.prevWorldPos = pb.read(prevWorldPos, ResourceState::ShaderResource); + data.motionVectors = pb.read(motionVectors, ResourceState::ShaderResource); + pb.read(fgDIPrev, ResourceState::ShaderResource); + pb.readWrite(fgDI, ResourceState::UnorderedAccess); + pb.sideEffects(); + data.device = device; + data.state = &state; + data.cbData = diTempCB; + data.width = width; + data.height = height; + data.readIdx = readIdx; + data.writeIdx = writeIdx; + }, + [](const DITemporalPassData& data, const FrameGraph& fg, fg::RenderContext* ctx) { + auto* depthTex = fg.GetPhysicalTexture(data.depth); + auto* normalTex = fg.GetPhysicalTexture(data.normal); + auto* prevNormalsTex = data.prevNormals.is_valid() ? fg.GetPhysicalTexture(data.prevNormals) : normalTex; + auto* baseColorTex = fg.GetPhysicalTexture(data.baseColor); + auto* worldPosTex = fg.GetPhysicalTexture(data.worldPos); + auto* prevWorldPosTex = data.prevWorldPos.is_valid() ? fg.GetPhysicalTexture(data.prevWorldPos) : worldPosTex; + auto* mvTex = fg.GetPhysicalTexture(data.motionVectors); + if (!depthTex || !normalTex || !baseColorTex || !worldPosTex || !mvTex) return; - auto& compositeData = fg.addCallbackPass( - "ReSTIR GI Composite", - [&, compositeCB, writeIdx, outHandle, fgDirectLighting, fgResA, fgResB](FrameGraph& builder, PassHandle passHandle, CompositePassData& data) { - RenderPassBuilder pb(builder, passHandle); - data.depth = pb.read(depth, ResourceState::ShaderResource); - data.normal = pb.read(normal, ResourceState::ShaderResource); - data.baseColor = pb.read(baseColor, ResourceState::ShaderResource); - data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); - data.sceneColorIn = pb.read(sceneColorIn, ResourceState::ShaderResource); - pb.read(fgDirectLighting, ResourceState::ShaderResource); - pb.read(fgResA, ResourceState::ShaderResource); - pb.read(fgResB, ResourceState::ShaderResource); - data.sceneColor = pb.write(outHandle, ResourceState::UnorderedAccess); - data.device = device; - data.state = &state; + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + cmdList->writeBuffer(data.state->cb, &data.cbData, sizeof(DITemporalCB)); + + auto* csReflection = GEnv.Render->GetShaderLoader()->GetCachedReflection("restir_di_temporal", ".cs"); + if (!csReflection) return; + + nvrhi::IBuffer* lightData = ClusteredLightManager::Instance().GetLightDataBuffer(); + if (!lightData) lightData = s_rtgiPlaceholderBuffer.Get(); + + framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRDI.Temporal"); + bsb.ConstantBuffer("ReSTIRDITemporalParams", data.state->cb); + BindBindlessMaterialTables(bsb); + bsb.BufferSRV("g_LightData", lightData); + bsb.Texture("t_PrevDI", data.state->diReservoir[data.readIdx]); + bsb.Texture("t_MotionVectors", mvTex); + bsb.Texture("t_Depth", depthTex); + bsb.Texture("t_PrevNormal", prevNormalsTex); + bsb.Texture("t_BaseColor", baseColorTex); + bsb.Texture("t_WorldPos", worldPosTex); + bsb.Texture("t_PrevWorldPos", prevWorldPosTex); + bsb.Texture("t_Normal", normalTex); + bsb.TextureUAV("u_DIReservoir", data.state->diReservoir[data.writeIdx]); + auto& cache = GetPassResourceCache(); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->diTemporalLayout, nvDevice); + if (!bindingSet) return; + + nvrhi::ComputeState cs; + cs.pipeline = data.state->diTemporalPipeline; + cs.bindings = { bindingSet }; + if (auto* backend = data.device ? data.device->GetBackend() : nullptr) { + if (auto* bindlessTable = backend->GetBindlessDescriptorTable()) + cs.addBindingSet(bindlessTable); + } + cmdList->setComputeState(cs); + cmdList->dispatch((data.width + 7) / 8, (data.height + 7) / 8, 1); + } + ); + } + + u32 compositeReservoirIdx = writeIdx; + if (state.spatialPipeline && ::ps_r_rt_gi_spatial_samples > 0) { + SpatialCB spatialCB; + spatialCB.invViewProj = invViewProj; + spatialCB.cameraPos = { cameraPos.x, cameraPos.y, cameraPos.z, 0 }; + spatialCB.screenWidth = (float)width; + spatialCB.screenHeight = (float)height; + spatialCB.invScreenWidth = 1.0f / width; + spatialCB.invScreenHeight = 1.0f / height; + spatialCB.frameIndex = Device.dwFrame; + spatialCB.spatialSamples = (u32)::ps_r_rt_gi_spatial_samples; + spatialCB.spatialRadius = ::ps_r_rt_gi_spatial_radius; + spatialCB.mMax = (u32)std::max(1, ::ps_r_rt_gi_m_max); + spatialCB.identityStaticCount = initialCB.identityStaticCount; + spatialCB.terrainBatchCount = initialCB.terrainBatchCount; + spatialCB.skinnedBatchStart = initialCB.skinnedBatchStart; + spatialCB.grassBatchStart = initialCB.grassBatchStart; + spatialCB.detailAtlasIndex = initialCB.detailAtlasIndex; + spatialCB.pad[0] = spatialCB.pad[1] = spatialCB.pad[2] = 0; + + VirtualResourceHandle fgResAOut = fg.ImportTexture("rtgi_ResA_S", state.reservoirA[readIdx].Get(), resDesc); + VirtualResourceHandle fgResBOut = fg.ImportTexture("rtgi_ResB_S", state.reservoirB[readIdx].Get(), resDesc); + VirtualResourceHandle fgResCOut = fg.ImportTexture("rtgi_ResC_S", state.reservoirC[readIdx].Get(), resCDesc); + VirtualResourceHandle fgSpecAOut = fg.ImportTexture("rtgi_SpecA_S", state.specReservoirA[readIdx].Get(), resDesc); + VirtualResourceHandle fgSpecBOut = fg.ImportTexture("rtgi_SpecB_S", state.specReservoirB[readIdx].Get(), resDesc); + + struct SpatialPassData { + fg::RenderDevice* device; + RTAccelStructManager* accelMgr; + ReSTIRGIPassState* state; + VirtualResourceHandle depth; + VirtualResourceHandle normal; + VirtualResourceHandle baseColor; + VirtualResourceHandle worldPos; + SpatialCB cbData; + u32 width, height; + u32 srcIdx; + u32 dstIdx; + }; + + fg.addCallbackPass( + "ReSTIR GI Spatial", + [&, spatialCB, writeIdx, readIdx, fgResA, fgResB, fgResC, fgSpecA, fgSpecB, fgResAOut, fgResBOut, fgResCOut, fgSpecAOut, fgSpecBOut](FrameGraph& builder, PassHandle passHandle, SpatialPassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.normal = pb.read(normal, ResourceState::ShaderResource); + data.baseColor = pb.read(baseColor, ResourceState::ShaderResource); + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); + pb.read(fgResA, ResourceState::ShaderResource); + pb.read(fgResB, ResourceState::ShaderResource); + pb.read(fgResC, ResourceState::ShaderResource); + pb.read(fgSpecA, ResourceState::ShaderResource); + pb.read(fgSpecB, ResourceState::ShaderResource); + pb.write(fgResAOut, ResourceState::UnorderedAccess); + pb.write(fgResBOut, ResourceState::UnorderedAccess); + pb.write(fgResCOut, ResourceState::UnorderedAccess); + pb.write(fgSpecAOut, ResourceState::UnorderedAccess); + pb.write(fgSpecBOut, ResourceState::UnorderedAccess); + pb.sideEffects(); + data.device = device; + data.accelMgr = accelMgr; + data.state = &state; + data.cbData = spatialCB; + data.width = width; + data.height = height; + data.srcIdx = writeIdx; + data.dstIdx = readIdx; + }, + [](const SpatialPassData& data, const FrameGraph& fg, fg::RenderContext* ctx) { + auto* depthTex = fg.GetPhysicalTexture(data.depth); + auto* normalTex = fg.GetPhysicalTexture(data.normal); + auto* baseColorTex = fg.GetPhysicalTexture(data.baseColor); + auto* worldPosTex = fg.GetPhysicalTexture(data.worldPos); + if (!depthTex || !normalTex || !baseColorTex || !worldPosTex) return; + + nvrhi::ITexture* srcA = data.state->reservoirA[data.srcIdx].Get(); + nvrhi::ITexture* srcB = data.state->reservoirB[data.srcIdx].Get(); + nvrhi::ITexture* srcC = data.state->reservoirC[data.srcIdx].Get(); + nvrhi::ITexture* srcSpecA = data.state->specReservoirA[data.srcIdx].Get(); + nvrhi::ITexture* srcSpecB = data.state->specReservoirB[data.srcIdx].Get(); + nvrhi::ITexture* dstA = data.state->reservoirA[data.dstIdx].Get(); + nvrhi::ITexture* dstB = data.state->reservoirB[data.dstIdx].Get(); + nvrhi::ITexture* dstC = data.state->reservoirC[data.dstIdx].Get(); + nvrhi::ITexture* dstSpecA = data.state->specReservoirA[data.dstIdx].Get(); + nvrhi::ITexture* dstSpecB = data.state->specReservoirB[data.dstIdx].Get(); + auto* tlas = data.accelMgr ? data.accelMgr->GetTLAS() : nullptr; + if (!srcA || !srcB || !srcC || !srcSpecA || !srcSpecB || !dstA || !dstB || !dstC || !dstSpecA || !dstSpecB || !tlas) return; + + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + + SpatialCB cb = data.cbData; + if (data.accelMgr) { + const auto& bc = data.accelMgr->GetBatchCounts(); + cb.identityStaticCount = bc.identityStatic; + cb.terrainBatchCount = bc.terrain; + cb.skinnedBatchStart = bc.skinned > 0 + ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + : 0xFFFFFFFFu; + cb.grassBatchStart = bc.grass > 0 + ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + bc.skinned + : 0xFFFFFFFFu; + cb.detailAtlasIndex = data.accelMgr->GetDetailAtlasIndex(); + } + cmdList->writeBuffer(data.state->cb, &cb, sizeof(SpatialCB)); + + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* csReflection = shaderLoader->GetCachedReflection("restir_gi_spatial", ".cs"); + if (!csReflection) return; + + nvrhi::IBuffer* batchInfo = data.accelMgr->GetBatchInfoBuffer(); + nvrhi::IBuffer* megaVB = data.accelMgr->GetMegaVB(); + nvrhi::IBuffer* megaIB = data.accelMgr->GetMegaIB(); + nvrhi::IBuffer* grassVB = data.accelMgr->GetGrassOutputVB(); + nvrhi::IBuffer* grassIB = data.accelMgr->GetGrassIB(); + if (!batchInfo) batchInfo = s_rtgiPlaceholderBuffer.Get(); + if (!megaVB) megaVB = s_rtgiPlaceholderBuffer.Get(); + if (!megaIB) megaIB = s_rtgiPlaceholderBuffer.Get(); + if (!grassVB) grassVB = s_rtgiPlaceholderBuffer.Get(); + if (!grassIB) grassIB = s_rtgiPlaceholderBuffer.Get(); + + framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRGI.Spatial"); + bsb.ConstantBuffer("ReSTIRSpatialParams", data.state->cb); + BindBindlessMaterialTables(bsb); + bsb.AccelStruct("g_SceneTLAS", tlas); + bsb.BufferSRV("g_BatchInfo", batchInfo); + bsb.BufferSRV("g_MegaVB", megaVB); + bsb.BufferSRV("g_MegaIB", megaIB); + bsb.BufferSRV("g_GrassVB", grassVB); + bsb.BufferSRV("g_GrassIB", grassIB); + bsb.Texture("t_Depth", depthTex); + bsb.Texture("t_Normal", normalTex); + bsb.Texture("t_BaseColor", baseColorTex); + bsb.Texture("t_WorldPos", worldPosTex); + bsb.Texture("t_ReservoirA", srcA); + bsb.Texture("t_ReservoirB", srcB); + bsb.Texture("t_ReservoirC", srcC); + bsb.Texture("t_SpecA", srcSpecA); + bsb.Texture("t_SpecB", srcSpecB); + bsb.TextureUAV("u_ReservoirA", dstA); + bsb.TextureUAV("u_ReservoirB", dstB); + bsb.TextureUAV("u_ReservoirC", dstC); + bsb.TextureUAV("u_SpecReservoirA", dstSpecA); + bsb.TextureUAV("u_SpecReservoirB", dstSpecB); + auto& cache = GetPassResourceCache(); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->spatialLayout, nvDevice); + if (!bindingSet) return; + + nvrhi::ComputeState cs; + cs.pipeline = data.state->spatialPipeline; + cs.bindings = { bindingSet }; + if (auto* backend = data.device ? data.device->GetBackend() : nullptr) { + if (auto* bindlessTable = backend->GetBindlessDescriptorTable()) + cs.addBindingSet(bindlessTable); + } + cmdList->setComputeState(cs); + cmdList->dispatch((data.width + 7) / 8, (data.height + 7) / 8, 1); + } + ); + + compositeReservoirIdx = readIdx; + fgResA = fgResAOut; + fgResB = fgResBOut; + fgResC = fgResCOut; + fgSpecA = fgSpecAOut; + fgSpecB = fgSpecBOut; + } + + if (state.diSpatialPipeline && state.diSpatialLayout && ::ps_r_rt_di_spatial_samples > 0) { + DISpatialCB diSpatCB{}; + diSpatCB.worldToView = Device.mView; + diSpatCB.cameraPos = { cameraPos.x, cameraPos.y, cameraPos.z, 0 }; + diSpatCB.screenWidth = (float)width; + diSpatCB.screenHeight = (float)height; + diSpatCB.invScreenWidth = 1.0f / width; + diSpatCB.invScreenHeight = 1.0f / height; + diSpatCB.frameIndex = Device.dwFrame; + diSpatCB.spatialSamples = (u32)::ps_r_rt_di_spatial_samples; + diSpatCB.spatialRadius = ::ps_r_rt_di_spatial_radius; + diSpatCB.mMax = (u32)std::max(1, ::ps_r_rt_di_m_max); + diSpatCB.clusterParams = initialCB.clusterParams; + diSpatCB.clusterScales = initialCB.clusterScales; + diSpatCB.grassBatchStart = initialCB.grassBatchStart; + diSpatCB.detailAtlasIndex = initialCB.detailAtlasIndex; + diSpatCB.identityStaticCount = initialCB.identityStaticCount; + diSpatCB.terrainBatchCount = initialCB.terrainBatchCount; + diSpatCB.skinnedBatchStart = initialCB.skinnedBatchStart; + diSpatCB.pad0 = 0; + + VirtualResourceHandle fgDIOut = fg.ImportTexture("rtgi_DI_S", state.diReservoir[readIdx].Get(), resDesc); + + struct DISpatialPassData { + fg::RenderDevice* device; + RTAccelStructManager* accelMgr; + ReSTIRGIPassState* state; + VirtualResourceHandle depth; + VirtualResourceHandle normal; + VirtualResourceHandle baseColor; + VirtualResourceHandle worldPos; + DISpatialCB cbData; + u32 width, height; + u32 srcIdx; + u32 dstIdx; + }; + + fg.addCallbackPass( + "ReSTIR DI Spatial", + [&, diSpatCB, writeIdx, readIdx, fgDI, fgDIOut](FrameGraph& builder, PassHandle passHandle, DISpatialPassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.normal = pb.read(normal, ResourceState::ShaderResource); + data.baseColor = pb.read(baseColor, ResourceState::ShaderResource); + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); + pb.read(fgDI, ResourceState::ShaderResource); + pb.write(fgDIOut, ResourceState::UnorderedAccess); + pb.sideEffects(); + data.device = device; + data.accelMgr = accelMgr; + data.state = &state; + data.cbData = diSpatCB; + data.width = width; + data.height = height; + data.srcIdx = writeIdx; + data.dstIdx = readIdx; + }, + [](const DISpatialPassData& data, const FrameGraph& fg, fg::RenderContext* ctx) { + auto* depthTex = fg.GetPhysicalTexture(data.depth); + auto* normalTex = fg.GetPhysicalTexture(data.normal); + auto* baseColorTex = fg.GetPhysicalTexture(data.baseColor); + auto* worldPosTex = fg.GetPhysicalTexture(data.worldPos); + if (!depthTex || !normalTex || !baseColorTex || !worldPosTex) return; + + nvrhi::ITexture* srcDI = data.state->diReservoir[data.srcIdx].Get(); + nvrhi::ITexture* dstDI = data.state->diReservoir[data.dstIdx].Get(); + auto* tlas = data.accelMgr ? data.accelMgr->GetTLAS() : nullptr; + if (!srcDI || !dstDI || !tlas) return; + + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + + auto* csReflection = GEnv.Render->GetShaderLoader()->GetCachedReflection("restir_di_spatial", ".cs"); + if (!csReflection) return; + + nvrhi::IBuffer* lightData = ClusteredLightManager::Instance().GetLightDataBuffer(); + if (!lightData) lightData = s_rtgiPlaceholderBuffer.Get(); + nvrhi::IBuffer* batchInfo = data.accelMgr->GetBatchInfoBuffer(); + nvrhi::IBuffer* megaVB = data.accelMgr->GetMegaVB(); + nvrhi::IBuffer* megaIB = data.accelMgr->GetMegaIB(); + nvrhi::IBuffer* grassVB = data.accelMgr->GetGrassOutputVB(); + nvrhi::IBuffer* grassIB = data.accelMgr->GetGrassIB(); + if (!batchInfo) batchInfo = s_rtgiPlaceholderBuffer.Get(); + if (!megaVB) megaVB = s_rtgiPlaceholderBuffer.Get(); + if (!megaIB) megaIB = s_rtgiPlaceholderBuffer.Get(); + if (!grassVB) grassVB = s_rtgiPlaceholderBuffer.Get(); + if (!grassIB) grassIB = s_rtgiPlaceholderBuffer.Get(); + + { + const auto& bc = data.accelMgr->GetBatchCounts(); + DISpatialCB cb = data.cbData; + cb.identityStaticCount = bc.identityStatic; + cb.terrainBatchCount = bc.terrain; + cb.skinnedBatchStart = bc.skinned > 0 + ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + : 0xFFFFFFFFu; + cb.grassBatchStart = bc.grass > 0 + ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + bc.skinned + : 0xFFFFFFFFu; + cb.detailAtlasIndex = data.accelMgr->GetDetailAtlasIndex(); + cmdList->writeBuffer(data.state->cb, &cb, sizeof(DISpatialCB)); + } + + framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRDI.Spatial"); + bsb.ConstantBuffer("ReSTIRDISpatialParams", data.state->cb); + BindBindlessMaterialTables(bsb); + bsb.AccelStruct("g_SceneTLAS", tlas); + bsb.BufferSRV("g_BatchInfo", batchInfo); + bsb.BufferSRV("g_MegaVB", megaVB); + bsb.BufferSRV("g_MegaIB", megaIB); + bsb.BufferSRV("g_GrassVB", grassVB); + bsb.BufferSRV("g_GrassIB", grassIB); + bsb.BufferSRV("g_LightData", lightData); + bsb.Texture("t_SrcDI", srcDI); + bsb.Texture("t_Depth", depthTex); + bsb.Texture("t_BaseColor", baseColorTex); + bsb.Texture("t_WorldPos", worldPosTex); + bsb.Texture("t_Normal", normalTex); + bsb.TextureUAV("u_DIReservoir", dstDI); + auto& cache = GetPassResourceCache(); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->diSpatialLayout, nvDevice); + if (!bindingSet) return; + + nvrhi::ComputeState cs; + cs.pipeline = data.state->diSpatialPipeline; + cs.bindings = { bindingSet }; + if (auto* backend = data.device ? data.device->GetBackend() : nullptr) { + if (auto* bindlessTable = backend->GetBindlessDescriptorTable()) + cs.addBindingSet(bindlessTable); + } + cmdList->setComputeState(cs); + cmdList->dispatch((data.width + 7) / 8, (data.height + 7) / 8, 1); + } + ); + + diIdx = readIdx; + fgDI = fgDIOut; + } + + if (state.diShadePipeline && state.diShadeLayout) { + DIShadeCB diShadeCB{}; + diShadeCB.cameraPos = { cameraPos.x, cameraPos.y, cameraPos.z, 0 }; + diShadeCB.screenWidth = (float)width; + diShadeCB.screenHeight = (float)height; + diShadeCB.grassBatchStart = initialCB.grassBatchStart; + diShadeCB.detailAtlasIndex = initialCB.detailAtlasIndex; + diShadeCB.clusterParams = initialCB.clusterParams; + diShadeCB.identityStaticCount = initialCB.identityStaticCount; + diShadeCB.terrainBatchCount = initialCB.terrainBatchCount; + diShadeCB.skinnedBatchStart = initialCB.skinnedBatchStart; + diShadeCB.pad = 0; + + struct DIShadePassData { + fg::RenderDevice* device; + RTAccelStructManager* accelMgr; + ReSTIRGIPassState* state; + VirtualResourceHandle depth; + VirtualResourceHandle normal; + VirtualResourceHandle baseColor; + VirtualResourceHandle worldPos; + DIShadeCB cbData; + u32 width, height; + u32 diIdx; + }; + + fg.addCallbackPass( + "ReSTIR DI Shade", + [&, diShadeCB, diIdx, fgDI, fgDirectLighting](FrameGraph& builder, PassHandle passHandle, DIShadePassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.normal = pb.read(normal, ResourceState::ShaderResource); + data.baseColor = pb.read(baseColor, ResourceState::ShaderResource); + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); + pb.read(fgDI, ResourceState::ShaderResource); + pb.readWrite(fgDirectLighting, ResourceState::UnorderedAccess); + pb.sideEffects(); + data.device = device; + data.accelMgr = accelMgr; + data.state = &state; + data.cbData = diShadeCB; + data.width = width; + data.height = height; + data.diIdx = diIdx; + }, + [](const DIShadePassData& data, const FrameGraph& fg, fg::RenderContext* ctx) { + auto* depthTex = fg.GetPhysicalTexture(data.depth); + auto* normalTex = fg.GetPhysicalTexture(data.normal); + auto* baseColorTex = fg.GetPhysicalTexture(data.baseColor); + auto* worldPosTex = fg.GetPhysicalTexture(data.worldPos); + if (!depthTex || !normalTex || !baseColorTex || !worldPosTex) return; + + nvrhi::ITexture* diRes = data.state->diReservoir[data.diIdx].Get(); + nvrhi::ITexture* directLit = data.state->directLighting.Get(); + auto* tlas = data.accelMgr ? data.accelMgr->GetTLAS() : nullptr; + if (!diRes || !directLit || !tlas) return; + + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + + auto* csReflection = GEnv.Render->GetShaderLoader()->GetCachedReflection("restir_di_shade", ".cs"); + if (!csReflection) return; + + nvrhi::IBuffer* lightData = ClusteredLightManager::Instance().GetLightDataBuffer(); + if (!lightData) lightData = s_rtgiPlaceholderBuffer.Get(); + nvrhi::IBuffer* batchInfo = data.accelMgr->GetBatchInfoBuffer(); + nvrhi::IBuffer* megaVB = data.accelMgr->GetMegaVB(); + nvrhi::IBuffer* megaIB = data.accelMgr->GetMegaIB(); + nvrhi::IBuffer* grassVB = data.accelMgr->GetGrassOutputVB(); + nvrhi::IBuffer* grassIB = data.accelMgr->GetGrassIB(); + if (!batchInfo) batchInfo = s_rtgiPlaceholderBuffer.Get(); + if (!megaVB) megaVB = s_rtgiPlaceholderBuffer.Get(); + if (!megaIB) megaIB = s_rtgiPlaceholderBuffer.Get(); + if (!grassVB) grassVB = s_rtgiPlaceholderBuffer.Get(); + if (!grassIB) grassIB = s_rtgiPlaceholderBuffer.Get(); + + { + const auto& bc = data.accelMgr->GetBatchCounts(); + DIShadeCB cb = data.cbData; + cb.identityStaticCount = bc.identityStatic; + cb.terrainBatchCount = bc.terrain; + cb.skinnedBatchStart = bc.skinned > 0 + ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + : 0xFFFFFFFFu; + cb.grassBatchStart = bc.grass > 0 + ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + bc.skinned + : 0xFFFFFFFFu; + cb.detailAtlasIndex = data.accelMgr->GetDetailAtlasIndex(); + cmdList->writeBuffer(data.state->cb, &cb, sizeof(DIShadeCB)); + } + + framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRDI.Shade"); + bsb.ConstantBuffer("ReSTIRDIShadeParams", data.state->cb); + BindBindlessMaterialTables(bsb); + bsb.AccelStruct("g_SceneTLAS", tlas); + bsb.BufferSRV("g_BatchInfo", batchInfo); + bsb.BufferSRV("g_MegaVB", megaVB); + bsb.BufferSRV("g_MegaIB", megaIB); + bsb.BufferSRV("g_GrassVB", grassVB); + bsb.BufferSRV("g_GrassIB", grassIB); + bsb.BufferSRV("g_LightData", lightData); + bsb.Texture("t_DIReservoir", diRes); + bsb.Texture("t_Depth", depthTex); + bsb.Texture("t_BaseColor", baseColorTex); + bsb.Texture("t_WorldPos", worldPosTex); + bsb.Texture("t_Normal", normalTex); + bsb.TextureUAV("u_DirectLighting", directLit); + auto& cache = GetPassResourceCache(); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->diShadeLayout, nvDevice); + if (!bindingSet) return; + + nvrhi::ComputeState cs; + cs.pipeline = data.state->diShadePipeline; + cs.bindings = { bindingSet }; + if (auto* backend = data.device ? data.device->GetBackend() : nullptr) { + if (auto* bindlessTable = backend->GetBindlessDescriptorTable()) + cs.addBindingSet(bindlessTable); + } + cmdList->setComputeState(cs); + cmdList->dispatch((data.width + 7) / 8, (data.height + 7) / 8, 1); + } + ); + } + + auto ndDesc = persistDesc; + ndDesc.format = nvrhi::Format::RGBA16_FLOAT; + VirtualResourceHandle fgNoisyDiff = fg.ImportTexture("rtgi_NoisyDiffuse", state.noisyDiffuse.Get(), ndDesc); + VirtualResourceHandle fgNoisySpec = fgNoisySpecEarly; + VirtualResourceHandle fgBlurTemp = fg.ImportTexture("rtgi_BlurTemp", state.blurTemp.Get(), ndDesc); + VirtualResourceHandle fgBlurTempSpec = fg.ImportTexture("rtgi_BlurTempSpec", state.blurTempSpec.Get(), ndDesc); + VirtualResourceHandle fgHistDiff = fg.ImportTexture("rtgi_HistDiffuse", state.histDiffuse.Get(), ndDesc); + VirtualResourceHandle fgHistSpec = fg.ImportTexture("rtgi_HistSpecular", state.histSpecular.Get(), ndDesc); + auto hdDesc = persistDesc; + hdDesc.format = nvrhi::Format::R16_FLOAT; + VirtualResourceHandle fgHitDist = fg.ImportTexture("rtgi_HitDistance", state.hitDistance.Get(), hdDesc); + fg.GetRTRegistry().RegisterRT("rt_RT_Diffuse", fgNoisyDiff); + fg.GetRTRegistry().RegisterRT("rt_RT_Specular", fgNoisySpec); + fg.GetRTRegistry().RegisterRT("rt_RT_HitDist", fgHitDist); + + CompositeCB compositeCB; + compositeCB.invViewProj = invViewProj; + compositeCB.cameraPos = { cameraPos.x, cameraPos.y, cameraPos.z, 0 }; + compositeCB.screenWidth = (float)width; + compositeCB.screenHeight = (float)height; + compositeCB.giIntensity = giIntensity; + compositeCB.pad = 0; + { + StaticGlobals fogFill{}; + FillGlobalConstants(fogFill); + compositeCB.fogParams = fogFill.fog_params; + compositeCB.fogColor = fogFill.fog_color; + } + + VirtualResourceHandle classifyWP = classifyWorldPos.is_valid() ? classifyWorldPos : worldPos; + + auto& compositeData = fg.addCallbackPass( + "ReSTIR GI Composite", + [&, compositeCB, compositeReservoirIdx, outHandle, fgDirectLighting, fgResA, fgResB, fgSpecA, fgSpecB, fgNoisyDiff, fgNoisySpec, fgHitDist, classifyWP](FrameGraph& builder, PassHandle passHandle, CompositePassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.normal = pb.read(normal, ResourceState::ShaderResource); + data.baseColor = pb.read(baseColor, ResourceState::ShaderResource); + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); + data.classifyWorldPos = pb.read(classifyWP, ResourceState::ShaderResource); + data.sceneColorIn = pb.read(sceneColorIn, ResourceState::ShaderResource); + pb.read(fgDirectLighting, ResourceState::ShaderResource); + pb.read(fgResA, ResourceState::ShaderResource); + pb.read(fgResB, ResourceState::ShaderResource); + pb.read(fgSpecA, ResourceState::ShaderResource); + pb.read(fgSpecB, ResourceState::ShaderResource); + pb.read(fgNoisySpec, ResourceState::ShaderResource); + data.sceneColor = pb.write(outHandle, ResourceState::UnorderedAccess); + pb.write(fgNoisyDiff, ResourceState::UnorderedAccess); + pb.write(fgHitDist, ResourceState::UnorderedAccess); + data.device = device; + data.state = &state; data.cbData = compositeCB; data.width = width; data.height = height; - data.reservoirIdx = writeIdx; + data.reservoirIdx = compositeReservoirIdx; }, [](const CompositePassData& data, const FrameGraph& fg, fg::RenderContext* ctx) { auto* depthTex = fg.GetPhysicalTexture(data.depth); auto* normalTex = fg.GetPhysicalTexture(data.normal); auto* baseColorTex = fg.GetPhysicalTexture(data.baseColor); auto* worldPosTex = fg.GetPhysicalTexture(data.worldPos); + auto* classifyTex = fg.GetPhysicalTexture(data.classifyWorldPos); auto* sceneColorInTex = fg.GetPhysicalTexture(data.sceneColorIn); auto* outTex = fg.GetPhysicalTexture(data.sceneColor); - if (!depthTex || !normalTex || !baseColorTex || !worldPosTex || !sceneColorInTex || !outTex) return; + if (!depthTex || !normalTex || !baseColorTex || !worldPosTex || !classifyTex || !sceneColorInTex || !outTex) return; nvrhi::ITexture* directLit = data.state->directLighting.Get(); nvrhi::ITexture* resA = data.state->reservoirA[data.reservoirIdx].Get(); nvrhi::ITexture* resB = data.state->reservoirB[data.reservoirIdx].Get(); - if (!directLit || !resA || !resB) { - Msg("! [RTGI Composite] Null persistent texture: directLit=%d resA=%d resB=%d idx=%d", - !!directLit, !!resA, !!resB, data.reservoirIdx); + nvrhi::ITexture* specA = data.state->specReservoirA[data.reservoirIdx].Get(); + nvrhi::ITexture* specB = data.state->specReservoirB[data.reservoirIdx].Get(); + nvrhi::ITexture* noisyDiff = data.state->noisyDiffuse.Get(); + nvrhi::ITexture* noisySpec = data.state->noisySpecular.Get(); + nvrhi::ITexture* hitDist = data.state->hitDistance.Get(); + if (!directLit || !resA || !resB || !specA || !specB || !noisyDiff || !noisySpec || !hitDist) { + Msg("! [RTGI Composite] Null persistent texture"); return; } @@ -654,7 +1762,7 @@ ReSTIRGIOutput setupReSTIRGIPass( auto* csReflection = shaderLoader->GetCachedReflection("restir_gi_composite", ".cs"); if (!csReflection) return; - framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRGI.Spatial"); + framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRGI.Composite"); bsb.ConstantBuffer("CompositeParams", data.state->cb); bsb.Texture("t_DirectLighting", directLit); bsb.Texture("t_ReservoirA", resA); @@ -664,7 +1772,13 @@ ReSTIRGIOutput setupReSTIRGIPass( bsb.Texture("t_BaseColor", baseColorTex); bsb.Texture("t_SceneColorIn", sceneColorInTex); bsb.Texture("t_WorldPos", worldPosTex); + bsb.Texture("t_NoisySpecular", noisySpec); + bsb.Texture("t_ClassifyWorldPos", classifyTex); + bsb.Texture("t_SpecReservoirA", specA); + bsb.Texture("t_SpecReservoirB", specB); bsb.TextureUAV("u_SceneColor", outTex); + bsb.TextureUAV("u_NoisyDiffuse", noisyDiff); + bsb.TextureUAV("u_HitDist", hitDist); auto& cache = GetPassResourceCache(); auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->compositeLayout, nvDevice); if (!bindingSet) return; @@ -678,9 +1792,611 @@ ReSTIRGIOutput setupReSTIRGIPass( } ); - state.currTemporalIdx ^= 1; + state.currTemporalIdx = 1 - compositeReservoirIdx; + + VirtualResourceHandle finalColor = compositeData.sceneColor; + + if (!skipInTreeDenoise && state.temporalFilterPipeline && state.temporalFilterLayout && hasPrevFrameData && + motionVectors.is_valid() && prevWorldPos.is_valid() && prevNormals.is_valid()) { + TemporalFilterCB tfCB{}; + tfCB.screenWidth = (float)width; + tfCB.screenHeight = (float)height; + tfCB.invScreenWidth = 1.0f / (float)width; + tfCB.invScreenHeight = 1.0f / (float)height; + tfCB.alpha = std::clamp(::ps_r_rt_gi_temporal_alpha, 0.f, 0.98f); + tfCB.pad0 = 0.f; + tfCB.enabled = 1; + tfCB.pad1 = 0; + + struct TemporalFilterPassData { + VirtualResourceHandle depth, normal, worldPos, prevWorldPos, prevNormal, motion; + fg::RenderDevice* device = nullptr; + ReSTIRGIPassState* state = nullptr; + TemporalFilterCB cbData{}; + u32 width = 0, height = 0; + }; + + fg.addCallbackPass( + "ReSTIR GI Temporal Filter", + [&, tfCB, fgNoisyDiff, fgNoisySpec, fgHistDiff, fgHistSpec, fgBlurTemp, fgBlurTempSpec](FrameGraph& builder, PassHandle passHandle, TemporalFilterPassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.normal = pb.read(normal, ResourceState::ShaderResource); + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); + data.prevWorldPos = pb.read(prevWorldPos, ResourceState::ShaderResource); + data.prevNormal = pb.read(prevNormals, ResourceState::ShaderResource); + data.motion = pb.read(motionVectors, ResourceState::ShaderResource); + pb.read(fgNoisyDiff, ResourceState::ShaderResource); + pb.read(fgNoisySpec, ResourceState::ShaderResource); + pb.read(fgHistDiff, ResourceState::ShaderResource); + pb.read(fgHistSpec, ResourceState::ShaderResource); + pb.write(fgBlurTemp, ResourceState::UnorderedAccess); + pb.write(fgBlurTempSpec, ResourceState::UnorderedAccess); + data.device = device; + data.state = &state; + data.cbData = tfCB; + data.width = width; + data.height = height; + }, + [](const TemporalFilterPassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + if (!data.state || !data.state->temporalFilterPipeline) + return; + auto* depthTex = fgGraph.GetPhysicalTexture(data.depth); + auto* normalTex = fgGraph.GetPhysicalTexture(data.normal); + auto* worldPosTex = fgGraph.GetPhysicalTexture(data.worldPos); + auto* prevWorldPosTex = fgGraph.GetPhysicalTexture(data.prevWorldPos); + auto* prevNormalTex = fgGraph.GetPhysicalTexture(data.prevNormal); + auto* mvTex = fgGraph.GetPhysicalTexture(data.motion); + nvrhi::ITexture* currD = data.state->noisyDiffuse.Get(); + nvrhi::ITexture* currS = data.state->noisySpecular.Get(); + nvrhi::ITexture* histD = data.state->histDiffuse.Get(); + nvrhi::ITexture* histS = data.state->histSpecular.Get(); + nvrhi::ITexture* outD = data.state->blurTemp.Get(); + nvrhi::ITexture* outS = data.state->blurTempSpec.Get(); + if (!depthTex || !normalTex || !worldPosTex || !prevWorldPosTex || !prevNormalTex || !mvTex || + !currD || !currS || !histD || !histS || !outD || !outS) + return; + + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + cmdList->writeBuffer(data.state->cb, &data.cbData, sizeof(TemporalFilterCB)); + + auto* csReflection = GEnv.Render->GetShaderLoader()->GetCachedReflection("restir_gi_temporal_filter", ".cs"); + if (!csReflection) + return; + + framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRGI.TempFilter"); + bsb.ConstantBuffer("TemporalFilterParams", data.state->cb); + bsb.Texture("t_CurrDiffuse", currD); + bsb.Texture("t_CurrSpecular", currS); + bsb.Texture("t_HistDiffuse", histD); + bsb.Texture("t_HistSpecular", histS); + bsb.Texture("t_MotionVectors", mvTex); + bsb.Texture("t_Depth", depthTex); + bsb.Texture("t_Normal", normalTex); + bsb.Texture("t_WorldPos", worldPosTex); + bsb.Texture("t_PrevWorldPos", prevWorldPosTex); + bsb.Texture("t_PrevNormal", prevNormalTex); + bsb.TextureUAV("u_OutDiffuse", outD); + bsb.TextureUAV("u_OutSpecular", outS); + auto& cache = GetPassResourceCache(); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->temporalFilterLayout, nvDevice); + if (!bindingSet) + return; + + nvrhi::ComputeState cs; + cs.pipeline = data.state->temporalFilterPipeline; + cs.bindings = { bindingSet }; + cmdList->setComputeState(cs); + cmdList->dispatch((data.width + 7) / 8, (data.height + 7) / 8, 1); + + cmdList->copyTexture(currD, nvrhi::TextureSlice(), outD, nvrhi::TextureSlice()); + cmdList->copyTexture(currS, nvrhi::TextureSlice(), outS, nvrhi::TextureSlice()); + } + ); + } + + if (!skipInTreeDenoise && state.blurPipeline && state.blurLayout) { + BlurCB blurCB{}; + blurCB.screenWidth = (float)width; + blurCB.screenHeight = (float)height; + blurCB.invScreenWidth = 1.0f / (float)width; + blurCB.invScreenHeight = 1.0f / (float)height; + blurCB.phiNormal = 4.0f; + blurCB.phiDepth = 6.0f; + blurCB.step = 1; + blurCB.mode = 0; + + struct BlurPassData { + VirtualResourceHandle depth, normal, sceneColorIn, sceneColor, classifyWorldPos, worldPos; + fg::RenderDevice* device = nullptr; + ReSTIRGIPassState* state = nullptr; + BlurCB cbData{}; + u32 width = 0, height = 0; + bool srcIsTemp = false; + bool dstIsTemp = false; + bool finalPass = false; + }; + + const int atrousSteps = std::clamp(::ps_r_rt_gi_atrous_steps, 1, 6); + bool srcIsTemp = false; + + for (int i = 0; i < atrousSteps; ++i) { + const u32 step = 1u << (atrousSteps - 1 - i); + const bool finalPass = (i == atrousSteps - 1); + blurCB.step = step; + blurCB.mode = finalPass ? 1u : 0u; + const VirtualResourceHandle srcDiff = srcIsTemp ? fgBlurTemp : fgNoisyDiff; + const VirtualResourceHandle dstDiff = finalPass ? fgNoisyDiff : (srcIsTemp ? fgNoisyDiff : fgBlurTemp); + const VirtualResourceHandle srcSpec = srcIsTemp ? fgBlurTempSpec : fgNoisySpec; + const VirtualResourceHandle dstSpec = finalPass ? fgNoisySpec : (srcIsTemp ? fgNoisySpec : fgBlurTempSpec); + const bool dstIsTemp = !finalPass && !srcIsTemp; + + auto& blurData = fg.addCallbackPass( + finalPass ? "ReSTIR GI Blur Final" : "ReSTIR GI Blur", + [&, blurCB, outHandle, fgDirectLighting, srcDiff, dstDiff, srcSpec, dstSpec, srcIsTemp, dstIsTemp, finalPass, sceneColorIn, fgHistDiff, fgHistSpec, classifyWP](FrameGraph& builder, PassHandle passHandle, BlurPassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.normal = pb.read(normal, ResourceState::ShaderResource); + data.sceneColorIn = pb.read(sceneColorIn, ResourceState::ShaderResource); + data.classifyWorldPos = pb.read(classifyWP, ResourceState::ShaderResource); + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); + pb.read(fgDirectLighting, ResourceState::ShaderResource); + pb.read(srcDiff, ResourceState::ShaderResource); + pb.read(srcSpec, ResourceState::ShaderResource); + data.sceneColor = pb.write(outHandle, ResourceState::UnorderedAccess); + pb.write(dstDiff, ResourceState::UnorderedAccess); + pb.write(dstSpec, ResourceState::UnorderedAccess); + if (finalPass) { + pb.write(fgHistDiff, ResourceState::UnorderedAccess); + pb.write(fgHistSpec, ResourceState::UnorderedAccess); + } + data.device = device; + data.state = &state; + data.cbData = blurCB; + data.width = width; + data.height = height; + data.srcIsTemp = srcIsTemp; + data.dstIsTemp = dstIsTemp; + data.finalPass = finalPass; + }, + [](const BlurPassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + if (!data.state || !data.state->blurPipeline) + return; + auto* depthTex = fgGraph.GetPhysicalTexture(data.depth); + auto* normalTex = fgGraph.GetPhysicalTexture(data.normal); + auto* sceneInTex = fgGraph.GetPhysicalTexture(data.sceneColorIn); + auto* classifyTex = fgGraph.GetPhysicalTexture(data.classifyWorldPos); + auto* worldPosTex = fgGraph.GetPhysicalTexture(data.worldPos); + auto* outTex = fgGraph.GetPhysicalTexture(data.sceneColor); + nvrhi::ITexture* directLit = data.state->directLighting.Get(); + nvrhi::ITexture* noisyDiff = data.state->noisyDiffuse.Get(); + nvrhi::ITexture* blurTemp = data.state->blurTemp.Get(); + nvrhi::ITexture* noisySpec = data.state->noisySpecular.Get(); + nvrhi::ITexture* blurTempSpec = data.state->blurTempSpec.Get(); + if (!depthTex || !normalTex || !sceneInTex || !classifyTex || !worldPosTex || !outTex || !directLit || + !noisyDiff || !blurTemp || !noisySpec || !blurTempSpec) + return; + + nvrhi::ITexture* srcD = data.srcIsTemp ? blurTemp : noisyDiff; + nvrhi::ITexture* dstD = data.finalPass ? noisyDiff : (data.dstIsTemp ? blurTemp : noisyDiff); + nvrhi::ITexture* srcS = data.srcIsTemp ? blurTempSpec : noisySpec; + nvrhi::ITexture* dstS = data.finalPass ? noisySpec : (data.dstIsTemp ? blurTempSpec : noisySpec); + + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + cmdList->writeBuffer(data.state->cb, &data.cbData, sizeof(BlurCB)); + + auto* csReflection = GEnv.Render->GetShaderLoader()->GetCachedReflection("restir_gi_blur", ".cs"); + if (!csReflection) + return; + + framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRGI.Blur"); + bsb.ConstantBuffer("BlurParams", data.state->cb); + bsb.Texture("t_DirectLighting", directLit); + bsb.Texture("t_NoisyDiffuse", srcD); + bsb.Texture("t_Depth", depthTex); + bsb.Texture("t_Normal", normalTex); + bsb.Texture("t_SceneColorIn", sceneInTex); + bsb.Texture("t_NoisySpecular", srcS); + bsb.Texture("t_ClassifyWorldPos", classifyTex); + bsb.Texture("t_WorldPos", worldPosTex); + bsb.TextureUAV("u_SceneColor", outTex); + bsb.TextureUAV("u_FilteredDiffuse", dstD); + bsb.TextureUAV("u_FilteredSpecular", dstS); + auto& cache = GetPassResourceCache(); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->blurLayout, nvDevice); + if (!bindingSet) + return; + + nvrhi::ComputeState cs; + cs.pipeline = data.state->blurPipeline; + cs.bindings = { bindingSet }; + cmdList->setComputeState(cs); + cmdList->dispatch((data.width + 7) / 8, (data.height + 7) / 8, 1); + + if (data.finalPass && data.state->histDiffuse && data.state->histSpecular) { + cmdList->copyTexture(data.state->histDiffuse, nvrhi::TextureSlice(), dstD, nvrhi::TextureSlice()); + cmdList->copyTexture(data.state->histSpecular, nvrhi::TextureSlice(), dstS, nvrhi::TextureSlice()); + } + } + ); + finalColor = blurData.sceneColor; + if (!finalPass) + srcIsTemp = !srcIsTemp; + } + } - return { compositeData.sceneColor }; + if (state.waterPipeline && state.waterLayout && accelMgr) { + WaterCB waterCB{}; + waterCB.invViewProj = invViewProj; + waterCB.cameraPos = { cameraPos.x, cameraPos.y, cameraPos.z, 0 }; + waterCB.sunDir_intensity = initialCB.sunDir_intensity; + waterCB.sunColor_skyWeight = initialCB.sunColor_skyWeight; + waterCB.screenWidth = (float)width; + waterCB.screenHeight = (float)height; + waterCB.giIntensity = giIntensity; + waterCB.identityStaticCount = initialCB.identityStaticCount; + waterCB.terrainBatchCount = initialCB.terrainBatchCount; + waterCB.skinnedBatchStart = initialCB.skinnedBatchStart; + waterCB.grassBatchStart = initialCB.grassBatchStart; + waterCB.detailAtlasIndex = initialCB.detailAtlasIndex; + waterCB.hemiColor = initialCB.hemiColor; + + ResourceDesc waterOutDesc = outDesc; + waterOutDesc.debugName = "rtgi_WaterSceneColor"; + VirtualResourceHandle waterOut = fg.CreateTexture("rtgi_WaterSceneColor", waterOutDesc); + + struct WaterPassData { + VirtualResourceHandle depth, normal, baseColor, worldPos, classifyWorldPos, sceneIn, sceneOut; + fg::RenderDevice* device = nullptr; + RTAccelStructManager* accelMgr = nullptr; + ReSTIRGIPassState* state = nullptr; + WaterCB cbData{}; + nvrhi::ITexture* sky0 = nullptr; + nvrhi::ITexture* sky1 = nullptr; + nvrhi::ITexture* underWorldPos = nullptr; + u32 width = 0, height = 0; + }; + + auto& waterData = fg.addCallbackPass( + "ReSTIR Water RT", + [&, waterCB, finalColor, waterOut, sky0Tex, sky1Tex, classifyWP](FrameGraph& builder, PassHandle passHandle, WaterPassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.normal = pb.read(normal, ResourceState::ShaderResource); + data.baseColor = pb.read(baseColor, ResourceState::ShaderResource); + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); + data.classifyWorldPos = pb.read(classifyWP, ResourceState::ShaderResource); + data.sceneIn = pb.read(finalColor, ResourceState::ShaderResource); + data.sceneOut = pb.write(waterOut, ResourceState::UnorderedAccess); + pb.sideEffects(); + data.device = device; + data.accelMgr = accelMgr; + data.state = &state; + data.cbData = waterCB; + data.sky0 = sky0Tex; + data.sky1 = sky1Tex; + data.underWorldPos = state.waterUnderWorldPos; + data.width = width; + data.height = height; + }, + [](const WaterPassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + if (!data.state || !data.state->waterPipeline || !data.accelMgr) + return; + auto* depthTex = fgGraph.GetPhysicalTexture(data.depth); + auto* normalTex = fgGraph.GetPhysicalTexture(data.normal); + auto* baseColorTex = fgGraph.GetPhysicalTexture(data.baseColor); + auto* worldPosTex = fgGraph.GetPhysicalTexture(data.worldPos); + auto* classifyTex = fgGraph.GetPhysicalTexture(data.classifyWorldPos); + auto* sceneIn = fgGraph.GetPhysicalTexture(data.sceneIn); + auto* sceneOut = fgGraph.GetPhysicalTexture(data.sceneOut); + auto* tlas = data.accelMgr->GetTLAS(); + auto* batchInfo = data.accelMgr->GetBatchInfoBuffer(); + auto* megaVB = data.accelMgr->GetMegaVB(); + auto* megaIB = data.accelMgr->GetMegaIB(); + if (!depthTex || !normalTex || !baseColorTex || !worldPosTex || !classifyTex || !sceneIn || !sceneOut || + !tlas || !batchInfo || !megaVB || !megaIB) + return; + + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + cmdList->writeBuffer(data.state->cb, &data.cbData, sizeof(WaterCB)); + + auto* csReflection = GEnv.Render->GetShaderLoader()->GetCachedReflection("restir_water_rt", ".cs"); + if (!csReflection) + return; + + nvrhi::IBuffer* skinnedVB = data.accelMgr->GetSkinnedOutputVB(); + nvrhi::IBuffer* skinnedIB = data.accelMgr->GetSkinnedIB(); + nvrhi::IBuffer* grassVB = data.accelMgr->GetGrassOutputVB(); + nvrhi::IBuffer* grassIB = data.accelMgr->GetGrassIB(); + if (!skinnedVB) skinnedVB = s_rtgiPlaceholderBuffer.Get(); + if (!skinnedIB) skinnedIB = s_rtgiPlaceholderBuffer.Get(); + if (!grassVB) grassVB = s_rtgiPlaceholderBuffer.Get(); + if (!grassIB) grassIB = s_rtgiPlaceholderBuffer.Get(); + + auto& cache = GetPassResourceCache(); + nvrhi::ITexture* sky0 = data.sky0 ? data.sky0 : s_rtgiPlaceholderCube.Get(); + nvrhi::ITexture* sky1 = data.sky1 ? data.sky1 : s_rtgiPlaceholderCube.Get(); + nvrhi::ITexture* underWP = data.underWorldPos ? data.underWorldPos : cache.GetDummyContactHistory(nvDevice); + + framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRGI.Water"); + bsb.ConstantBuffer("ReSTIRWaterParams", data.state->cb); + bsb.AccelStruct("g_SceneTLAS", tlas); + bsb.BufferSRV("g_BatchInfo", batchInfo); + bsb.BufferSRV("g_MegaVB", megaVB); + bsb.BufferSRV("g_MegaIB", megaIB); + bsb.Texture("g_Sky0", sky0); + bsb.Texture("g_Sky1", sky1); + bsb.BufferSRV("g_SkinnedVB", skinnedVB); + BindBindlessMaterialTables(bsb); + bsb.BufferSRV("g_SkinnedIB", skinnedIB); + bsb.BufferSRV("g_GrassVB", grassVB); + bsb.BufferSRV("g_GrassIB", grassIB); + bsb.Texture("t_Depth", depthTex); + bsb.Texture("t_Normal", normalTex); + bsb.Texture("t_BaseColor", baseColorTex); + bsb.Texture("t_WorldPos", worldPosTex); + bsb.Texture("t_ClassifyWorldPos", classifyTex); + bsb.Texture("t_SceneColorIn", sceneIn); + bsb.Texture("t_UnderWorldPos", underWP); + bsb.TextureUAV("u_SceneColor", sceneOut); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->waterLayout, nvDevice); + if (!bindingSet) + return; + + nvrhi::ComputeState cs; + cs.pipeline = data.state->waterPipeline; + cs.bindings = { bindingSet }; + if (auto* backend = data.device ? data.device->GetBackend() : nullptr) { + if (auto* bindlessTable = backend->GetBindlessDescriptorTable()) + cs.addBindingSet(bindlessTable); + } + cmdList->setComputeState(cs); + cmdList->dispatch((data.width + 7) / 8, (data.height + 7) / 8, 1); + } + ); + finalColor = waterData.sceneOut; + } + + return { finalColor, fgNoisyDiff, fgNoisySpec, fgHitDist }; +} + +VirtualResourceHandle setupRTVolumetricPass( + FrameGraph& fg, + fg::RenderDevice* device, + RTAccelStructManager* accelMgr, + VirtualResourceHandle sceneColor, + VirtualResourceHandle depth, + VirtualResourceHandle worldPos, + const Fmatrix& invViewProj, + const Fvector& cameraPos, + u32 width, + u32 height, + ReSTIRGIPassState& state) +{ + if (!device || !accelMgr || !sceneColor.is_valid()) + return sceneColor; + InitializeResources(device, state); + EnsureVolumetricPipeline(device, state); + if (!state.volPipeline || !state.volLayout || !state.cb || !accelMgr->GetTLAS()) + return sceneColor; + if (!g_pGamePersistent || !g_pGameLevel) + return sceneColor; + if (Device.dwPrecacheFrame || g_pGamePersistent->IsLoadingScreenShown()) + return sceneColor; + + float shaftI = ResolveSunShaftsIntensity(); + if (::ps_r_rt_gi) + shaftI *= 1.15f; + if (shaftI < 1e-4f) + return sceneColor; + + const auto& env = g_pGamePersistent->Environment().CurrentEnv; + const float fogDensity01 = std::clamp(env.fog_density, 0.f, 1.f); + const float weatherShaftI = env.m_fSunShaftsIntensity; + + Fvector3 sc = { env.sun_color.x, env.sun_color.y, env.sun_color.z }; + float sunIntensity = std::max({ sc.x, sc.y, sc.z }); + if (sunIntensity < 1e-4f) + return sceneColor; + Fvector sunDir = env.sun_dir; + + u32 steps = (::ps_r_rt_vol_steps > 0) ? (u32)::ps_r_rt_vol_steps : 0u; + if (steps == 0) + { + if (ps_r_sun_shafts >= 3) + steps = 40; + else if (ps_r_sun_shafts == 2) + steps = 28; + else + steps = 16; + } + + const float fogFar = std::max(env.fog_distance, 1400.f); + + Fvector camDir = Device.vCameraDirection; + float sunSat = -camDir.dotproduct(sunDir); + sunSat = 0.5f * sunSat + 0.5f; + sunSat = 0.80f * sunSat + 0.20f; + + { + static float s_lastLogged = -1.f; + if (fabsf(shaftI - s_lastLogged) > 0.01f) + { + s_lastLogged = shaftI; + Msg("* [RT SunShafts] weather=%.3f final=%.3f steps=%u sunI=%.3f sat=%.2f", + weatherShaftI, shaftI, steps, sunIntensity, sunSat); + } + } + + RTVolCB volCB{}; + volCB.invViewProj = invViewProj; + volCB.cameraPos = { cameraPos.x, cameraPos.y, cameraPos.z, 0 }; + volCB.sunDir_intensity = { sunDir.x, sunDir.y, sunDir.z, sunIntensity }; + volCB.sunColor_fog = { sc.x, sc.y, sc.z, sunSat }; + volCB.screenWidth = (float)width; + volCB.screenHeight = (float)height; + volCB.fogDensity = 0.f; + volCB.fogHeight = Device.vCameraPosition.y - 35.f; + volCB.steps = steps; + volCB.fogFar = fogFar; + volCB.heightFalloff = 0.0007f + fogDensity01 * 0.0025f; + volCB.shaftIntensity = shaftI; + { + const auto& bc = accelMgr->GetBatchCounts(); + volCB.grassBatchStart = bc.grass > 0 + ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + bc.skinned + : 0xFFFFFFFFu; + volCB.detailAtlasIndex = accelMgr->GetDetailAtlasIndex(); + volCB.pad0 = volCB.pad1 = 0; + } + + if (state.volAllocFailed) + return sceneColor; + + nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); + if (!state.volSceneColor || state.volTexWidth != width || state.volTexHeight != height) { + nvrhi::TextureDesc desc; + desc.debugName = "rtgi_VolSceneColor"; + desc.width = width; + desc.height = height; + desc.format = nvrhi::Format::RGBA16_FLOAT; + desc.isUAV = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + state.volSceneColor = nvDevice->createTexture(desc); + state.volTexWidth = width; + state.volTexHeight = height; + if (!state.volSceneColor) { + state.volAllocFailed = true; + Msg("! [RT SunShafts] VolSceneColor alloc failed (%ux%u) — RT shafts disabled", width, height); + return sceneColor; + } + } + + ResourceDesc volOutDesc; + volOutDesc.type = ResourceDesc::Type::Texture2D; + volOutDesc.width = width; + volOutDesc.height = height; + volOutDesc.format = nvrhi::Format::RGBA16_FLOAT; + volOutDesc.isUAV = true; + volOutDesc.allowUAV = true; + volOutDesc.isRenderTarget = false; + volOutDesc.isImported = true; + volOutDesc.debugName = "rtgi_VolSceneColor"; + VirtualResourceHandle volOut = fg.ImportTexture("rtgi_VolSceneColor", state.volSceneColor.Get(), volOutDesc); + + struct VolPassData { + VirtualResourceHandle depth, worldPos, sceneIn, sceneOut; + fg::RenderDevice* device = nullptr; + RTAccelStructManager* accelMgr = nullptr; + ReSTIRGIPassState* state = nullptr; + RTVolCB cbData{}; + u32 width = 0, height = 0; + }; + + auto& volData = fg.addCallbackPass( + "RT SunShafts", + [&, volCB, sceneColor, volOut, depth, worldPos](FrameGraph& builder, PassHandle passHandle, VolPassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); + data.sceneIn = pb.read(sceneColor, ResourceState::ShaderResource); + data.sceneOut = pb.write(volOut, ResourceState::UnorderedAccess); + pb.sideEffects(); + data.device = device; + data.accelMgr = accelMgr; + data.state = &state; + data.cbData = volCB; + data.width = width; + data.height = height; + }, + [](const VolPassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + auto* sceneIn = fgGraph.GetPhysicalTexture(data.sceneIn); + auto* sceneOut = fgGraph.GetPhysicalTexture(data.sceneOut); + if (!sceneIn || !sceneOut || !data.device) + return; + + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + + auto blitScene = [&]() { + nvrhi::TextureSlice slice; + cmdList->copyTexture(sceneOut, slice, sceneIn, slice); + }; + + if (!data.state || !data.state->volPipeline || !data.accelMgr) + { + blitScene(); + return; + } + auto* depthTex = fgGraph.GetPhysicalTexture(data.depth); + auto* worldPosTex = fgGraph.GetPhysicalTexture(data.worldPos); + auto* tlas = data.accelMgr->GetTLAS(); + if (!depthTex || !worldPosTex || !tlas) + { + blitScene(); + return; + } + + RTVolCB cb = data.cbData; + { + const auto& bc = data.accelMgr->GetBatchCounts(); + cb.grassBatchStart = bc.grass > 0 + ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + bc.skinned + : 0xFFFFFFFFu; + cb.detailAtlasIndex = data.accelMgr->GetDetailAtlasIndex(); + } + cmdList->writeBuffer(data.state->cb, &cb, sizeof(RTVolCB)); + + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* csReflection = shaderLoader->GetCachedReflection("rt_volumetric", ".cs"); + if (!csReflection) + { + blitScene(); + return; + } + + nvrhi::IBuffer* batchInfo = data.accelMgr->GetBatchInfoBuffer(); + nvrhi::IBuffer* grassVB = data.accelMgr->GetGrassOutputVB(); + nvrhi::IBuffer* grassIB = data.accelMgr->GetGrassIB(); + if (!batchInfo) batchInfo = s_rtgiPlaceholderBuffer.Get(); + if (!grassVB) grassVB = s_rtgiPlaceholderBuffer.Get(); + if (!grassIB) grassIB = s_rtgiPlaceholderBuffer.Get(); + + framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRGI.Vol"); + bsb.ConstantBuffer("RTVolParams", data.state->cb); + bsb.AccelStruct("g_SceneTLAS", tlas); + bsb.Texture("t_Depth", depthTex); + bsb.Texture("t_SceneColorIn", sceneIn); + bsb.Texture("t_WorldPos", worldPosTex); + bsb.BufferSRV("g_BatchInfo", batchInfo); + bsb.BufferSRV("g_GrassVB", grassVB); + bsb.BufferSRV("g_GrassIB", grassIB); + BindBindlessMaterialTables(bsb); + bsb.TextureUAV("u_SceneColor", sceneOut); + auto& cache = GetPassResourceCache(); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->volLayout, nvDevice); + if (!bindingSet) + { + blitScene(); + return; + } + + nvrhi::ComputeState cs; + cs.pipeline = data.state->volPipeline; + cs.bindings = { bindingSet }; + if (auto* backend = data.device->GetBackend()) { + if (auto* bindlessTable = backend->GetBindlessDescriptorTable()) + cs.addBindingSet(bindlessTable); + } + cmdList->setComputeState(cs); + cmdList->dispatch((data.width + 7) / 8, (data.height + 7) / 8, 1); + } + ); + return volData.sceneOut; } void ShutdownReSTIRGI(ReSTIRGIPassState& state) @@ -689,15 +2405,48 @@ void ShutdownReSTIRGI(ReSTIRGIPassState& state) state.initialLayout = nullptr; state.temporalPipeline = nullptr; state.temporalLayout = nullptr; + state.spatialPipeline = nullptr; + state.spatialLayout = nullptr; + state.diTemporalPipeline = nullptr; + state.diTemporalLayout = nullptr; + state.diSpatialPipeline = nullptr; + state.diSpatialLayout = nullptr; + state.diShadePipeline = nullptr; + state.diShadeLayout = nullptr; + state.waterPipeline = nullptr; + state.waterLayout = nullptr; state.compositePipeline = nullptr; state.compositeLayout = nullptr; + state.blurPipeline = nullptr; + state.blurLayout = nullptr; + state.temporalFilterPipeline = nullptr; + state.temporalFilterLayout = nullptr; + state.volPipeline = nullptr; + state.volLayout = nullptr; state.cb = nullptr; state.sampler = nullptr; for (int i = 0; i < 2; i++) { state.reservoirA[i] = nullptr; state.reservoirB[i] = nullptr; + state.reservoirC[i] = nullptr; + state.specReservoirA[i] = nullptr; + state.specReservoirB[i] = nullptr; + state.diReservoir[i] = nullptr; } state.directLighting = nullptr; + state.noisyDiffuse = nullptr; + state.noisySpecular = nullptr; + state.blurTemp = nullptr; + state.blurTempSpec = nullptr; + state.histDiffuse = nullptr; + state.histSpecular = nullptr; + state.hitDistance = nullptr; + state.volSceneColor = nullptr; + state.irradianceCache = nullptr; + state.irradianceCacheSize = 0; + state.volTexWidth = 0; + state.volTexHeight = 0; + state.volAllocFailed = false; s_rtgiPlaceholderBuffer = nullptr; s_rtgiPlaceholderCube = nullptr; state.initialized = false; diff --git a/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.h index 6545dd0a345..e5eac3a9842 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.h @@ -15,27 +15,67 @@ struct ReSTIRGIPassState { nvrhi::BindingLayoutHandle initialLayout; nvrhi::ComputePipelineHandle temporalPipeline; nvrhi::BindingLayoutHandle temporalLayout; + nvrhi::ComputePipelineHandle spatialPipeline; + nvrhi::BindingLayoutHandle spatialLayout; + nvrhi::ComputePipelineHandle diTemporalPipeline; + nvrhi::BindingLayoutHandle diTemporalLayout; + nvrhi::ComputePipelineHandle diSpatialPipeline; + nvrhi::BindingLayoutHandle diSpatialLayout; + nvrhi::ComputePipelineHandle diShadePipeline; + nvrhi::BindingLayoutHandle diShadeLayout; + nvrhi::ComputePipelineHandle waterPipeline; + nvrhi::BindingLayoutHandle waterLayout; nvrhi::ComputePipelineHandle compositePipeline; nvrhi::BindingLayoutHandle compositeLayout; + nvrhi::ComputePipelineHandle blurPipeline; + nvrhi::BindingLayoutHandle blurLayout; + nvrhi::ComputePipelineHandle temporalFilterPipeline; + nvrhi::BindingLayoutHandle temporalFilterLayout; + nvrhi::ComputePipelineHandle volPipeline; + nvrhi::BindingLayoutHandle volLayout; nvrhi::IBuffer* cb = nullptr; nvrhi::SamplerHandle sampler; nvrhi::TextureHandle reservoirA[2]; nvrhi::TextureHandle reservoirB[2]; + nvrhi::TextureHandle reservoirC[2]; + nvrhi::TextureHandle specReservoirA[2]; + nvrhi::TextureHandle specReservoirB[2]; + nvrhi::TextureHandle diReservoir[2]; nvrhi::TextureHandle directLighting; + nvrhi::TextureHandle noisyDiffuse; + nvrhi::TextureHandle noisySpecular; + nvrhi::TextureHandle blurTemp; + nvrhi::TextureHandle blurTempSpec; + nvrhi::TextureHandle histDiffuse; + nvrhi::TextureHandle histSpecular; + nvrhi::TextureHandle hitDistance; + nvrhi::TextureHandle volSceneColor; + nvrhi::BufferHandle irradianceCache; + u32 irradianceCacheSize = 0; u32 currTemporalIdx = 0; u32 texWidth = 0; u32 texHeight = 0; + u32 volTexWidth = 0; + u32 volTexHeight = 0; + bool volAllocFailed = false; + nvrhi::ITexture* waterUnderWorldPos = nullptr; bool initialized = false; bool enabled = false; + bool initialLoadFailed = false; }; struct ReSTIRGIOutput { framegraph::VirtualResourceHandle sceneColor; + framegraph::VirtualResourceHandle noisyDiffuse; + framegraph::VirtualResourceHandle noisySpecular; + framegraph::VirtualResourceHandle hitDistance; }; +bool IsRTGIActive(const RTAccelStructManager* accelMgr); + ReSTIRGIOutput setupReSTIRGIPass( framegraph::FrameGraph& fg, fg::RenderDevice* device, @@ -54,9 +94,26 @@ ReSTIRGIOutput setupReSTIRGIPass( float giIntensity, u32 width, u32 height, ReSTIRGIPassState& state, - bool hasPrevFrameData + bool hasPrevFrameData, + bool skipInTreeDenoise = false, + framegraph::VirtualResourceHandle classifyWorldPos = {}, + bool skipLightingTemporal = false ); void ShutdownReSTIRGI(ReSTIRGIPassState& state); +framegraph::VirtualResourceHandle setupRTVolumetricPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + RTAccelStructManager* accelMgr, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle worldPos, + const Fmatrix& invViewProj, + const Fvector& cameraPos, + u32 width, + u32 height, + ReSTIRGIPassState& state +); + } diff --git a/src/Layers/xrRender/FrameGraphPasses/RibbonPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/RibbonPassSetup.cpp index 6ae30eaecc4..8ca27ac596e 100644 --- a/src/Layers/xrRender/FrameGraphPasses/RibbonPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/RibbonPassSetup.cpp @@ -339,9 +339,9 @@ RibbonPassOutput setupRibbonPass( auto* psReflection = shaderLoader->GetCachedReflection("ribbon", ".ps"); BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Ribbon"); bsb.ConstantBuffer("static_globals", staticGlobalsCB) - .ConstantBuffer("RibbonParams", ribbonParamsCB) - .BufferSRV("g_Materials", matBuffer.GetBuffer()) - .BufferSRV("g_ControlPoints", st.controlPointBuffer); + .ConstantBuffer("RibbonParams", ribbonParamsCB); + BindBindlessMaterialTables(bsb); + bsb.BufferSRV("g_ControlPoints", st.controlPointBuffer); auto bindDesc = bsb.Build(); auto bindingSet = cache.GetOrCreateBindingSet(bindDesc, st.layout, nvDevice); diff --git a/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.cpp index 0b898c58805..005e84df93e 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.cpp @@ -18,42 +18,80 @@ using namespace framegraph; namespace { -void EnsureHistory(nvrhi::IDevice* nv, SSGIPassState& st, u32 w, u32 h) +constexpr u32 kSSGIPipeVersion = 9; + +nvrhi::TextureHandle CreateRT(nvrhi::IDevice* nv, u32 w, u32 h, const char* name) { - if (st.history[0] && st.historyW == w && st.historyH == h) - return; - for (int i = 0; i < 2; ++i) + nvrhi::TextureDesc td; + td.width = w; + td.height = h; + td.format = nvrhi::Format::RGBA16_FLOAT; + td.isRenderTarget = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = name; + return nv->createTexture(td); +} + +void EnsureTargets(nvrhi::IDevice* nv, SSGIPassState& st, u32 w, u32 h, u32 tw, u32 th) +{ + if (!st.outputTex || st.outputW != w || st.outputH != h) { - nvrhi::TextureDesc td; - td.width = w; - td.height = h; - td.format = nvrhi::Format::RGBA16_FLOAT; - td.isRenderTarget = true; - td.isShaderResource = true; - td.initialState = nvrhi::ResourceStates::ShaderResource; - td.keepInitialState = true; - td.debugName = (i == 0) ? "rt_SSGI_Hist0" : "rt_SSGI_Hist1"; - st.history[i] = nv->createTexture(td); + st.outputTex = CreateRT(nv, w, h, "rt_SSGI_Out"); + st.colorCopy = CreateRT(nv, w, h, "SSGI_ColorCopy"); + st.outputW = w; + st.outputH = h; + } + if (!st.giTraceTex || st.historyW != tw || st.historyH != th) + { + st.giTraceTex = CreateRT(nv, tw, th, "rt_SSGI"); + st.giBlurTex = CreateRT(nv, tw, th, "rt_SSGI_Blur"); + st.giTemporalTex = CreateRT(nv, tw, th, "rt_SSGI_Temporal"); + for (int i = 0; i < 2; ++i) + st.history[i] = CreateRT(nv, tw, th, (i == 0) ? "rt_SSGI_Hist0" : "rt_SSGI_Hist1"); + st.historyW = tw; + st.historyH = th; + st.hasHistory = false; + st.historyIndex = 0; } - st.historyW = w; - st.historyH = h; - st.hasHistory = false; - st.historyIndex = 0; } void InitSSGI(nvrhi::IDevice* nv, SSGIPassState& st) { - if (st.initialized || !nv) + if (!nv) + return; + if (st.initialized && st.pipeVersion == kSSGIPipeVersion && + st.tracePipeline && st.blurPipeline && st.applyPipeline) return; + + st.initialized = false; + st.pipeVersion = 0; + st.tracePipeline = nullptr; + st.blurPipeline = nullptr; + st.temporalPipeline = nullptr; + st.applyPipeline = nullptr; + st.traceLayout = nullptr; + st.blurLayout = nullptr; + st.temporalLayout = nullptr; + st.applyLayout = nullptr; + auto* loader = GEnv.Render->GetShaderLoader(); - if (!loader) { st.initialized = true; return; } + if (!loader) + { + st.initialized = true; + return; + } + BindingSetBuilder::InvalidateReflectionCache(); auto vs = loader->LoadVertexShader("fullscreen"); auto psTrace = loader->LoadPixelShader("ssgi_trace"); auto psBlur = loader->LoadPixelShader("ssgi_blur"); auto psTemporal = loader->LoadPixelShader("ssgi_temporal"); auto psApply = loader->LoadPixelShader("ssgi_apply"); - if (!vs.handle || !psTrace.handle || !psBlur.handle || !psTemporal.handle || !psApply.handle) + if (!vs.handle || !psTrace.handle || !psBlur.handle || !psTemporal.handle || !psApply.handle || + !vs.reflection || !psTrace.reflection || !psBlur.reflection || !psTemporal.reflection || + !psApply.reflection) { st.initialized = true; return; @@ -64,7 +102,7 @@ void InitSSGI(nvrhi::IDevice* nv, SSGIPassState& st) fb.addColorFormat(nvrhi::Format::RGBA16_FLOAT); auto makePipe = [&](const char* name, auto& ps, nvrhi::BindingLayoutHandle& layout, - nvrhi::GraphicsPipelineHandle& pipe) { + nvrhi::GraphicsPipelineHandle& pipe, bool additive) { layout = cache.GetOrCreateBindingLayoutFromReflection( name, *vs.reflection, *ps.reflection, nv); if (!layout) @@ -73,25 +111,39 @@ void InitSSGI(nvrhi::IDevice* nv, SSGIPassState& st) desc.setVertexShader(vs.handle); desc.setPixelShader(ps.handle); desc.addBindingLayout(layout); - auto* backend = GEnv.Backend; - if (backend && backend->GetBindlessLayout()) - desc.addBindingLayout(backend->GetBindlessLayout()); desc.setPrimType(nvrhi::PrimitiveType::TriangleList); - desc.renderState.blendState.targets[0].setBlendEnable(false); + auto& bt = desc.renderState.blendState.targets[0]; + if (additive) + { + bt.setBlendEnable(true); + bt.setSrcBlend(nvrhi::BlendFactor::One); + bt.setDestBlend(nvrhi::BlendFactor::One); + bt.setBlendOp(nvrhi::BlendOp::Add); + bt.setSrcBlendAlpha(nvrhi::BlendFactor::One); + bt.setDestBlendAlpha(nvrhi::BlendFactor::One); + bt.setBlendOpAlpha(nvrhi::BlendOp::Add); + } + else + bt.setBlendEnable(false); desc.renderState.depthStencilState.setDepthTestEnable(false); desc.renderState.depthStencilState.setDepthWriteEnable(false); desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); pipe = cache.GetOrCreatePipeline(name, desc, fb, nv); }; - makePipe("SSGI_Trace_v2", psTrace, st.traceLayout, st.tracePipeline); - makePipe("SSGI_Blur_v2", psBlur, st.blurLayout, st.blurPipeline); - makePipe("SSGI_Temporal_v2", psTemporal, st.temporalLayout, st.temporalPipeline); - makePipe("SSGI_Apply_v2", psApply, st.applyLayout, st.applyPipeline); + makePipe("SSGI_Trace_v9", psTrace, st.traceLayout, st.tracePipeline, false); + makePipe("SSGI_Blur_v9", psBlur, st.blurLayout, st.blurPipeline, false); + makePipe("SSGI_Temporal_v9", psTemporal, st.temporalLayout, st.temporalPipeline, false); + makePipe("SSGI_Apply_v9", psApply, st.applyLayout, st.applyPipeline, false); st.initialized = true; + st.pipeVersion = kSSGIPipeVersion; + if (st.tracePipeline && st.blurPipeline && st.applyPipeline) + Msg("* [SSGI] initialized v9 (stable frame RNG, composite apply)"); + else + Msg("! [SSGI] Pipeline create failed"); } -void DrawFullscreen( +void DrawSSGIFullscreen( nvrhi::ICommandList* cmd, nvrhi::IDevice* nv, nvrhi::GraphicsPipelineHandle pipe, @@ -110,8 +162,6 @@ void DrawFullscreen( gs.pipeline = pipe; gs.framebuffer = fb; gs.bindings = { set }; - if (GEnv.Backend && GEnv.Backend->GetBindlessDescriptorTable()) - gs.addBindingSet(GEnv.Backend->GetBindlessDescriptorTable()); gs.viewport.addViewportAndScissorRect(nvrhi::Viewport(float(w), float(h))); cmd->setGraphicsState(gs); cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); @@ -142,48 +192,41 @@ VirtualResourceHandle setupSSGIPass( const u32 tw = halfRes ? std::max(1u, width / 2) : width; const u32 th = halfRes ? std::max(1u, height / 2) : height; - EnsureHistory(device->GetNVRHIDevice(), state, tw, th); + EnsureTargets(device->GetNVRHIDevice(), state, width, height, tw, th); + if (!state.outputTex || !state.colorCopy || !state.giTraceTex || !state.giBlurTex) + return sceneColor; + const u32 readIdx = state.historyIndex; const u32 writeIdx = 1u - readIdx; const bool useTemporal = state.temporalPipeline && motionVectors.is_valid() && hasPrevFrame && state.hasHistory && state.history[0]; - ResourceDesc giDesc; - giDesc.type = ResourceDesc::Type::Texture2D; - giDesc.width = tw; - giDesc.height = th; - giDesc.format = nvrhi::Format::RGBA16_FLOAT; - giDesc.isRenderTarget = true; - giDesc.isTransient = true; - giDesc.debugName = "rt_SSGI"; - auto giTrace = fg.CreateTexture("rt_SSGI", giDesc); - - ResourceDesc blurDesc = giDesc; - blurDesc.debugName = "rt_SSGI_Blur"; - auto giBlur = fg.CreateTexture("rt_SSGI_Blur", blurDesc); - - ResourceDesc tempDesc = giDesc; - tempDesc.debugName = "rt_SSGI_Temporal"; - auto giTemporal = fg.CreateTexture("rt_SSGI_Temporal", tempDesc); - ResourceDesc outDesc; outDesc.type = ResourceDesc::Type::Texture2D; outDesc.width = width; outDesc.height = height; outDesc.format = nvrhi::Format::RGBA16_FLOAT; outDesc.isRenderTarget = true; - outDesc.isTransient = true; + outDesc.isTransient = false; + outDesc.isImported = true; outDesc.debugName = "rt_SSGI_Out"; - auto output = fg.CreateTexture("rt_SSGI_Out", outDesc); + auto output = fg.ImportTexture("rt_SSGI_Out", state.outputTex.Get(), outDesc); + + ResourceDesc giDesc = outDesc; + giDesc.width = tw; + giDesc.height = th; + giDesc.debugName = "rt_SSGI"; + auto giTrace = fg.ImportTexture("rt_SSGI", state.giTraceTex.Get(), giDesc); + giDesc.debugName = "rt_SSGI_Blur"; + auto giBlur = fg.ImportTexture("rt_SSGI_Blur", state.giBlurTex.Get(), giDesc); + giDesc.debugName = "rt_SSGI_Temporal"; + auto giTemporal = fg.ImportTexture("rt_SSGI_Temporal", state.giTemporalTex.Get(), giDesc); VirtualResourceHandle histRead{}; if (useTemporal) { - ResourceDesc histDesc = giDesc; - histDesc.isTransient = false; - histDesc.isImported = true; - histDesc.debugName = "rt_SSGI_HistRead"; - histRead = fg.ImportTexture("rt_SSGI_HistRead", state.history[readIdx].Get(), histDesc); + giDesc.debugName = "rt_SSGI_HistRead"; + histRead = fg.ImportTexture("rt_SSGI_HistRead", state.history[readIdx].Get(), giDesc); } struct PassData @@ -229,7 +272,7 @@ VirtualResourceHandle setupSSGIPass( }, [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { auto* cmd = ctx->GetCommandList(); - auto* nv = cmd->getDevice(); + auto* nv = cmd ? cmd->getDevice() : nullptr; auto* color = graph.GetPhysicalTexture(data.color); auto* depthTex = graph.GetPhysicalTexture(data.depth); auto* normalTex = graph.GetPhysicalTexture(data.normal); @@ -238,27 +281,24 @@ VirtualResourceHandle setupSSGIPass( auto* giB = graph.GetPhysicalTexture(data.giBlur); auto* giTemp = graph.GetPhysicalTexture(data.giTemporal); auto* out = graph.GetPhysicalTexture(data.output); - if (!cmd || !nv || !color || !depthTex || !normalTex || !baseTex || - !giT || !giB || !giTemp || !out || !data.st) + if (!cmd || !nv || !color || !out || !data.st || !data.st->colorCopy) return; - const auto& cdesc = color->getDesc(); - if (!data.st->colorCopy || - data.st->colorCopy->getDesc().width != cdesc.width || - data.st->colorCopy->getDesc().height != cdesc.height) + nvrhi::ITexture* sceneSrv = color; + if (color == out) { - nvrhi::TextureDesc td = cdesc; - td.debugName = "SSGI_ColorCopy"; - td.isRenderTarget = true; - td.initialState = nvrhi::ResourceStates::ShaderResource; - td.keepInitialState = true; - data.st->colorCopy = nv->createTexture(td); - } - if (data.st->colorCopy) cmd->copyTexture(data.st->colorCopy, nvrhi::TextureSlice(), color, nvrhi::TextureSlice()); + cmd->setTextureState(data.st->colorCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + sceneSrv = data.st->colorCopy; + } + + if (!depthTex || !normalTex || !baseTex || !giT || !giB || !giTemp) + return; auto& cache = GetPassResourceCache(); auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + return; auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); auto* psTraceR = loader->GetCachedReflection("ssgi_trace", ".ps"); auto* psBlurR = loader->GetCachedReflection("ssgi_blur", ".ps"); @@ -269,15 +309,21 @@ VirtualResourceHandle setupSSGIPass( auto staticGlobalsCB = cache.GetOrCreateVolatileCB( "Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + if (staticGlobalsCB) + { + StaticGlobals sg = BuildStaticGlobals(); + cmd->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } struct alignas(16) TraceCB { float intensity, maxDistance, thickness, rayCount; + float frameIndex, p1, p2, p3; }; float rays = (data.quality <= 1) ? 4.f : (data.quality == 2) ? 6.f : 8.f; float maxDist = (data.quality <= 1) ? 8.f : (data.quality == 2) ? 12.f : 18.f; - TraceCB tcb{ data.intensity, maxDist, 0.85f, rays }; - auto* traceCB = cache.GetOrCreateVolatileCB("SSGI", "TraceParams", sizeof(TraceCB), data.device); + TraceCB tcb{ data.intensity, maxDist, 0.85f, rays, float(Device.dwFrame & 1023), 0, 0, 0 }; + auto* traceCB = cache.GetOrCreateVolatileCB("SSGI", "TraceParams_v9", sizeof(TraceCB), data.device); if (traceCB) cmd->writeBuffer(traceCB, &tcb, sizeof(tcb)); @@ -289,16 +335,17 @@ VirtualResourceHandle setupSSGIPass( bsb.ConstantBuffer("SSGIParams", traceCB); bsb.Texture("g_Normal", normalTex) .Texture("g_Base", baseTex) - .Texture("g_SceneColor", data.st->colorCopy) + .Texture("g_SceneColor", sceneSrv) .Texture("g_SceneDepth", depthTex); auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->traceLayout, nv); - DrawFullscreen(cmd, nv, data.st->tracePipeline, set, giT, data.tw, data.th, "SSGI_Trace"); + if (set) + DrawSSGIFullscreen(cmd, nv, data.st->tracePipeline, set, giT, data.tw, data.th, "SSGI_Trace_v9"); cmd->setTextureState(giT, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); } struct alignas(16) BlurCB { float texelX, texelY, p2, p3; }; BlurCB bcb{ 1.f / float(data.tw), 1.f / float(data.th), 0, 0 }; - auto* blurCB = cache.GetOrCreateVolatileCB("SSGI", "BlurParams", sizeof(BlurCB), data.device); + auto* blurCB = cache.GetOrCreateVolatileCB("SSGI", "BlurParams_v9", sizeof(BlurCB), data.device); if (blurCB) cmd->writeBuffer(blurCB, &bcb, sizeof(bcb)); @@ -308,7 +355,8 @@ VirtualResourceHandle setupSSGIPass( bsb.ConstantBuffer("SSGIBlurParams", blurCB); bsb.Texture("g_SSGI", giT).Texture("g_Depth", depthTex); auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->blurLayout, nv); - DrawFullscreen(cmd, nv, data.st->blurPipeline, set, giB, data.tw, data.th, "SSGI_Blur"); + if (set) + DrawSSGIFullscreen(cmd, nv, data.st->blurPipeline, set, giB, data.tw, data.th, "SSGI_Blur_v9"); cmd->setTextureState(giB, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); } @@ -335,9 +383,15 @@ VirtualResourceHandle setupSSGIPass( } if (hist && motionTex) { - struct alignas(16) TempCB { float sx, sy, hasHistory, pad; }; - TempCB tcb2{ float(data.tw), float(data.th), 1.f, 0.f }; - auto* tempCB = cache.GetOrCreateVolatileCB("SSGI", "TemporalParams", sizeof(TempCB), data.device); + struct alignas(16) TempCB + { + float traceX, traceY, hasHistory, pad0; + float fullX, fullY, pad1, pad2; + }; + TempCB tcb2{ + float(data.tw), float(data.th), 1.f, 0.f, + float(data.width), float(data.height), 0.f, 0.f}; + auto* tempCB = cache.GetOrCreateVolatileCB("SSGI", "TemporalParams_v9", sizeof(TempCB), data.device); if (tempCB) cmd->writeBuffer(tempCB, &tcb2, sizeof(tcb2)); BindingSetBuilder bsb(*vsR, *psTempR, nv, "SSGI.Temporal"); @@ -348,9 +402,12 @@ VirtualResourceHandle setupSSGIPass( .Texture("g_Motion", motionTex) .Texture("g_Depth", depthTex); auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->temporalLayout, nv); - DrawFullscreen(cmd, nv, data.st->temporalPipeline, set, giTemp, data.tw, data.th, "SSGI_Temporal"); - cmd->setTextureState(giTemp, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); - giForApply = giTemp; + if (set) + { + DrawSSGIFullscreen(cmd, nv, data.st->temporalPipeline, set, giTemp, data.tw, data.th, "SSGI_Temporal_v9"); + cmd->setTextureState(giTemp, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + giForApply = giTemp; + } } } @@ -365,7 +422,7 @@ VirtualResourceHandle setupSSGIPass( struct alignas(16) ApplyCB { float intensity, p1, p2, p3; }; ApplyCB acb{ 1.0f, 0, 0, 0 }; - auto* applyCB = cache.GetOrCreateVolatileCB("SSGI", "ApplyParams", sizeof(ApplyCB), data.device); + auto* applyCB = cache.GetOrCreateVolatileCB("SSGI", "ApplyParams_v9", sizeof(ApplyCB), data.device); if (applyCB) cmd->writeBuffer(applyCB, &acb, sizeof(acb)); @@ -375,14 +432,20 @@ VirtualResourceHandle setupSSGIPass( bsb.ConstantBuffer("static_globals", staticGlobalsCB); if (applyCB) bsb.ConstantBuffer("SSGIApplyParams", applyCB); - bsb.Texture("g_Color", data.st->colorCopy) + bsb.Texture("g_Color", sceneSrv) .Texture("g_SSGI", giForApply) .Texture("g_Normal", normalTex) .Texture("g_Base", baseTex) .Texture("g_Depth", depthTex); auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->applyLayout, nv); - DrawFullscreen(cmd, nv, data.st->applyPipeline, set, out, data.width, data.height, "SSGI_Apply"); + if (set) + { + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); + DrawSSGIFullscreen(cmd, nv, data.st->applyPipeline, set, out, data.width, data.height, "SSGI_Apply_v9"); + } } + + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); }); return pd.output; diff --git a/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.h index e1da6ab0438..1a9a3c208c1 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/SSGIPassSetup.h @@ -21,10 +21,17 @@ struct SSGIPassState nvrhi::BindingLayoutHandle temporalLayout; nvrhi::BindingLayoutHandle applyLayout; nvrhi::TextureHandle colorCopy; + nvrhi::TextureHandle outputTex; + nvrhi::TextureHandle giTraceTex; + nvrhi::TextureHandle giBlurTex; + nvrhi::TextureHandle giTemporalTex; nvrhi::TextureHandle history[2]; u32 historyW = 0; u32 historyH = 0; + u32 outputW = 0; + u32 outputH = 0; u32 historyIndex = 0; + u32 pipeVersion = 0; bool hasHistory = false; bool initialized = false; }; diff --git a/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.cpp index 5248386430f..34b922333fb 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.cpp @@ -2,6 +2,7 @@ #include "SSRPassSetup.h" #include "ShaderConstants.h" #include "PassCommon.h" +#include "IBLPrefilterPassSetup.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/PassResourceCache.h" #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" @@ -18,7 +19,7 @@ using namespace framegraph; namespace { -constexpr u32 kSSRPipeVersion = 43; +constexpr u32 kSSRPipeVersion = 72; struct SSRQualityParams { @@ -27,45 +28,63 @@ struct SSRQualityParams float thickness; float refine; float blurIntensity; + float blurKernel; + float enableNearMarch; + float enableBlurRefine; }; SSRQualityParams QualityFromConsole() { switch (std::clamp(ps_r_ssr_quality, 1, 4)) { - case 1: return {16.f, 80.f, 1.50f, 2.f, 0.70f}; - case 2: return {24.f, 120.f, 1.25f, 4.f, 0.85f}; - case 3: return {32.f, 160.f, 1.00f, 6.f, 1.00f}; - default: return {48.f, 220.f, 0.85f, 8.f, 1.00f}; + case 1: return {16.f, 80.f, 1.50f, 2.f, 1.25f, 3.f, 0.f, 0.f}; + case 2: return {24.f, 120.f, 1.25f, 4.f, 1.35f, 4.f, 0.f, 0.f}; + case 3: return {32.f, 160.f, 1.00f, 6.f, 1.45f, 5.f, 1.f, 1.f}; + default: return {48.f, 220.f, 0.85f, 8.f, 1.55f, 6.f, 1.f, 1.f}; } } -void EnsureSSRBuffer(nvrhi::IDevice* nv, SSRPassState& st, u32 w, u32 h) +void EnsureSSRBuffer(nvrhi::IDevice* nv, SSRPassState& st, u32 fullW, u32 fullH, u32 traceW, u32 traceH) { - if (st.history[0] && st.history[1] && st.outputTex && st.blurTex && - st.historyW == w && st.historyH == h) + const bool needTrace = !st.history[0] || !st.history[1] || !st.blurTex || + st.historyW != traceW || st.historyH != traceH; + const bool needOut = !st.outputTex || st.outputW != fullW || st.outputH != fullH; + if (!needTrace && !needOut) return; + nvrhi::TextureDesc td; - td.width = w; - td.height = h; td.format = nvrhi::Format::RGBA16_FLOAT; td.isRenderTarget = true; td.isShaderResource = true; td.initialState = nvrhi::ResourceStates::ShaderResource; td.keepInitialState = true; - td.debugName = "rt_SSR_History0"; - st.history[0] = nv->createTexture(td); - td.debugName = "rt_SSR_History1"; - st.history[1] = nv->createTexture(td); - td.debugName = "rt_SSR_Output"; - st.outputTex = nv->createTexture(td); - td.debugName = "rt_SSR_BlurPersist"; - st.blurTex = nv->createTexture(td); - st.historyW = w; - st.historyH = h; - st.historyIndex = 0; - st.historyNeedsClear = true; - st.hasHistory = false; + + if (needOut) + { + td.width = fullW; + td.height = fullH; + td.debugName = "rt_SSR_Output"; + st.outputTex = nv->createTexture(td); + st.outputW = fullW; + st.outputH = fullH; + } + + if (needTrace) + { + td.width = traceW; + td.height = traceH; + td.debugName = "rt_SSR_History0"; + st.history[0] = nv->createTexture(td); + td.debugName = "rt_SSR_History1"; + st.history[1] = nv->createTexture(td); + td.debugName = "rt_SSR_BlurPersist"; + st.blurTex = nv->createTexture(td); + st.historyW = traceW; + st.historyH = traceH; + st.historyIndex = 0; + st.historyNeedsClear = true; + st.hasHistory = false; + } } void DrawSSRFullscreen( @@ -114,23 +133,26 @@ void InitSSR(nvrhi::IDevice* nv, SSRPassState& st) auto vs = loader->LoadVertexShader("fullscreen"); auto ps = loader->LoadPixelShader("ssr_resolve"); auto psBlur = loader->LoadPixelShader("ssr_blur"); + auto psTemp = loader->LoadPixelShader("ssr_temporal"); auto psApply = loader->LoadPixelShader("ssr_apply"); - if (!vs.handle || !ps.handle || !psBlur.handle || !psApply.handle || - !vs.reflection || !ps.reflection || !psBlur.reflection || !psApply.reflection) + if (!vs.handle || !ps.handle || !psBlur.handle || !psTemp.handle || !psApply.handle || + !vs.reflection || !ps.reflection || !psBlur.reflection || !psTemp.reflection || !psApply.reflection) { - Msg("! [SSR] Shader load failed (resolve=%d blur=%d apply=%d) — will retry", - !!ps.handle, !!psBlur.handle, !!psApply.handle); + Msg("! [SSR] Shader load failed (resolve=%d blur=%d temp=%d apply=%d) — will retry", + !!ps.handle, !!psBlur.handle, !!psTemp.handle, !!psApply.handle); st.initialized = false; return; } auto& cache = GetPassResourceCache(); st.layout = cache.GetOrCreateBindingLayoutFromReflection( - "SSRResolve_v45", *vs.reflection, *ps.reflection, nv); + "SSRResolve_v71", *vs.reflection, *ps.reflection, nv); st.blurLayout = cache.GetOrCreateBindingLayoutFromReflection( - "SSRBlur_v45", *vs.reflection, *psBlur.reflection, nv); + "SSRBlur_v71", *vs.reflection, *psBlur.reflection, nv); + st.temporalLayout = cache.GetOrCreateBindingLayoutFromReflection( + "SSRTemporal_v71", *vs.reflection, *psTemp.reflection, nv); st.applyLayout = cache.GetOrCreateBindingLayoutFromReflection( - "SSRApply_v45", *vs.reflection, *psApply.reflection, nv); + "SSRApply_v72_MetalWet", *vs.reflection, *psApply.reflection, nv); auto makeSingle = [&](auto& psSh, nvrhi::BindingLayoutHandle layout, nvrhi::GraphicsPipelineHandle& pipe, const char* name) { @@ -153,17 +175,18 @@ void InitSSR(nvrhi::IDevice* nv, SSRPassState& st) pipe = cache.GetOrCreatePipeline(name, desc, fb, nv); }; - makeSingle(ps, st.layout, st.pipeline, "SSRResolve_v45"); - makeSingle(psBlur, st.blurLayout, st.blurPipeline, "SSRBlur_v45"); - makeSingle(psApply, st.applyLayout, st.applyPipeline, "SSRApply_v45"); + makeSingle(ps, st.layout, st.pipeline, "SSRResolve_v71"); + makeSingle(psBlur, st.blurLayout, st.blurPipeline, "SSRBlur_v71"); + makeSingle(psTemp, st.temporalLayout, st.temporalPipeline, "SSRTemporal_v71"); + makeSingle(psApply, st.applyLayout, st.applyPipeline, "SSRApply_v72_MetalWet"); st.initialized = true; st.pipeVersion = kSSRPipeVersion; - if (st.pipeline && st.blurPipeline && st.applyPipeline) - Msg("* [SSR] initialized v45 (SSR dim sky + NPC face sheen)"); + if (st.pipeline && st.blurPipeline && st.temporalPipeline && st.applyPipeline) + Msg("* [SSR] initialized v72 (SSR∪RT metal/wet)"); else - Msg("! [SSR] Pipeline create failed (resolve=%d blur=%d apply=%d)", - !!st.pipeline, !!st.blurPipeline, !!st.applyPipeline); + Msg("! [SSR] Pipeline create failed (resolve=%d blur=%d temp=%d apply=%d)", + !!st.pipeline, !!st.blurPipeline, !!st.temporalPipeline, !!st.applyPipeline); } } // namespace @@ -187,6 +210,7 @@ VirtualResourceHandle setupSSRPass( InitSSR(device->GetNVRHIDevice(), state); if (!state.pipeline || !state.layout || !state.blurPipeline || !state.blurLayout || + !state.temporalPipeline || !state.temporalLayout || !state.applyPipeline || !state.applyLayout) { static bool s_loggedPipe = false; @@ -203,14 +227,20 @@ VirtualResourceHandle setupSSRPass( if (!depth.is_valid()) return sceneColor; - EnsureSSRBuffer(device->GetNVRHIDevice(), state, width, height); + const int quality = std::clamp(ps_r_ssr_quality, 1, 4); +#if !defined(XR_PLATFORM_APPLE) + const bool halfRes = quality < 4; +#else + const bool halfRes = false; +#endif + const u32 tw = halfRes ? std::max(1u, width / 2) : width; + const u32 th = halfRes ? std::max(1u, height / 2) : height; + + EnsureSSRBuffer(device->GetNVRHIDevice(), state, width, height, tw, th); if (!state.outputTex || !state.blurTex || !state.history[0] || !state.history[1]) return sceneColor; (void)hasPrevFrame; - (void)motionVectors; - (void)worldPos; - // Persistent imported RTs — never FG-aliased (CreateTexture non-transient was not enough). ResourceDesc outDesc; outDesc.type = ResourceDesc::Type::Texture2D; outDesc.width = width; @@ -223,6 +253,8 @@ VirtualResourceHandle setupSSRPass( auto output = fg.ImportTexture("rt_SSR", state.outputTex.Get(), outDesc); ResourceDesc blurDesc = outDesc; + blurDesc.width = tw; + blurDesc.height = th; blurDesc.debugName = "rt_SSR_Blur"; auto blurRT = fg.ImportTexture("rt_SSR_Blur", state.blurTex.Get(), blurDesc); @@ -231,7 +263,7 @@ VirtualResourceHandle setupSSRPass( { const u32 writeIdx = state.historyIndex & 1u; const u32 readIdx = (writeIdx ^ 1u); - ResourceDesc ssrDesc = outDesc; + ResourceDesc ssrDesc = blurDesc; ssrDesc.debugName = "rt_SSR_Buffer"; ssrBuf = fg.ImportTexture("rt_SSR_Buffer", state.history[writeIdx].Get(), ssrDesc); ssrDesc.debugName = "rt_SSR_History"; @@ -240,11 +272,14 @@ VirtualResourceHandle setupSSRPass( struct PassData { - VirtualResourceHandle color, reflection, depth, normal, base, ssr, hist, blur, output; - u32 width, height; + VirtualResourceHandle color, reflection, depth, normal, base, worldPos, motion, ssr, hist, blur, output; + u32 width, height, tw, th; + int quality = 2; SSRPassState* st = nullptr; fg::RenderDevice* device = nullptr; bool hasHistory = false; + bool hasWorldPos = false; + bool hasMotion = false; }; auto& pd = fg.addCallbackPass( @@ -255,12 +290,21 @@ VirtualResourceHandle setupSSRPass( data.device = device; data.width = width; data.height = height; + data.tw = tw; + data.th = th; + data.quality = quality; data.hasHistory = state.hasHistory && !state.historyNeedsClear; data.color = pb.read(sceneColor, ResourceState::ShaderResource); data.reflection = pb.read(reflectionColor, ResourceState::ShaderResource); data.depth = pb.read(depth, ResourceState::ShaderResource); data.normal = pb.read(normal, ResourceState::ShaderResource); data.base = pb.read(baseColor, ResourceState::ShaderResource); + data.hasWorldPos = worldPos.is_valid(); + if (data.hasWorldPos) + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); + data.hasMotion = motionVectors.is_valid(); + if (data.hasMotion) + data.motion = pb.read(motionVectors, ResourceState::ShaderResource); data.hist = pb.read(ssrHist, ResourceState::ShaderResource); data.ssr = pb.write(ssrBuf, ResourceState::RenderTarget); data.blur = pb.write(blurRT, ResourceState::RenderTarget); @@ -275,6 +319,8 @@ VirtualResourceHandle setupSSRPass( auto* depthTex = graph.GetPhysicalTexture(data.depth); auto* normalTex = graph.GetPhysicalTexture(data.normal); auto* baseTex = graph.GetPhysicalTexture(data.base); + auto* worldPosTex = data.hasWorldPos ? graph.GetPhysicalTexture(data.worldPos) : nullptr; + auto* motionTex = data.hasMotion ? graph.GetPhysicalTexture(data.motion) : nullptr; auto* histTex = graph.GetPhysicalTexture(data.hist); auto* out = graph.GetPhysicalTexture(data.output); auto* ssrTex = graph.GetPhysicalTexture(data.ssr); @@ -288,6 +334,21 @@ VirtualResourceHandle setupSSRPass( if (!loader) return; + auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psR = loader->GetCachedReflection("ssr_resolve", ".ps"); + auto* psBlurR = loader->GetCachedReflection("ssr_blur", ".ps"); + auto* psApplyR = loader->GetCachedReflection("ssr_apply", ".ps"); + auto* psTempR = loader->GetCachedReflection("ssr_temporal", ".ps"); + if (!vsR || !psR || !psBlurR || !psTempR || !psApplyR) + { + cmd->setTextureState(color, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(out, nvrhi::TextureSlice(), color, nvrhi::TextureSlice()); + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + return; + } + +#if defined(XR_PLATFORM_APPLE) const auto& rdesc = reflection->getDesc(); if (!data.st->colorCopy || data.st->colorCopy->getDesc().width != rdesc.width || @@ -310,12 +371,6 @@ VirtualResourceHandle setupSSRPass( cmd->setTextureState(reflection, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); } - auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); - auto* psR = loader->GetCachedReflection("ssr_resolve", ".ps"); - auto* psBlurR = loader->GetCachedReflection("ssr_blur", ".ps"); - auto* psApplyR = loader->GetCachedReflection("ssr_apply", ".ps"); - - // Lit scene copy for apply SRV (must NOT be the same texture as `out`) const auto& cdesc = color->getDesc(); if (!data.st->litCopy || data.st->litCopy->getDesc().width != cdesc.width || @@ -337,7 +392,6 @@ VirtualResourceHandle setupSSRPass( cmd->setTextureState(data.st->litCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); } - // Depth → typeless SRV copy (same as water SSR on Metal/MoltenVK) { const auto& ddesc = depthTex->getDesc(); if (!data.st->depthCopy || @@ -363,22 +417,43 @@ VirtualResourceHandle setupSSRPass( cmd->setTextureState(data.st->depthCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); } } +#else + nvrhi::ITexture* sceneColorSrv = reflection; + nvrhi::ITexture* litSrv = (color != out) ? color : nullptr; + if (!litSrv) + { + const auto& cdesc = color->getDesc(); + if (!data.st->litCopy || + data.st->litCopy->getDesc().width != cdesc.width || + data.st->litCopy->getDesc().height != cdesc.height) + { + nvrhi::TextureDesc td = cdesc; + td.debugName = "SSR_LitCopy"; + td.isRenderTarget = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + data.st->litCopy = nv->createTexture(td); + } + if (data.st->litCopy) + { + cmd->setTextureState(color, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(data.st->litCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(data.st->litCopy, nvrhi::TextureSlice(), color, nvrhi::TextureSlice()); + cmd->setTextureState(data.st->litCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + litSrv = data.st->litCopy; + } + else + litSrv = color; + } +#endif - // Fallback: scene already in out if resolve/apply fail - cmd->setTextureState(color, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); - cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); - cmd->copyTexture(out, nvrhi::TextureSlice(), color, nvrhi::TextureSlice()); cmd->setTextureState(color, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(reflection, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); cmd->setTextureState(depthTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); cmd->setTextureState(normalTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); cmd->setTextureState(baseTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); - if (!vsR || !psR || !psBlurR || !psApplyR) - { - cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); - return; - } - auto staticGlobalsCB = cache.GetOrCreateVolatileCB( "Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); if (staticGlobalsCB) @@ -392,15 +467,15 @@ VirtualResourceHandle setupSSRPass( { float cutoff, intensity, hasHistory, frameIndex; float marchSteps, marchDistance, marchThickness, refineSteps; - float pad0, pad1, pad2, pad3; + float enableNearMarch, pad1, pad2, pad3; }; PCB p{ - 0.65f, 0.95f, + 0.65f, 1.25f, data.hasHistory ? 1.f : 0.f, float(Device.dwFrame & 1023), q.steps, q.distance, q.thickness, q.refine, - 0.f, 0.f, 0.f, 0.f}; - auto* ssrCB = cache.GetOrCreateVolatileCB("SSR", "SSRParams_v45", sizeof(PCB), data.device); + q.enableNearMarch, 0.f, 0.f, 0.f}; + auto* ssrCB = cache.GetOrCreateVolatileCB("SSR", "SSRParams_v71", sizeof(PCB), data.device); if (ssrCB) cmd->writeBuffer(ssrCB, &p, sizeof(p)); @@ -414,12 +489,11 @@ VirtualResourceHandle setupSSRPass( td.keepInitialState = true; s_zeroMotion = nv->createTexture(td); } + nvrhi::ITexture* motionSrv = motionTex ? motionTex : s_zeroMotion.Get(); - nvrhi::ITexture* sky0 = nullptr; - nvrhi::ITexture* sky1 = nullptr; - ResolveEnvSkyCubes(data.device, sky0, sky1); - if (!sky0) sky0 = cache.GetDummyCubeMap(nv); - if (!sky1) sky1 = sky0; + const auto& ibl = GetCurrentIBLBindResources(); + nvrhi::ITexture* sky0 = ibl.spec0 ? ibl.spec0 : cache.GetDummyCubeMap(nv); + nvrhi::ITexture* sky1 = ibl.spec1 ? ibl.spec1 : sky0; nvrhi::ITexture* histSrv = histTex ? histTex : cache.GetDummyContactHistory(nv); if (data.st->historyNeedsClear && histSrv) @@ -428,25 +502,41 @@ VirtualResourceHandle setupSSRPass( data.st->historyNeedsClear = false; } - static int s_ssrLog = 0; - if ((s_ssrLog++ % 300) == 0) - Msg("* [SSR] exec v45 resolve→blur→apply"); +#if defined(XR_PLATFORM_APPLE) + nvrhi::ITexture* sceneColorSrv = data.st->colorCopy ? data.st->colorCopy.Get() : reflection; + nvrhi::ITexture* litSrv = data.st->litCopy ? data.st->litCopy.Get() : color; + nvrhi::ITexture* depthSrv = data.st->depthCopy ? data.st->depthCopy.Get() : depthTex; +#else + nvrhi::ITexture* depthSrv = depthTex; +#endif - // 1) Resolve cmd->clearTextureFloat(ssrTex, nvrhi::AllSubresources, nvrhi::Color(0.f)); { - nvrhi::ITexture* depthSrv = data.st->depthCopy ? data.st->depthCopy.Get() : depthTex; BindingSetBuilder bsb(*vsR, *psR, nv, "SSR.Resolve"); if (staticGlobalsCB) bsb.ConstantBuffer("static_globals", staticGlobalsCB); if (ssrCB) bsb.ConstantBuffer("SSRParams", ssrCB); - bsb.Texture("g_Color", data.st->litCopy ? data.st->litCopy.Get() : color) + static nvrhi::TextureHandle s_zeroWorldPos; + if (!s_zeroWorldPos) + { + nvrhi::TextureDesc td; + td.width = 1; + td.height = 1; + td.format = nvrhi::Format::RGBA16_FLOAT; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "SSR_DummyWorldPos"; + s_zeroWorldPos = nv->createTexture(td); + } + nvrhi::ITexture* wpSrv = worldPosTex ? worldPosTex : s_zeroWorldPos.Get(); + bsb.Texture("g_Color", litSrv) .Texture("g_Normal", normalTex) .Texture("g_Base", baseTex) .Texture("g_History", histSrv) - .Texture("g_Motion", s_zeroMotion) - .Texture("g_SceneColor", data.st->colorCopy) + .Texture("g_WorldPos", wpSrv) + .Texture("g_Motion", motionSrv) + .Texture("g_SceneColor", sceneColorSrv) .Texture("g_SceneDepth", depthSrv) .Texture("s_env0", sky0) .Texture("s_env1", sky1); @@ -459,29 +549,25 @@ VirtualResourceHandle setupSSRPass( Msg("! [SSR] Resolve binding failed — scene passthrough"); s_loggedBind = true; } - // Never leave rt_SSR unwritten (transient-era footgun). - nvrhi::ITexture* litSrc = data.st->litCopy ? data.st->litCopy.Get() : color; - if (litSrc && out) + if (litSrv && out) { - cmd->setTextureState(litSrc, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(litSrv, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); - cmd->copyTexture(out, nvrhi::TextureSlice(), litSrc, nvrhi::TextureSlice()); + cmd->copyTexture(out, nvrhi::TextureSlice(), litSrv, nvrhi::TextureSlice()); } cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); return; } - DrawSSRFullscreen(cmd, nv, data.st->pipeline, set, ssrTex, data.width, data.height, "SSR_Resolve_v45"); + DrawSSRFullscreen(cmd, nv, data.st->pipeline, set, ssrTex, data.tw, data.th, "SSR_Resolve_v71"); cmd->setTextureState(ssrTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); } - // 2) Bilateral denoise - struct alignas(16) BlurCB { float texelX, texelY, blurIntensity, pad; }; - BlurCB bcb{1.f / float(data.width), 1.f / float(data.height), q.blurIntensity, 0.f}; - auto* blurCB = cache.GetOrCreateVolatileCB("SSR", "BlurParams_v45", sizeof(BlurCB), data.device); + struct alignas(16) BlurCB { float texelX, texelY, blurIntensity, blurKernel; }; + BlurCB bcb{1.f / float(data.tw), 1.f / float(data.th), q.blurIntensity, q.blurKernel}; + auto* blurCB = cache.GetOrCreateVolatileCB("SSR", "BlurParams_v71", sizeof(BlurCB), data.device); if (blurCB) cmd->writeBuffer(blurCB, &bcb, sizeof(bcb)); { - nvrhi::ITexture* depthSrv = data.st->depthCopy ? data.st->depthCopy.Get() : depthTex; BindingSetBuilder bsb(*vsR, *psBlurR, nv, "SSR.Blur"); if (blurCB) bsb.ConstantBuffer("SSRBlurParams", blurCB); @@ -490,7 +576,7 @@ VirtualResourceHandle setupSSRPass( .Texture("g_Normal", normalTex); auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->blurLayout, nv); if (set) - DrawSSRFullscreen(cmd, nv, data.st->blurPipeline, set, blurTex, data.width, data.height, "SSR_Blur_v45"); + DrawSSRFullscreen(cmd, nv, data.st->blurPipeline, set, blurTex, data.tw, data.th, "SSR_Blur_v71"); else { cmd->setTextureState(ssrTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); @@ -500,30 +586,77 @@ VirtualResourceHandle setupSSRPass( cmd->setTextureState(blurTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); } - // Store blurred SSR into history write slot for next frame temporal hold + nvrhi::ITexture* ssrForApply = blurTex; { - cmd->setTextureState(blurTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); - cmd->setTextureState(ssrTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); - cmd->copyTexture(ssrTex, nvrhi::TextureSlice(), blurTex, nvrhi::TextureSlice()); - cmd->setTextureState(ssrTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); - cmd->setTextureState(blurTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + struct alignas(16) TempCB { float sx, sy, hasHistory, pad; }; + TempCB tcb{ + float(data.tw), float(data.th), + data.hasHistory ? 1.f : 0.f, 0.f}; + auto* tempCB = cache.GetOrCreateVolatileCB("SSR", "TemporalParams_v71", sizeof(TempCB), data.device); + if (tempCB) + cmd->writeBuffer(tempCB, &tcb, sizeof(tcb)); + BindingSetBuilder bsb(*vsR, *psTempR, nv, "SSR.Temporal"); + if (tempCB) + bsb.ConstantBuffer("SSRTemporalParams", tempCB); + bsb.Texture("g_Current", blurTex) + .Texture("g_History", histSrv) + .Texture("g_Depth", depthSrv) + .Texture("g_Motion", motionSrv); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->temporalLayout, nv); + if (set) + { + DrawSSRFullscreen(cmd, nv, data.st->temporalPipeline, set, ssrTex, data.tw, data.th, "SSR_Temporal_v71"); + cmd->setTextureState(ssrTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + ssrForApply = ssrTex; + } + else + { + cmd->setTextureState(blurTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(ssrTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(ssrTex, nvrhi::TextureSlice(), blurTex, nvrhi::TextureSlice()); + cmd->setTextureState(ssrTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + ssrForApply = ssrTex; + } data.st->historyIndex ^= 1u; data.st->hasHistory = true; } - // 3) Apply: read litCopy, write out — never the same texture + if (q.enableBlurRefine > 0.5f && ssrForApply == ssrTex) + { + BlurCB bcb2{1.f / float(data.tw), 1.f / float(data.th), q.blurIntensity * 0.85f, q.blurKernel}; + auto* blurCB2 = cache.GetOrCreateVolatileCB("SSR", "BlurParams2_v71", sizeof(BlurCB), data.device); + if (blurCB2) + cmd->writeBuffer(blurCB2, &bcb2, sizeof(bcb2)); + BindingSetBuilder bsb(*vsR, *psBlurR, nv, "SSR.BlurRefine"); + if (blurCB2) + bsb.ConstantBuffer("SSRBlurParams", blurCB2); + bsb.Texture("g_SSR", ssrTex) + .Texture("g_Depth", depthSrv) + .Texture("g_Normal", normalTex); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->blurLayout, nv); + if (set) + { + DrawSSRFullscreen(cmd, nv, data.st->blurPipeline, set, blurTex, data.tw, data.th, "SSR_BlurRefine_v71"); + cmd->setTextureState(blurTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + ssrForApply = blurTex; + } + } + { - nvrhi::ITexture* litSrc = data.st->litCopy ? data.st->litCopy.Get() : color; BindingSetBuilder bsb(*vsR, *psApplyR, nv, "SSR.Apply"); - bsb.Texture("g_Color", litSrc).Texture("g_SSR", blurTex); + bsb.Texture("g_Color", litSrv) + .Texture("g_SSR", ssrForApply) + .Texture("g_Base", baseTex) + .Texture("g_Normal", normalTex) + .Texture("g_WorldPos", worldPosTex ? worldPosTex : cache.GetDummyContactHistory(nv)); auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->applyLayout, nv); if (set) - DrawSSRFullscreen(cmd, nv, data.st->applyPipeline, set, out, data.width, data.height, "SSR_Apply_v45"); - else if (litSrc && out) + DrawSSRFullscreen(cmd, nv, data.st->applyPipeline, set, out, data.width, data.height, "SSR_Apply_v72"); + else if (litSrv && out) { - cmd->setTextureState(litSrc, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(litSrv, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); - cmd->copyTexture(out, nvrhi::TextureSlice(), litSrc, nvrhi::TextureSlice()); + cmd->copyTexture(out, nvrhi::TextureSlice(), litSrv, nvrhi::TextureSlice()); } cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); } diff --git a/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.h index 062c14ac256..cbda0471bdf 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.h @@ -16,16 +16,20 @@ struct SSRPassState nvrhi::BindingLayoutHandle layout; nvrhi::GraphicsPipelineHandle blurPipeline; nvrhi::BindingLayoutHandle blurLayout; + nvrhi::GraphicsPipelineHandle temporalPipeline; + nvrhi::BindingLayoutHandle temporalLayout; nvrhi::GraphicsPipelineHandle applyPipeline; nvrhi::BindingLayoutHandle applyLayout; nvrhi::TextureHandle colorCopy; // pre-wet reflection source nvrhi::TextureHandle litCopy; // post-wet lit scene for apply (never R/W same as out) nvrhi::TextureHandle depthCopy; // typeless D32 SRV copy (Metal-safe, like water SSR) nvrhi::TextureHandle outputTex; // persistent composite out (never FG-aliased) - nvrhi::TextureHandle blurTex; // persistent blur RT - nvrhi::TextureHandle history[2]; + nvrhi::TextureHandle blurTex; // persistent blur RT (trace res) + nvrhi::TextureHandle history[2]; // trace-res temporal history u32 historyW = 0; u32 historyH = 0; + u32 outputW = 0; + u32 outputH = 0; u32 historyIndex = 0; u32 pipeVersion = 0; bool hasHistory = false; diff --git a/src/Layers/xrRender/FrameGraphPasses/SSSSSPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SSSSSPassSetup.cpp new file mode 100644 index 00000000000..1dffa18cab9 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/SSSSSPassSetup.cpp @@ -0,0 +1,215 @@ +#include "stdafx.h" +#include "SSSSSPassSetup.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" + +extern ENGINE_API int ps_r_sssss; +extern ENGINE_API float ps_r_sssss_width; +extern ENGINE_API float ps_r_sssss_strength; +extern ENGINE_API int ps_r_sssss_debug; + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +struct alignas(16) SSSSSCB +{ + float texelX, texelY, dirX, dirY; + float widthScale, strength, debugMode, pad0; +}; +static_assert(sizeof(SSSSSCB) % 16 == 0); + +void InitSSSSS(nvrhi::IDevice* nv, SSSSSPassState& st) +{ + if (st.initialized && st.blurPipeline && st.blurLayout && st.pipeVersion == 3) + return; + st.initialized = false; + st.pipeVersion = 0; + if (!nv) + { + st.initialized = true; + return; + } + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + { + st.initialized = true; + return; + } + auto vs = loader->LoadVertexShader("fullscreen"); + auto ps = loader->LoadPixelShader("sssss_blur"); + if (!vs.handle || !ps.handle || !vs.reflection || !ps.reflection) + { + st.initialized = true; + return; + } + auto& cache = GetPassResourceCache(); + st.blurLayout = cache.GetOrCreateBindingLayoutFromReflection( + "SSSSSBlur_v3", *vs.reflection, *ps.reflection, nv); + if (st.blurLayout) + { + nvrhi::GraphicsPipelineDesc desc; + desc.setVertexShader(vs.handle); + desc.setPixelShader(ps.handle); + desc.addBindingLayout(st.blurLayout); + desc.setPrimType(nvrhi::PrimitiveType::TriangleList); + desc.renderState.blendState.targets[0].setBlendEnable(false); + desc.renderState.depthStencilState.setDepthTestEnable(false); + desc.renderState.depthStencilState.setDepthWriteEnable(false); + desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + nvrhi::FramebufferInfoEx fb; + fb.addColorFormat(nvrhi::Format::RGBA16_FLOAT); + st.blurPipeline = cache.GetOrCreatePipeline("SSSSSBlur_v3", desc, fb, nv); + } + st.initialized = true; + st.pipeVersion = 3; +} + +void DrawSSSSSFullscreen( + nvrhi::ICommandList* cmd, + nvrhi::IDevice* nv, + nvrhi::GraphicsPipelineHandle pipe, + nvrhi::BindingSetHandle set, + nvrhi::ITexture* out, + u32 w, u32 h, + const char* fbName) +{ + if (!cmd || !pipe || !set || !out) + return; + auto& cache = GetPassResourceCache(); + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(out); + auto fb = cache.GetOrCreateFramebuffer(fbName, fbDesc, nv); + nvrhi::GraphicsState gs; + gs.pipeline = pipe; + gs.framebuffer = fb; + gs.bindings = { set }; + gs.viewport.addViewportAndScissorRect(nvrhi::Viewport(float(w), float(h))); + cmd->setGraphicsState(gs); + cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); +} +} // namespace + +VirtualResourceHandle setupSSSSSPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle sceneColor, + VirtualResourceHandle depth, + VirtualResourceHandle normal, + VirtualResourceHandle baseColor, + u32 width, + u32 height, + SSSSSPassState& state) +{ + if (!ps_r_sssss || !device || !device->GetNVRHIDevice()) + return sceneColor; + if (!sceneColor.is_valid() || !depth.is_valid() || !normal.is_valid() || !baseColor.is_valid()) + return sceneColor; + + InitSSSSS(device->GetNVRHIDevice(), state); + if (!state.blurPipeline || !state.blurLayout) + return sceneColor; + + ResourceDesc tmpDesc; + tmpDesc.type = ResourceDesc::Type::Texture2D; + tmpDesc.width = width; + tmpDesc.height = height; + tmpDesc.format = nvrhi::Format::RGBA16_FLOAT; + tmpDesc.isRenderTarget = true; + tmpDesc.isTransient = true; + tmpDesc.debugName = "rt_SSSSS_Temp"; + auto tempRT = fg.CreateTexture("rt_SSSSS_Temp", tmpDesc); + + ResourceDesc outDesc = tmpDesc; + outDesc.debugName = "rt_SSSSS_Out"; + auto output = fg.CreateTexture("rt_SSSSS_Out", outDesc); + + struct PassData + { + VirtualResourceHandle color, depth, normal, base, temp, output; + u32 width = 0, height = 0; + SSSSSPassState* st = nullptr; + fg::RenderDevice* device = nullptr; + float widthScale = 1.0f; + float strength = 1.0f; + float debugMode = 0.0f; + }; + + auto& pd = fg.addCallbackPass( + "SSSSS", + [&](FrameGraph& b, PassHandle ph, PassData& data) { + RenderPassBuilder pb(b, ph); + data.st = &state; + data.device = device; + data.width = width; + data.height = height; + data.widthScale = ps_r_sssss_width; + data.strength = ps_r_sssss_strength; + data.debugMode = ps_r_sssss_debug ? 1.f : 0.f; + data.color = pb.read(sceneColor, ResourceState::ShaderResource); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.normal = pb.read(normal, ResourceState::ShaderResource); + data.base = pb.read(baseColor, ResourceState::ShaderResource); + data.temp = pb.write(tempRT, ResourceState::RenderTarget); + data.output = pb.write(output, ResourceState::RenderTarget); + pb.sideEffects(); + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* nv = cmd ? cmd->getDevice() : nullptr; + auto* color = graph.GetPhysicalTexture(data.color); + auto* depthTex = graph.GetPhysicalTexture(data.depth); + auto* normalTex = graph.GetPhysicalTexture(data.normal); + auto* baseTex = graph.GetPhysicalTexture(data.base); + auto* tempTex = graph.GetPhysicalTexture(data.temp); + auto* out = graph.GetPhysicalTexture(data.output); + if (!cmd || !nv || !color || !depthTex || !normalTex || !baseTex || !tempTex || !out || !data.st) + return; + + auto& cache = GetPassResourceCache(); + auto* loader = GEnv.Render->GetShaderLoader(); + if (!loader) + return; + auto* vsR = loader->GetCachedReflection("fullscreen", ".vs"); + auto* psR = loader->GetCachedReflection("sssss_blur", ".ps"); + if (!vsR || !psR) + return; + + auto blurPass = [&](nvrhi::ITexture* src, nvrhi::ITexture* dst, float dirX, float dirY, const char* name) { + SSSSSCB cb{ + 1.f / float(data.width), 1.f / float(data.height), + dirX, dirY, + data.widthScale, data.strength, + data.debugMode, 0.f}; + auto* cbuf = cache.GetOrCreateVolatileCB("SSSSS", name, sizeof(SSSSSCB), data.device); + if (!cbuf) + return; + cmd->writeBuffer(cbuf, &cb, sizeof(cb)); + BindingSetBuilder bsb(*vsR, *psR, nv, name); + bsb.ConstantBuffer("SSSSSParams", cbuf) + .Texture("g_Color", src) + .Texture("g_Depth", depthTex) + .Texture("g_Normal", normalTex) + .Texture("g_Base", baseTex); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->blurLayout, nv); + if (set) + DrawSSSSSFullscreen(cmd, nv, data.st->blurPipeline, set, dst, data.width, data.height, name); + }; + + blurPass(color, tempTex, 1.f, 0.f, "SSSSS_H_v3"); + cmd->setTextureState(tempTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + blurPass(tempTex, out, 0.f, 1.f, "SSSSS_V_v3"); + cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + }); + + return pd.output; +} + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/SSSSSPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SSSSSPassSetup.h new file mode 100644 index 00000000000..e97027a0f57 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/SSSSSPassSetup.h @@ -0,0 +1,32 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes +{ + +struct SSSSSPassState +{ + nvrhi::GraphicsPipelineHandle blurPipeline; + nvrhi::BindingLayoutHandle blurLayout; + u32 pipeVersion = 0; + bool initialized = false; +}; + +framegraph::VirtualResourceHandle setupSSSSSPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle normal, + framegraph::VirtualResourceHandle baseColor, + u32 width, + u32 height, + SSSSSPassState& state); + +} // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.cpp index d8e754fb1b8..7c37935ec50 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.cpp @@ -3,6 +3,7 @@ #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" #include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" namespace xray::render::fg::passes { @@ -15,15 +16,57 @@ struct SceneReflectionCaptureData VirtualResourceHandle src; VirtualResourceHandle dst; }; + +nvrhi::TextureHandle EnsureSceneReflectionTexture( + nvrhi::IDevice* nvDevice, + SceneReflectionPassState& state, + u32 width, + u32 height) +{ + if (!nvDevice || width == 0 || height == 0) + return nullptr; + + if (state.texture && state.width == width && state.height == height) + return state.texture; + + nvrhi::TextureDesc desc; + desc.width = width; + desc.height = height; + desc.format = nvrhi::Format::RGBA16_FLOAT; + desc.isRenderTarget = true; + desc.isShaderResource = true; + desc.debugName = "rt_SceneReflection"; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + + state.texture = nvDevice->createTexture(desc); + state.width = width; + state.height = height; + if (!state.texture) + { + Msg("! [SceneReflection] Failed to create rt_SceneReflection %ux%u RGBA16F", + width, height); + state.width = 0; + state.height = 0; + } + return state.texture; +} } // namespace VirtualResourceHandle setupSceneReflectionCapture( FrameGraph& fg, + fg::RenderDevice* device, VirtualResourceHandle litSceneColor, u32 width, - u32 height) + u32 height, + SceneReflectionPassState& state) { - if (!litSceneColor.is_valid() || width == 0 || height == 0) + if (!litSceneColor.is_valid() || width == 0 || height == 0 || !device) + return litSceneColor; + + nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); + nvrhi::ITexture* phys = EnsureSceneReflectionTexture(nvDevice, state, width, height).Get(); + if (!phys) return litSceneColor; ResourceDesc desc; @@ -32,10 +75,9 @@ VirtualResourceHandle setupSceneReflectionCapture( desc.height = height; desc.format = nvrhi::Format::RGBA16_FLOAT; desc.isRenderTarget = true; - // Must NOT be transient — aliasing reused normal/worldPos memory → green/purple SSR desc.isTransient = false; desc.debugName = "rt_SceneReflection"; - auto dst = fg.CreateTexture("rt_SceneReflection", desc); + auto dst = fg.ImportTexture("rt_SceneReflection", phys, desc); (void)fg.addCallbackPass( "SceneReflectionCapture", diff --git a/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.h index 175820ad8df..5fcab17c02f 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/SceneReflectionPassSetup.h @@ -2,6 +2,7 @@ #include "Layers/xrRender/FrameGraph/FGTypes.h" #include "Layers/xrRender/FrameGraph/FGResource.h" +#include namespace xray::render::framegraph { class FrameGraph; } namespace xray::render::fg { class RenderDevice; } @@ -9,15 +10,19 @@ namespace xray::render::fg { class RenderDevice; } namespace xray::render::fg::passes { -// Snapshot of fully-lit scene color BEFORE wet/AO darkening. -// This is the shared reflection source for: -// - wet surface specular (screen-space + sky IBL fallback) -// - fullscreen SSR -// Named RT: "rt_SceneReflection" +struct SceneReflectionPassState +{ + nvrhi::TextureHandle texture; + u32 width = 0; + u32 height = 0; +}; + framegraph::VirtualResourceHandle setupSceneReflectionCapture( framegraph::FrameGraph& fg, + fg::RenderDevice* device, framegraph::VirtualResourceHandle litSceneColor, u32 width, - u32 height); + u32 height, + SceneReflectionPassState& state); } // namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.h b/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.h index 1f7bf708dce..ea6436a7dfa 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.h +++ b/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.h @@ -23,8 +23,14 @@ extern ENGINE_API Fvector4 ps_dev_param_4; extern ENGINE_API float psHUD_FOV; extern ENGINE_API int ps_r_contact_shadows; extern ENGINE_API float ps_r_contact_shadows_length; +extern ENGINE_API int ps_r_rt_gi; +extern ENGINE_API float ps_r_rt_gi_ambient_scale; +extern ENGINE_API int ps_r_path_tracer; extern ENGINE_API int ps_r_sky_ibl; extern ENGINE_API float ps_r_sky_ibl_intensity; +extern ENGINE_API int ps_r_ibl_prefilter; +extern ENGINE_API int ps_r_ibl_probe_auto; +extern ENGINE_API float ps_r_ibl_probe_radius; extern ENGINE_API int ps_r_foliage_sss; extern ENGINE_API float ps_r_foliage_sss_intensity; extern ECORE_API u32 ps_r_sun_quality; @@ -178,14 +184,15 @@ inline void FillClusterParams(StaticGlobals& cb) } else { - const u32 tilesX = (w + ::xray::render::fg::CLUSTER_TILE_SIZE - 1) / ::xray::render::fg::CLUSTER_TILE_SIZE; - const u32 tilesY = (h + ::xray::render::fg::CLUSTER_TILE_SIZE - 1) / ::xray::render::fg::CLUSTER_TILE_SIZE; + const u32 tileSize = ::xray::render::fg::ClusterTileSize(); + const u32 tilesX = (w + tileSize - 1) / tileSize; + const u32 tilesY = (h + tileSize - 1) / tileSize; cb.cluster_params.set( static_cast(tilesX), static_cast(tilesY), static_cast(::xray::render::fg::CLUSTER_NUM_SLICES), 0.0f); - cb.cluster_scales.set(zNear, zFar, 1.0f, static_cast(::xray::render::fg::CLUSTER_TILE_SIZE)); + cb.cluster_scales.set(zNear, zFar, 1.0f, static_cast(tileSize)); } } @@ -249,12 +256,23 @@ inline void FillGlobalConstants(GlobalConstants& cb) { cb.pos_decompression_params2.set((float)Device.dwWidth, (float)Device.dwHeight, 1.0f / (float)Device.dwWidth, 1.0f / (float)Device.dwHeight); // Parallax mapping: .x = height scale (r2_parallax_h; 0 disables object parallax), - // .y = bias, .z = foliage SSS intensity, .w = sky IBL intensity. Object steep - // parallax in bindless_forward.ps keys off .x>0 and requires r2_steep_parallax. + // .y = bias, .z = foliage SSS intensity, .w = sky IBL intensity. + // When RT GI is active, .w = -1 signals unlit GBuffer-only forward (see common_functions.h). const float parallaxH = ps_r2_ls_flags.test(R2FLAG_STEEP_PARALLAX) ? ps_r2_df_parallax_h : 0.0f; - cb.parallax.set(parallaxH, -0.01f, - (ps_r_foliage_sss != 0) ? ps_r_foliage_sss_intensity : 0.0f, - (ps_r_sky_ibl != 0) ? ps_r_sky_ibl_intensity : 0.0f); + const bool rtgiUnlit = (ps_r_rt_gi != 0) || (ps_r_path_tracer != 0); + if (rtgiUnlit) + { + cb.parallax.set(parallaxH, -0.01f, + (ps_r_foliage_sss != 0) ? ps_r_foliage_sss_intensity : 0.0f, + -1.0f); + cb.padding3 = 0.0f; + } + else + { + cb.parallax.set(parallaxH, -0.01f, + (ps_r_foliage_sss != 0) ? ps_r_foliage_sss_intensity : 0.0f, + (ps_r_sky_ibl != 0) ? ps_r_sky_ibl_intensity : 0.0f); + } // Screen resolution (for UI shaders and other effects) cb.screen_res.set( @@ -266,7 +284,8 @@ inline void FillGlobalConstants(GlobalConstants& cb) { cb.hud_fov = psHUD_FOV; // Screen-space contact shadow length (0 = disabled); packed as StaticGlobals.padding3 - cb.padding3 = (ps_r_contact_shadows != 0) ? ps_r_contact_shadows_length : 0.0f; + if (!rtgiUnlit) + cb.padding3 = (ps_r_contact_shadows != 0) ? ps_r_contact_shadows_length : 0.0f; // ═══════════════════════════════════════════════════════ // FORWARD+ EXTENSIONS (Phase 1.3) @@ -318,16 +337,34 @@ inline void FillGlobalConstants(GlobalConstants& cb) { cb.dev_param_3 = ps_dev_param_3; cb.dev_param_4 = ps_dev_param_4; + // Classic soft-filter kernels (were registered but unused). Drive FG PCF/PCSS. + // .x = r2_ls_dsm_kernel — directional / sun soft scale (default 0.7) + // .y = r2_ls_psm_kernel — point / OMNIPART local soft (texels) + // .z = r2_ls_ssm_kernel — spot local soft (texels) + // .w = r2_slight_fade — shadow distance / LOD fade + cb.dev_param_1.x = ps_r2_ls_dsm_kernel; + cb.dev_param_1.y = ps_r2_ls_psm_kernel; + cb.dev_param_1.z = ps_r2_ls_ssm_kernel; + cb.dev_param_1.w = ps_r2_slight_fade; + + // .x = |r2_ls_depth_bias| for local SampleCmp receiver bias + cb.dev_param_2.x = std::abs(ps_r2_ls_depth_bias); + // r_cluster_debug -> common_functions.h ClusterDebugColor (dev_param_2.w) + cb.dev_param_2.w = float(ps_r_cluster_debug); + cb.dev_param_2.y = (ps_r_shadow_hzb != 0) ? 1.0f : 0.0f; + cb.dev_param_2.z = (ps_r_shadow_mask != 0) ? 1.0f : 0.0f; + // r_shadow_debug overlay mode -> shadow_sampling.h ShadowDebugColor cb.dev_param_3.z = float(ps_r_shadow_debug); // r2_sun_normal_bias (meters) -> shadow_sampling.h NormalOffsetWorld cb.dev_param_3.w = ps_r2_sun_normal_bias; // PCSS filtering controls -> shadow_sampling.h SampleCascadePCSS. - // (dev_param_4 was previously view_shadow_proj, which no shader reads.) cb.dev_param_4.x = ps_r2_sun_soft; // max penumbra (texels) — softness cb.dev_param_4.y = ps_r2_sun_blocker; // blocker-search spacing (texels) cb.dev_param_4.z = ps_r2_sun_contact; // min penumbra (texels) — contact sharpness + cb.dev_param_4.w = float(xray::render::fg::LocalShadowAtlasSize()) + + ((ps_r_local_shadow_filter != 0) ? 0.5f : 0.0f); if (g_ShadowCascadeGPUData.valid) { @@ -387,26 +424,37 @@ inline void FillSunConstants(StaticGlobals& cb, const SunLightData& sun) { dir.normalize_safe(); cb.L_sun_dir_w.set(dir.x, dir.y, dir.z); + float ambScale = 1.0f; + if ((ps_r_rt_gi != 0) || (ps_r_path_tracer != 0)) + ambScale = std::clamp(ps_r_rt_gi_ambient_scale, 0.0f, 1.0f); + cb.L_ambient.set( - desc.ambient.x * ps_r2_sun_lumscale_amb, - desc.ambient.y * ps_r2_sun_lumscale_amb, - desc.ambient.z * ps_r2_sun_lumscale_amb, + desc.ambient.x * ps_r2_sun_lumscale_amb * ambScale, + desc.ambient.y * ps_r2_sun_lumscale_amb * ambScale, + desc.ambient.z * ps_r2_sun_lumscale_amb * ambScale, desc.weight ); - // 3-arg set keeps L_hemi_color.w (ps_r2_sun_lumscale_hemi from FillGlobalConstants) cb.L_hemi_color.set( - desc.hemi_color.x, - desc.hemi_color.y, - desc.hemi_color.z + desc.hemi_color.x * ambScale, + desc.hemi_color.y * ambScale, + desc.hemi_color.z * ambScale ); } void GetSunLightData(SunLightData& outSun, float hdrIntensity = 2.0f); -inline StaticGlobals BuildStaticGlobals(float hdrIntensity = 2.0f) { +inline StaticGlobals BuildStaticGlobals(float hdrIntensity = 2.0f, bool allowRtgiUnlit = true) { StaticGlobals sg = {}; FillGlobalConstants(sg); + if (!allowRtgiUnlit && sg.parallax.w < -0.5f) + { + const float parallaxH = ps_r2_ls_flags.test(R2FLAG_STEEP_PARALLAX) ? ps_r2_df_parallax_h : 0.0f; + sg.parallax.set(parallaxH, -0.01f, + (ps_r_foliage_sss != 0) ? ps_r_foliage_sss_intensity : 0.0f, + (ps_r_sky_ibl != 0) ? ps_r_sky_ibl_intensity : 0.0f); + sg.padding3 = (ps_r_contact_shadows != 0) ? ps_r_contact_shadows_length : 0.0f; + } SunLightData sunData; GetSunLightData(sunData, hdrIntensity); FillSunConstants(sg, sunData); diff --git a/src/Layers/xrRender/FrameGraphPasses/ShadowHZBPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ShadowHZBPassSetup.cpp new file mode 100644 index 00000000000..c79b5dda70a --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/ShadowHZBPassSetup.cpp @@ -0,0 +1,238 @@ +#include "stdafx.h" +#include "ShadowHZBPassSetup.h" +#include "HiZBuildPassSetup.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/xrRender_console.h" +#include + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +struct ShadowHZBCB +{ + u32 outputWidth; + u32 outputHeight; + u32 inputMipLevel; + u32 isFirstMip; +}; + +void EnsureHZBTextures(nvrhi::IDevice* nv, ShadowHZBPassState& st, ShadowPassState& shadow) +{ + for (u32 c = 0; c < kCSMCascadeCount; ++c) + { + const u32 res = shadow.cascadeResolution[c] ? shadow.cascadeResolution[c] : GetCSMCascadeResolution(c); + const u32 baseW = std::max(1u, res / 2); + const u32 baseH = std::max(1u, res / 2); + const u32 mips = CalculateHiZMipLevels(baseW, baseH); + if (st.hzb[c] && st.cascadeRes[c] == res && st.mipLevels[c] == mips) + continue; + + nvrhi::TextureDesc td; + td.width = baseW; + td.height = baseH; + td.format = nvrhi::Format::R32_FLOAT; + td.mipLevels = mips; + td.isUAV = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + char nameBuf[64]; + snprintf(nameBuf, sizeof(nameBuf), "rt_ShadowHZB_c%u", c); + td.debugName = nameBuf; + st.hzb[c] = nv->createTexture(td); + st.cascadeRes[c] = res; + st.mipLevels[c] = st.hzb[c] ? mips : 0; + if (st.hzb[c]) + { + nvrhi::CommandListHandle cmd = nv->createCommandList(); + cmd->open(); + cmd->clearTextureFloat(st.hzb[c], nvrhi::AllSubresources, nvrhi::Color(1.f)); + cmd->close(); + nv->executeCommandList(cmd); + } + } +} +} // namespace + +ShadowHZBOutput setupShadowHZBPass( + FrameGraph& fg, + fg::RenderDevice* device, + ShadowPassState& shadowState, + ShadowHZBPassState& state, + VirtualResourceHandle shadowMapHandle) +{ + ShadowHZBOutput out{}; + if (!device || ps_r_shadow_hzb == 0 || !shadowState.enabled) + return out; + + nvrhi::IDevice* nv = device->GetNVRHIDevice(); + if (!nv) + return out; + + if (!state.initialized) + { + auto cs = GEnv.Render->GetShaderLoader()->LoadComputeShader("shadow_hzb_build"); + if (!cs.handle || !cs.reflection) + { + Msg("! [ShadowHZB] shadow_hzb_build.cs missing"); + state.initialized = true; + state.computeEnabled = false; + return out; + } + auto& cache = GetPassResourceCache(); + state.layout = cache.GetOrCreateBindingLayoutFromReflection( + "ShadowHZBPass", *cs.reflection, nv); + if (state.layout) + { + nvrhi::ComputePipelineDesc pd; + pd.CS = cs.handle; + pd.bindingLayouts = {state.layout}; + state.pipeline = cache.GetOrCreateComputePipeline("ShadowHZBPass", pd, nv); + } + state.computeEnabled = (state.pipeline != nullptr); + state.initialized = true; + if (state.computeEnabled) + Msg("* [ShadowHZB] pipeline ready"); + cs.reflection = nullptr; + } + + if (!state.computeEnabled) + return out; + + EnsureHZBTextures(nv, state, shadowState); + for (u32 c = 0; c < kCSMCascadeCount; ++c) + { + if (!state.hzb[c] || !shadowState.shadowCascades[c]) + return out; + } + + VirtualResourceHandle hzbHandles[kCSMCascadeCount]; + for (u32 c = 0; c < kCSMCascadeCount; ++c) + { + ResourceDesc desc; + desc.type = ResourceDesc::Type::Texture2D; + desc.width = state.hzb[c]->getDesc().width; + desc.height = state.hzb[c]->getDesc().height; + desc.format = nvrhi::Format::R32_FLOAT; + desc.mipLevels = state.mipLevels[c]; + desc.isUAV = true; + desc.isImported = true; + char nameBuf[64]; + snprintf(nameBuf, sizeof(nameBuf), "rt_ShadowHZB_c%u", c); + desc.debugName = nameBuf; + hzbHandles[c] = fg.ImportTexture(nameBuf, state.hzb[c], desc); + } + + struct PassData + { + ShadowHZBPassState* st = nullptr; + ShadowPassState* shadow = nullptr; + fg::RenderDevice* device = nullptr; + VirtualResourceHandle hzbHandles[kCSMCascadeCount]; + }; + + fg.addCallbackPass( + "ShadowHZB", + [&, hzbHandles, shadowMapHandle](FrameGraph& builder, PassHandle passHandle, PassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.st = &state; + data.shadow = &shadowState; + data.device = device; + if (shadowMapHandle.is_valid()) + pb.read(shadowMapHandle, ResourceState::ShaderResource); + for (u32 c = 0; c < kCSMCascadeCount; ++c) + { + data.hzbHandles[c] = pb.write(hzbHandles[c], ResourceState::UnorderedAccess); + } + }, + [](const PassData& data, const FrameGraph&, fg::RenderContext* ctx) { + if (!data.st || !data.shadow || !data.st->pipeline) + return; + nvrhi::ICommandList* cmd = ctx->GetCommandList(); + nvrhi::IDevice* nv = data.device->GetNVRHIDevice(); + if (!cmd || !nv) + return; + + auto* refl = GEnv.Render->GetShaderLoader()->GetCachedReflection("shadow_hzb_build", ".cs"); + if (!refl) + return; + + auto cb = GetPassResourceCache().GetOrCreateVolatileCB( + "ShadowHZB", "HZBParams", sizeof(ShadowHZBCB), data.device, 256); + if (!cb) + return; + + constexpr u32 kMaxShadowHZBMips = 7; + for (u32 c = 0; c < kCSMCascadeCount; ++c) + { + nvrhi::ITexture* src = data.shadow->shadowCascades[c]; + nvrhi::ITexture* hzb = data.st->hzb[c]; + if (!src || !hzb) + continue; + + const u32 baseW = hzb->getDesc().width; + const u32 baseH = hzb->getDesc().height; + const u32 mips = std::min(data.st->mipLevels[c], kMaxShadowHZBMips); + + for (u32 mip = 0; mip < mips; ++mip) + { + const u32 outW = std::max(1u, baseW >> mip); + const u32 outH = std::max(1u, baseH >> mip); + ShadowHZBCB pcb{}; + pcb.outputWidth = outW; + pcb.outputHeight = outH; + pcb.inputMipLevel = 0; + pcb.isFirstMip = (mip == 0) ? 1u : 0u; + cmd->writeBuffer(cb, &pcb, sizeof(pcb)); + + nvrhi::TextureSubresourceSet inSub; + inSub.baseMipLevel = (mip > 0) ? mip - 1 : 0; + inSub.numMipLevels = 1; + nvrhi::TextureSubresourceSet outSub; + outSub.baseMipLevel = mip; + outSub.numMipLevels = 1; + + BindingSetBuilder bsb(*refl, nv, "ShadowHZB"); + if (mip == 0) + bsb.Texture("g_input_depth", src); + else + bsb.Texture("g_input_depth", hzb, nvrhi::Format::R32_FLOAT, inSub); + bsb.TextureUAV("g_output_hiz", hzb, nvrhi::Format::R32_FLOAT, outSub); + bsb.ConstantBuffer("ShadowHZBParams", cb); + auto set = GetPassResourceCache().GetOrCreateBindingSet( + bsb.Build(), data.st->layout, nv); + if (!set) + continue; + + nvrhi::ComputeState cs; + cs.pipeline = data.st->pipeline; + cs.bindings = {set}; + cmd->setComputeState(cs); + cmd->dispatch((outW + 7) / 8, (outH + 7) / 8, 1); + if (mip + 1 < mips) + nvrhi::utils::TextureUavBarrier(cmd, hzb); + } + cmd->setTextureState(hzb, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + }); + + out.valid = true; + for (u32 c = 0; c < kCSMCascadeCount; ++c) + { + out.hzb[c] = state.hzb[c]; + out.handles[c] = hzbHandles[c]; + out.mipLevels[c] = state.mipLevels[c]; + } + return out; +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/ShadowHZBPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ShadowHZBPassSetup.h new file mode 100644 index 00000000000..da2c4639645 --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/ShadowHZBPassSetup.h @@ -0,0 +1,47 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include "Layers/xrRender/FrameGraphPasses/ShadowPassSetup.h" +#include + +namespace xray::render::framegraph +{ +class FrameGraph; +} + +namespace xray::render::fg +{ +class RenderDevice; +} + +namespace xray::render::fg::passes +{ + +struct ShadowHZBPassState +{ + nvrhi::ComputePipelineHandle pipeline; + nvrhi::BindingLayoutHandle layout; + nvrhi::TextureHandle hzb[kCSMCascadeCount]; + u32 cascadeRes[kCSMCascadeCount] = {}; + u32 mipLevels[kCSMCascadeCount] = {}; + bool initialized = false; + bool computeEnabled = false; +}; + +struct ShadowHZBOutput +{ + nvrhi::ITexture* hzb[kCSMCascadeCount] = {}; + framegraph::VirtualResourceHandle handles[kCSMCascadeCount]; + u32 mipLevels[kCSMCascadeCount] = {}; + bool valid = false; +}; + +ShadowHZBOutput setupShadowHZBPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + ShadowPassState& shadowState, + ShadowHZBPassState& state, + framegraph::VirtualResourceHandle shadowMapHandle = {}); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/ShadowMaskPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ShadowMaskPassSetup.cpp new file mode 100644 index 00000000000..7ee73c781ae --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/ShadowMaskPassSetup.cpp @@ -0,0 +1,257 @@ +#include "stdafx.h" +#include "ShadowMaskPassSetup.h" +#include "ShaderConstants.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/ClusteredLightManager.h" +#include "Layers/xrRender/FrameGraphPasses/PassCommon.h" +#include "Layers/xrRender/xrRender_console.h" +#include + +namespace xray::render::fg::passes +{ +using namespace framegraph; + +namespace +{ +struct ShadowMaskCB +{ + u32 maskW, maskH; + u32 fullW, fullH; +}; + +void EnsureMask(nvrhi::IDevice* nv, ShadowMaskPassState& st, u32 w, u32 h) +{ + if (st.mask && st.width == w && st.height == h) + return; + nvrhi::TextureDesc td; + td.width = w; + td.height = h; + td.format = nvrhi::Format::RGBA8_UNORM; + td.isUAV = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "rt_ShadowMask"; + st.mask = nv->createTexture(td); + st.width = st.mask ? w : 0; + st.height = st.mask ? h : 0; + if (st.mask) + { + nvrhi::CommandListHandle cmd = nv->createCommandList(); + cmd->open(); + cmd->clearTextureFloat(st.mask, nvrhi::AllSubresources, nvrhi::Color(1.f, 1.f, 1.f, 1.f)); + cmd->close(); + nv->executeCommandList(cmd); + } +} +} // namespace + +ShadowMaskOutput setupShadowMaskPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle depthInput, + u32 fullWidth, + u32 fullHeight, + const BindlessForwardConfig& bindlessConfig, + const ShadowHZBOutput& hzb, + ShadowMaskPassState& state) +{ + ShadowMaskOutput out{}; + if (!device || ps_r_shadow_mask == 0 || !depthInput.is_valid()) + return out; + + nvrhi::IDevice* nv = device->GetNVRHIDevice(); + if (!nv) + return out; + + if (!state.initialized) + { + auto cs = GEnv.Render->GetShaderLoader()->LoadComputeShader("shadow_mask"); + if (!cs.handle || !cs.reflection) + { + Msg("! [ShadowMask] shadow_mask.cs missing"); + state.initialized = true; + state.computeEnabled = false; + return out; + } + auto& cache = GetPassResourceCache(); + state.layout = cache.GetOrCreateBindingLayoutFromReflection( + "ShadowMaskPass", *cs.reflection, nv); + if (state.layout) + { + nvrhi::ComputePipelineDesc pd; + pd.CS = cs.handle; + pd.bindingLayouts = {state.layout}; + state.pipeline = cache.GetOrCreateComputePipeline("ShadowMaskPass", pd, nv); + } + state.computeEnabled = (state.pipeline != nullptr); + state.initialized = true; + if (state.computeEnabled) + Msg("* [ShadowMask] pipeline ready"); + cs.reflection = nullptr; + } + if (!state.computeEnabled) + return out; + + const u32 maskW = std::max(1u, fullWidth); + const u32 maskH = std::max(1u, fullHeight); + EnsureMask(nv, state, maskW, maskH); + if (!state.mask) + return out; + + { + static bool s_once = false; + if (!s_once) + { + Msg("* [ShadowMask] full-res %ux%u", maskW, maskH); + s_once = true; + } + } + + ResourceDesc desc; + desc.type = ResourceDesc::Type::Texture2D; + desc.width = maskW; + desc.height = maskH; + desc.format = nvrhi::Format::RGBA8_UNORM; + desc.isUAV = true; + desc.isImported = true; + desc.debugName = "rt_ShadowMask"; + auto maskHandle = fg.ImportTexture("rt_ShadowMask", state.mask, desc); + + struct PassData + { + VirtualResourceHandle depth; + VirtualResourceHandle mask; + ShadowMaskPassState* st = nullptr; + BindlessForwardConfig config; + ShadowHZBOutput hzb; + fg::RenderDevice* device = nullptr; + u32 fullW = 0, fullH = 0; + u32 maskW = 0, maskH = 0; + }; + + auto& passData = fg.addCallbackPass( + "ShadowMask", + [&, maskHandle, depthInput, fullWidth, fullHeight, maskW, maskH, bindlessConfig, hzb]( + FrameGraph& builder, PassHandle passHandle, PassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.depth = pb.read(depthInput, ResourceState::ShaderResource); + data.mask = pb.write(maskHandle, ResourceState::UnorderedAccess); + if (hzb.valid) + { + for (u32 i = 0; i < 3; ++i) + { + if (hzb.handles[i].is_valid()) + pb.read(hzb.handles[i], ResourceState::ShaderResource); + } + } + data.st = &state; + data.config = bindlessConfig; + data.hzb = hzb; + data.device = device; + data.fullW = fullWidth; + data.fullH = fullHeight; + data.maskW = maskW; + data.maskH = maskH; + }, + [](const PassData& data, const FrameGraph& graph, fg::RenderContext* ctx) { + if (!data.st || !data.st->pipeline) + return; + nvrhi::ICommandList* cmd = ctx->GetCommandList(); + nvrhi::IDevice* nv = data.device->GetNVRHIDevice(); + auto* depthTex = graph.GetPhysicalTexture(data.depth); + auto* maskTex = graph.GetPhysicalTexture(data.mask); + if (!cmd || !nv || !depthTex || !maskTex) + return; + + auto* refl = GEnv.Render->GetShaderLoader()->GetCachedReflection("shadow_mask", ".cs"); + if (!refl) + return; + + auto& cache = GetPassResourceCache(); + auto* staticGlobalsCB = cache.GetOrCreateVolatileCB( + "ShadowMask", "StaticGlobals", sizeof(StaticGlobals), data.device, 8); + if (!staticGlobalsCB) + return; + StaticGlobals globals = BuildStaticGlobals(); + cmd->writeBuffer(staticGlobalsCB, &globals, sizeof(globals)); + + auto paramsCB = cache.GetOrCreateVolatileCB( + "ShadowMask", "Params", sizeof(ShadowMaskCB), data.device, 8); + if (!paramsCB) + return; + ShadowMaskCB pcb{data.maskW, data.maskH, data.fullW, data.fullH}; + cmd->writeBuffer(paramsCB, &pcb, sizeof(pcb)); + + auto& clm = ClusteredLightManager::Instance(); + nvrhi::ITexture* dummy2D = cache.GetDummyShadowMap2D(nv); + nvrhi::ITexture* dummyHZB = cache.GetDummyContactDepth(nv); + nvrhi::ITexture* contactHist = data.config.contactHistory + ? data.config.contactHistory + : cache.GetDummyContactHistory(nv); + nvrhi::ITexture* localAtlas = data.config.localShadowAtlas + ? data.config.localShadowAtlas + : cache.GetDummyShadowMap(nv); + + BindingSetBuilder bsb(*refl, nv, "ShadowMask"); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.ConstantBuffer("ShadowMaskParams", paramsCB); + BindBindlessMaterialTables(bsb); + bsb.Texture("g_SceneDepth", depthTex); + bsb.TextureUAV("g_OutMask", maskTex); + static const char* kSm[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* sm = data.config.shadowCascades[i] + ? data.config.shadowCascades[i] + : dummy2D; + bsb.Texture(kSm[i], sm); + nvrhi::ITexture* hz = (data.hzb.valid && data.hzb.hzb[i]) + ? data.hzb.hzb[i] + : dummyHZB; + bsb.Texture(kHzb[i], hz); + } + if (contactHist) + bsb.Texture("g_ContactHistory", contactHist); + if (localAtlas) + bsb.Texture("g_LocalShadowAtlas", localAtlas); + nvrhi::ITexture* localEsm = data.config.localShadowESM + ? data.config.localShadowESM + : cache.GetDummyLocalShadowESM(nv); + if (localEsm) + bsb.Texture("g_LocalShadowESM", localEsm); + if (clm.GetLightDataBuffer()) + bsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); + if (clm.GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", clm.GetShadowDataBuffer()); + if (clm.GetClusterGridBuffer()) + bsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); + if (clm.GetLightIndexListBuffer()) + bsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); + + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->layout, nv); + if (!set) + return; + + nvrhi::ComputeState cs; + cs.pipeline = data.st->pipeline; + cs.bindings = {set}; + cmd->setComputeState(cs); + cmd->dispatch((data.maskW + 7) / 8, (data.maskH + 7) / 8, 1); + cmd->setTextureState(maskTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + }); + + out.mask = state.mask; + out.handle = passData.mask; + out.valid = true; + return out; +} + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/ShadowMaskPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ShadowMaskPassSetup.h new file mode 100644 index 00000000000..e26feba597a --- /dev/null +++ b/src/Layers/xrRender/FrameGraphPasses/ShadowMaskPassSetup.h @@ -0,0 +1,50 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include "Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h" +#include "Layers/xrRender/FrameGraphPasses/ShadowHZBPassSetup.h" +#include + +namespace xray::render::framegraph +{ +class FrameGraph; +} + +namespace xray::render::fg +{ +class RenderDevice; +} + +namespace xray::render::fg::passes +{ + +struct ShadowMaskPassState +{ + nvrhi::ComputePipelineHandle pipeline; + nvrhi::BindingLayoutHandle layout; + nvrhi::TextureHandle mask; + u32 width = 0; + u32 height = 0; + bool initialized = false; + bool computeEnabled = false; +}; + +struct ShadowMaskOutput +{ + nvrhi::ITexture* mask = nullptr; + framegraph::VirtualResourceHandle handle; + bool valid = false; +}; + +ShadowMaskOutput setupShadowMaskPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle depthInput, + u32 fullWidth, + u32 fullHeight, + const BindlessForwardConfig& bindlessConfig, + const ShadowHZBOutput& hzb, + ShadowMaskPassState& state); + +} // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.cpp index ea156459ab6..cd66a69e07a 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.cpp @@ -26,6 +26,8 @@ #include "xrEngine/IRenderBackend.h" #include "Layers/xrRender/r__sector.h" #include "Layers/xrRender/GPUCullingManager.h" +#include "Layers/xrRender/Geometry/SkinnedGeometryPools.h" +#include "Layers/xrRender/ShaderVariant/VariantPSOCache.h" extern ENGINE_API float ps_r3_grass_blade_height; extern ENGINE_API float psHUD_FOV; @@ -511,6 +513,98 @@ void InitializeShadowPass(fg::RenderDevice* device, ShadowPassState& state) 44, nvrhi::Format::RGBA32_FLOAT, state.skinned3wVs, state.skinned3wLayout, state.skinned3wInputLayout, state.skinned3wPipeline); + auto initSkinnedMdi = [&](u32 fmt, const char* vsName, const char* dbgName, + const nvrhi::VertexAttributeDesc* baseAttribs, u32 baseAttrCount) { + auto v = loader->LoadVertexShader(vsName, "main"); + if (!v.handle || !v.reflection || !ps.handle || !ps.reflection) + return; + if (!state.skinnedMdiLayout) + { + state.skinnedMdiLayout = cache.GetOrCreateBindingLayoutFromReflection( + "ShadowCascadeSkinnedMDI_v1", *v.reflection, *ps.reflection, nv); + } + if (!state.skinnedMdiLayout) + { + v.reflection = nullptr; + return; + } + nvrhi::VertexAttributeDesc attribs[8]; + for (u32 i = 0; i < baseAttrCount; ++i) + attribs[i] = baseAttribs[i]; + attribs[baseAttrCount] = nvrhi::VertexAttributeDesc() + .setName("DRAWINDEX").setFormat(nvrhi::Format::R32_UINT) + .setBufferIndex(1).setOffset(0).setElementStride(4).setIsInstanced(true); + state.skinnedMdiInputLayout[fmt] = nv->createInputLayout(attribs, baseAttrCount + 1, v.handle); + nvrhi::GraphicsPipelineDesc d; + d.VS = v.handle; + d.PS = state.ps; + d.inputLayout = state.skinnedMdiInputLayout[fmt]; + d.primType = nvrhi::PrimitiveType::TriangleList; + d.bindingLayouts = {state.skinnedMdiLayout}; + if (bindlessLayout) + d.bindingLayouts.push_back(bindlessLayout); + d.renderState.depthStencilState.setDepthTestEnable(true); + d.renderState.depthStencilState.setDepthWriteEnable(true); + d.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + d.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + d.renderState.rasterState.depthBias = 4; + d.renderState.rasterState.slopeScaledDepthBias = 3.0f; + state.skinnedMdiPipeline[fmt] = cache.GetOrCreatePipeline(dbgName, d, fbInfo, nv); + v.reflection = nullptr; + }; + + { + constexpr u32 stride = 24; + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA16_SNORM).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(8).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(12).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG16_SNORM).setOffset(20).setElementStride(stride), + }; + initSkinnedMdi(VF_SKINNED_NONHQ, "shadow\\shadow_cascade_skinned_mdi", + "ShadowCascadeSkinnedMDI_nonHQ_v1", attribs, 5); + } + { + constexpr u32 stride = 36; + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(20).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), + }; + initSkinnedMdi(VF_SKINNED_HQ1W, "shadow\\shadow_cascade_skinned_hq_mdi", + "ShadowCascadeSkinnedMDI_hq1w_v1", attribs, 5); + } + { + constexpr u32 stride = 40; + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(20).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BLENDINDICES").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(36).setElementStride(stride), + }; + initSkinnedMdi(VF_SKINNED_HQ4W, "shadow\\shadow_cascade_skinned_4w_mdi", + "ShadowCascadeSkinnedMDI_hq4w_v1", attribs, 6); + } + { + constexpr u32 stride = 44; + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(20).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), + }; + initSkinnedMdi(VF_SKINNED_HQ2W, "shadow\\shadow_cascade_skinned_2w_mdi", + "ShadowCascadeSkinnedMDI_hq2w_v1", attribs, 5); + initSkinnedMdi(VF_SKINNED_HQ3W, "shadow\\shadow_cascade_skinned_3w_mdi", + "ShadowCascadeSkinnedMDI_hq3w_v1", attribs, 5); + } + if (!state.skinnedPipeline) Msg("! [ShadowPass] Actor skinned CSM pipeline unavailable"); } @@ -620,6 +714,12 @@ void ShutdownShadowPass(fg::RenderDevice* device, ShadowPassState& state) state.skinnedLayout = nullptr; state.skinned4wPipeline = nullptr; state.skinned4wLayout = nullptr; + state.skinnedMdiLayout = nullptr; + for (u32 i = 0; i < 6; ++i) + { + state.skinnedMdiPipeline[i] = nullptr; + state.skinnedMdiInputLayout[i] = nullptr; + } state.inputLayout = nullptr; state.grassInputLayout = nullptr; state.skinnedInputLayout = nullptr; @@ -653,7 +753,8 @@ ShadowCascadeOutputs setupCascadedShadowPass( const xr_vector* hudBatches, const xr_vector* worldSkinnedBatches, fg::GPUCullingManager* gpuCulling, - ShadowPassState& state) + ShadowPassState& state, + framegraph::VirtualResourceHandle skinnedDrawArgs) { using namespace framegraph; @@ -774,12 +875,13 @@ ShadowCascadeOutputs setupCascadedShadowPass( struct PassData { VirtualResourceHandle shadowCascades[kCSMCascadeCount]; + VirtualResourceHandle skinnedDrawArgs; ShadowPassState* passState = nullptr; BindlessForwardConfig bindlessConfig; MaterialCache* materialCache = nullptr; FGDetailManager* detailManager = nullptr; - const xr_vector* hudBatches = nullptr; - const xr_vector* worldSkinnedBatches = nullptr; + const xr_vector* hudBatches = nullptr; + const xr_vector* worldSkinnedBatches = nullptr; GPUCullingManager* gpuCulling = nullptr; fg::RenderDevice* device = nullptr; bool sunOn = false; @@ -789,16 +891,17 @@ ShadowCascadeOutputs setupCascadedShadowPass( }; const bool sunDetails = sunOn && ps_r2_ls_flags.test(R2FLAG_SUN_DETAILS); - // World actor→CSM0 stays off (camera-attached disc). HUD casts into dedicated map. - const bool actorShadow = false; + const bool actorShadow = ps_r__common_flags.test(RFLAG_ACTOR_SHADOW); const bool hudShadow = sunOn && state.hudShadowMap != nullptr; auto& passData = fg.addCallbackPass( "CascadedShadows", - [&, sunOn, sunDetails, actorShadow, hudShadow, detailManager, hudBatches, worldSkinnedBatches, gpuCulling](FrameGraph& builder, PassHandle passHandle, PassData& data) { + [&, sunOn, sunDetails, actorShadow, hudShadow, detailManager, hudBatches, worldSkinnedBatches, gpuCulling, skinnedDrawArgs](FrameGraph& builder, PassHandle passHandle, PassData& data) { RenderPassBuilder pb(builder, passHandle); for (u32 i = 0; i < kCSMCascadeCount; ++i) data.shadowCascades[i] = pb.write(shadowHandles[i], ResourceState::DepthStencilWrite); + if (skinnedDrawArgs.is_valid()) + data.skinnedDrawArgs = pb.read(skinnedDrawArgs, ResourceState::IndirectArgument); data.passState = &state; data.bindlessConfig = bindlessConfig; data.materialCache = materialCache; @@ -830,16 +933,32 @@ ShadowCascadeOutputs setupCascadedShadowPass( ShadowPassState& st = *data.passState; + // Far cascade (c=2): skip clear+redraw every other frame when camera is calm. + static Fvector s_prevCamPos = {0, 0, 0}; + static Fvector s_prevCamDir = {0, 0, 1}; + static bool s_farCascadeValid = false; + const float camMove = Device.vCameraPosition.distance_to(s_prevCamPos); + const float camTurn = 1.f - _abs(Device.vCameraDirection.dotproduct(s_prevCamDir)); + const bool cameraCalm = camMove < 0.35f && camTurn < 0.02f; + const bool skipFarCascade = s_farCascadeValid && cameraCalm && ((Device.dwFrame % 3u) != 0); + s_prevCamPos = Device.vCameraPosition; + s_prevCamDir = Device.vCameraDirection; + for (u32 c = 0; c < kCSMCascadeCount; ++c) { if (!shadowTex[c]) continue; + if (c == 2 && skipFarCascade) + continue; cmd->clearDepthStencilTexture( shadowTex[c], nvrhi::TextureSubresourceSet(0, 1, 0, 1), true, 1.0f, false, 0); } if (!data.sunOn) + { + s_farCascadeValid = false; return; + } if (data.materialCache) data.materialCache->FinalizePendingMaterials(ctx); @@ -948,7 +1067,8 @@ ShadowCascadeOutputs setupCascadedShadowPass( bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); bsb.ConstantBuffer("SkinnedMaterialCB", matCB); bsb.BufferSRV("g_BoneMatrices", boneBuf); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + BindBindlessMaterialTables(bsb); + BindPaintSplatBuffer(bsb, nv); auto set = cache.GetOrCreateBindingSet(bsb.Build(), layout, nv); if (!set) continue; @@ -974,8 +1094,6 @@ ShadowCascadeOutputs setupCascadedShadowPass( } } - // World actor→CSM0 remains disabled (camera-attached disc). - if (!data.bindlessConfig.UseGPUCulling() || !data.bindlessConfig.UseMegaBuffers()) return; @@ -1036,6 +1154,10 @@ ShadowCascadeOutputs setupCascadedShadowPass( if (!shadowTex[c]) continue; + // Far cascade cadence: reuse previous contents when camera is calm + if (c == 2 && skipFarCascade) + continue; + const u32 cres = st.cascadeResolution[c] ? st.cascadeResolution[c] : GetCSMCascadeResolution(c); nvrhi::Viewport cascadeVp(0.f, float(cres), 0.f, float(cres), 0.f, 1.f); @@ -1108,7 +1230,7 @@ ShadowCascadeOutputs setupCascadedShadowPass( framegraph::BindingSetBuilder bsb(*vsRefl, *psRefl, nv, "ShadowCascade_v2"); bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + BindBindlessMaterialTables(bsb); bsb.BufferSRV("g_InstanceData", set.instanceBuffer); bsb.BufferSRV("g_CompactBatchIndices", batchIndices); bsb.BufferSRV("g_CompactMaterialIDs", materialIDs); @@ -1152,11 +1274,50 @@ ShadowCascadeOutputs setupCascadedShadowPass( } drawSet(data.bindlessConfig.staticSet, st.pipeline); - drawSet(data.bindlessConfig.dynamicSet, st.pipeline); - // Trees/bushes (alpha-blended → transparent set) cast as alpha-tested foliage — near+mid only - if (c < 2) + const bool shaftsNeedFarCasters = ps_r_sun_shafts > 0; + if (c < 2 || shaftsNeedFarCasters) + drawSet(data.bindlessConfig.dynamicSet, st.pipeline); + if (c < 2 || shaftsNeedFarCasters) drawSet(data.bindlessConfig.transparentCasterSet, st.foliagePipeline); + if (data.gpuCulling && data.gpuCulling->GetTessObjectCount() > 0) + { + nvrhi::IBuffer* tessInst = data.gpuCulling->GetTessInstanceBuffer(); + nvrhi::IBuffer* tessMat = data.gpuCulling->GetTessMaterialIDBuffer(); + nvrhi::IBuffer* tessIdx = data.gpuCulling->GetTessBatchIndicesBuffer(); + nvrhi::IBuffer* tessArgs = data.gpuCulling->GetTessDrawArgsBuffer(); + const u32 tessCount = data.gpuCulling->GetTessObjectCount(); + if (tessInst && tessMat && tessIdx && tessArgs && tessCount > 0) + { + framegraph::BindingSetBuilder tsb(*vsRefl, *psRefl, nv, "ShadowCascade.Tess"); + tsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + BindBindlessMaterialTables(tsb); + tsb.BufferSRV("g_InstanceData", tessInst); + tsb.BufferSRV("g_CompactBatchIndices", tessIdx); + tsb.BufferSRV("g_CompactMaterialIDs", tessMat); + auto tessSet = cache.GetOrCreateBindingSet(tsb.Build(), st.layout, nv); + if (tessSet) + { + nvrhi::GraphicsState gs; + gs.pipeline = st.pipeline; + gs.framebuffer = fb; + gs.bindings = {tessSet}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = { + {data.bindlessConfig.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = { + data.bindlessConfig.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + gs.viewport.addViewport(cascadeVp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.indirectParams = tessArgs; + cmd->setGraphicsState(gs); + cmd->drawIndexedIndirect(0, tessCount); + } + } + } + // Terrain cast into near+mid (far cascade too coarse / expensive) if (st.terrainPipeline && st.terrainLayout && data.bindlessConfig.HasTerrain() && @@ -1174,6 +1335,7 @@ ShadowCascadeOutputs setupCascadedShadowPass( { framegraph::BindingSetBuilder tbsb(*vsRefl, *terrainPsRefl, nv, "ShadowCascade.Terrain"); tbsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + BindBindlessMaterialTables(tbsb); tbsb.BufferSRV("g_InstanceData", data.bindlessConfig.terrainInstanceBuffer); tbsb.BufferSRV("g_CompactBatchIndices", data.bindlessConfig.terrainCompactBatchIndicesBuffer); tbsb.BufferSRV("g_CompactMaterialIDs", data.bindlessConfig.terrainCompactMaterialIDBuffer); @@ -1223,6 +1385,8 @@ ShadowCascadeOutputs setupCascadedShadowPass( *bbVsRefl, *bbPsRefl, nv, "ShadowCascade.BillboardGrass"); gsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); gsb.ConstantBuffer("GrassShadowCB", st.grassCB); + // detail_billboard_shadow.ps includes bindless_common.h → t8/t9/t10 + BindBindlessMaterialTables(gsb); gsb.BufferSRV("visible_indices", dm->visibleBillboardInstancesBuffer); gsb.BufferSRV("detail_models", dm->detailModelsBuffer); gsb.BufferSRV("pulled_vertices", dm->pulledVertexBuffer); @@ -1285,90 +1449,155 @@ ShadowCascadeOutputs setupCascadedShadowPass( } } - // World skinned: near + mid only (far cascade barely resolves NPC scale) - if (worldSkinnedReady && skDynCB && skMatCB && c < 1) + if (worldSkinnedReady && c < 2) { - for (const auto& batch : *data.worldSkinnedBatches) + const bool mdiActive = data.gpuCulling->IsSkinnedMDIEnabled() && + st.skinnedMdiLayout && skBoneBuf; + auto* mdiVsRefl = mdiActive + ? shaderLoader->GetCachedReflection("shadow\\shadow_cascade_skinned_mdi", ".vs") + : nullptr; + auto drawIndexBuffer = mdiActive + ? GetOrCreateDrawIndexBuffer("ShadowCascade_SkinnedMDI", nv) + : nullptr; + + if (mdiActive && mdiVsRefl && drawIndexBuffer) + { + auto& pools = data.gpuCulling->GetSkinnedPools(); + for (u32 f = SkinnedGeometryPools::FIRST_FORMAT; f < SkinnedGeometryPools::FORMAT_COUNT; ++f) + { + const auto& bucket = data.gpuCulling->GetSkinnedBucket(f); + if (bucket.count == 0 || !st.skinnedMdiPipeline[f]) + continue; + nvrhi::IBuffer* poolVB = pools.GetVertexBuffer(f); + nvrhi::IBuffer* poolIB = pools.GetIndexBuffer(f); + if (!poolVB || !poolIB || !bucket.compactDrawArgsBuffer || + !bucket.compactCountBuffer || !bucket.recordsBuffer || + !bucket.compactBatchIndicesBuffer || !bucket.compactMaterialIDBuffer) + continue; + + framegraph::BindingSetBuilder bsb(*mdiVsRefl, *skPsRefl, nv, "ShadowCascade.WorldSkinMDI"); + bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + bsb.BufferSRV("g_BoneMatrices", skBoneBuf); + BindBindlessMaterialTables(bsb); + BindPaintSplatBuffer(bsb, nv); + bsb.BufferSRV("g_SkinnedRecords", bucket.recordsBuffer); + bsb.BufferSRV("g_SkinnedCompactIndices", bucket.compactBatchIndicesBuffer); + bsb.BufferSRV("g_SkinnedCompactMaterialIDs", bucket.compactMaterialIDBuffer); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), st.skinnedMdiLayout, nv); + if (!set) + continue; + + nvrhi::GraphicsState gs; + gs.pipeline = st.skinnedMdiPipeline[f]; + gs.framebuffer = fb; + gs.bindings = {set}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = {{poolVB, 0, 0}, {drawIndexBuffer, 1, 0}}; + gs.indexBuffer = {poolIB, nvrhi::Format::R16_UINT, 0}; + gs.viewport.addViewport(cascadeVp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + gs.indirectParams = bucket.compactDrawArgsBuffer; + gs.indirectCountBuffer = bucket.compactCountBuffer; + cmd->setGraphicsState(gs); + DrawIndexedIndirectCountOrFallback(cmd, 0, 0, bucket.count); + } + } + + if (skDynCB && skMatCB) { - if (!batch.isSkinned || !batch.vertexBuffer || !batch.indexBuffer) - continue; - - CKinematics* parent = nullptr; - const u32 visualType = batch.visual ? batch.visual->getType() : 0; - if (visualType == MT_SKELETON_GEOMDEF_ST) - parent = static_cast(batch.visual)->GetParent(); - else if (visualType == MT_SKELETON_GEOMDEF_PM) - parent = static_cast(batch.visual)->GetParent(); - if (!parent) - continue; - - // Match the shadow pipeline to the vertex format, mirroring - // SkinningPass::SelectSkinnedPipelineFromVariants. RM_* from - // CSkeletonX: 1 SINGLE,2 SINGLE_HQ,3 1B,4 1B_HQ,5 2B,6 2B_HQ, - // 7 3B,8 3B_HQ,9 4B,10 4B_HQ. - const u16 rm = batch.skinningRenderMode; - const u32 stride = batch.vertexStride; - nvrhi::IGraphicsPipeline* pipe = nullptr; - nvrhi::IBindingLayout* layout = nullptr; - nvrhi::IInputLayout* il = nullptr; - auto* vsRefl = skVsReflNonHQ; - auto pick3w = [&] { pipe = st.skinned3wPipeline.Get(); layout = st.skinned3wLayout.Get(); il = st.skinned3wInputLayout.Get(); vsRefl = skVsRefl3W; }; - auto pick2w = [&] { pipe = st.skinned2wPipeline.Get(); layout = st.skinned2wLayout.Get(); il = st.skinned2wInputLayout.Get(); vsRefl = skVsRefl2W; }; - auto pick4w = [&] { pipe = st.skinned4wPipeline.Get(); layout = st.skinned4wLayout.Get(); il = st.skinned4wInputLayout.Get(); vsRefl = skVsRefl4W; }; - auto pickHq = [&] { pipe = st.skinnedHqPipeline.Get(); layout = st.skinnedHqLayout.Get(); il = st.skinnedHqInputLayout.Get(); vsRefl = skVsReflHQ; }; - auto pickNonHq = [&] { pipe = st.skinnedPipeline.Get(); layout = st.skinnedLayout.Get(); il = st.skinnedInputLayout.Get(); vsRefl = skVsReflNonHQ; }; - if (rm == 7 || rm == 8) pick3w(); - else if (rm == 5 || rm == 6) pick2w(); - else if (rm == 9 || rm == 10) pick4w(); - else if (rm == 4 || rm == 2) pickHq(); - else if (rm == 3 || rm == 1) pickNonHq(); - else if (stride == 36) pickHq(); - else if (stride == 40) pick4w(); - else if (stride == 44) pick2w(); - else pickNonHq(); - - if (!pipe || !layout || !il || !vsRefl) - continue; - - const u32 boneOffset = data.gpuCulling->GetOrUploadSkeleton(cmd, parent); - - DynamicTransforms dyn{}; - FillDynamicTransforms(dyn, batch.worldMatrix); - cmd->writeBuffer(skDynCB, &dyn, sizeof(dyn)); - - SkinnedMaterialCB matId{}; - matId.materialID = batch.bindlessMaterialID; - matId.skeletonBoneOffset = boneOffset; - cmd->writeBuffer(skMatCB, &matId, sizeof(matId)); - - framegraph::BindingSetBuilder bsb(*vsRefl, *skPsRefl, nv, "ShadowCascade.WorldSkin"); - bsb.ConstantBuffer("dynamic_transforms", skDynCB); - bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); - bsb.ConstantBuffer("SkinnedMaterialCB", skMatCB); - bsb.BufferSRV("g_BoneMatrices", skBoneBuf); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); - auto set = cache.GetOrCreateBindingSet(bsb.Build(), layout, nv); - if (!set) - continue; - - nvrhi::GraphicsState gs; - gs.pipeline = pipe; - gs.framebuffer = fb; - gs.bindings = {set}; - if (bindlessTable) - gs.addBindingSet(bindlessTable); - gs.vertexBuffers = {{batch.vertexBuffer, 0, 0}}; - gs.indexBuffer = {batch.indexBuffer, nvrhi::Format::R16_UINT, 0}; - gs.viewport.addViewport(cascadeVp); - gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); - cmd->setGraphicsState(gs); - cmd->drawIndexed( - nvrhi::DrawArguments() - .setVertexCount(batch.indexCount) - .setStartIndexLocation(batch.startIndex) - .setStartVertexLocation(batch.baseVertex)); + for (const auto& batch : *data.worldSkinnedBatches) + { + if (!batch.isSkinned || !batch.vertexBuffer || !batch.indexBuffer) + continue; + + if (mdiActive) + { + const u32 variantIdx = bindless::MaterialBuffer::Instance().GetShaderVariant(batch.bindlessMaterialID); + const bool pooled = variantIdx == 0 + && batch.skinnedPoolFormat >= SkinnedGeometryPools::FIRST_FORMAT + && batch.skinnedPoolFormat < SkinnedGeometryPools::FORMAT_COUNT; + if (pooled) + continue; + } + + CKinematics* parent = nullptr; + const u32 visualType = batch.visual ? batch.visual->getType() : 0; + if (visualType == MT_SKELETON_GEOMDEF_ST) + parent = static_cast(batch.visual)->GetParent(); + else if (visualType == MT_SKELETON_GEOMDEF_PM) + parent = static_cast(batch.visual)->GetParent(); + if (!parent) + continue; + + const u16 rm = batch.skinningRenderMode; + const u32 stride = batch.vertexStride; + nvrhi::IGraphicsPipeline* pipe = nullptr; + nvrhi::IBindingLayout* layout = nullptr; + nvrhi::IInputLayout* il = nullptr; + auto* vsRefl = skVsReflNonHQ; + auto pick3w = [&] { pipe = st.skinned3wPipeline.Get(); layout = st.skinned3wLayout.Get(); il = st.skinned3wInputLayout.Get(); vsRefl = skVsRefl3W; }; + auto pick2w = [&] { pipe = st.skinned2wPipeline.Get(); layout = st.skinned2wLayout.Get(); il = st.skinned2wInputLayout.Get(); vsRefl = skVsRefl2W; }; + auto pick4w = [&] { pipe = st.skinned4wPipeline.Get(); layout = st.skinned4wLayout.Get(); il = st.skinned4wInputLayout.Get(); vsRefl = skVsRefl4W; }; + auto pickHq = [&] { pipe = st.skinnedHqPipeline.Get(); layout = st.skinnedHqLayout.Get(); il = st.skinnedHqInputLayout.Get(); vsRefl = skVsReflHQ; }; + auto pickNonHq = [&] { pipe = st.skinnedPipeline.Get(); layout = st.skinnedLayout.Get(); il = st.skinnedInputLayout.Get(); vsRefl = skVsReflNonHQ; }; + if (rm == 7 || rm == 8) pick3w(); + else if (rm == 5 || rm == 6) pick2w(); + else if (rm == 9 || rm == 10) pick4w(); + else if (rm == 4 || rm == 2) pickHq(); + else if (rm == 3 || rm == 1) pickNonHq(); + else if (stride == 36) pickHq(); + else if (stride == 40) pick4w(); + else if (stride == 44) pick2w(); + else pickNonHq(); + + if (!pipe || !layout || !il || !vsRefl) + continue; + + const u32 boneOffset = data.gpuCulling->GetOrUploadSkeleton(cmd, parent); + + DynamicTransforms dyn{}; + FillDynamicTransforms(dyn, batch.worldMatrix); + cmd->writeBuffer(skDynCB, &dyn, sizeof(dyn)); + + SkinnedMaterialCB matId{}; + matId.materialID = batch.bindlessMaterialID; + matId.skeletonBoneOffset = boneOffset; + cmd->writeBuffer(skMatCB, &matId, sizeof(matId)); + + framegraph::BindingSetBuilder bsb(*vsRefl, *skPsRefl, nv, "ShadowCascade.WorldSkin"); + bsb.ConstantBuffer("dynamic_transforms", skDynCB); + bsb.ConstantBuffer("ShadowCascadeCB", st.cascadeCB); + bsb.ConstantBuffer("SkinnedMaterialCB", skMatCB); + bsb.BufferSRV("g_BoneMatrices", skBoneBuf); + BindBindlessMaterialTables(bsb); + BindPaintSplatBuffer(bsb, nv); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), layout, nv); + if (!set) + continue; + + nvrhi::GraphicsState gs; + gs.pipeline = pipe; + gs.framebuffer = fb; + gs.bindings = {set}; + if (bindlessTable) + gs.addBindingSet(bindlessTable); + gs.vertexBuffers = {{batch.vertexBuffer, 0, 0}}; + gs.indexBuffer = {batch.indexBuffer, nvrhi::Format::R16_UINT, 0}; + gs.viewport.addViewport(cascadeVp); + gs.viewport.addScissorRect(nvrhi::Rect(cres, cres)); + cmd->setGraphicsState(gs); + cmd->drawIndexed( + nvrhi::DrawArguments() + .setVertexCount(batch.indexCount) + .setStartIndexLocation(batch.startIndex) + .setStartVertexLocation(batch.baseVertex)); + } } } + + if (c == 2) + s_farCascadeValid = true; } }); diff --git a/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.h index ac1df9c34cc..786e93f5d9d 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.h @@ -74,6 +74,9 @@ struct ShadowPassState nvrhi::BindingLayoutHandle skinned2wLayout; nvrhi::GraphicsPipelineHandle skinned3wPipeline; // 3-bone HQ (44B) nvrhi::BindingLayoutHandle skinned3wLayout; + nvrhi::GraphicsPipelineHandle skinnedMdiPipeline[6]; + nvrhi::BindingLayoutHandle skinnedMdiLayout; + nvrhi::InputLayoutHandle skinnedMdiInputLayout[6]; nvrhi::InputLayoutHandle inputLayout; nvrhi::InputLayoutHandle grassInputLayout; nvrhi::InputLayoutHandle skinnedInputLayout; @@ -134,7 +137,8 @@ ShadowCascadeOutputs setupCascadedShadowPass( const xr_vector* hudBatches, const xr_vector* worldSkinnedBatches, fg::GPUCullingManager* gpuCulling, - ShadowPassState& state); + ShadowPassState& state, + framegraph::VirtualResourceHandle skinnedDrawArgs = {}); void InitializeShadowPass(fg::RenderDevice* device, ShadowPassState& state); void ShutdownShadowPass(fg::RenderDevice* device, ShadowPassState& state); diff --git a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp index 00fdef67b80..1b0cb19a12a 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp @@ -3,6 +3,7 @@ // Uses GPU-driven global bone buffer for efficient skinning #include "stdafx.h" #include "SkinningPassSetup.h" +#include "IBLPrefilterPassSetup.h" #include "ShaderConstants.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/IPass.h" @@ -28,6 +29,8 @@ #include "PassCommon.h" #include "Layers/xrRender/ClusteredLightManager.h" #include "xrCore/FMesh.hpp" +#include "xrEngine/IGame_Persistent.h" +#include "xrEngine/ShadersExternalData.h" extern ENGINE_API float psHUD_FOV; @@ -90,16 +93,25 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf state.ps = skinnedPsResult.handle; auto skinnedVsForReflection = shaderLoader->LoadVertexShader("bindless_skinned", "main"); - state.layout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_IBL_CSMLadder", *skinnedVsForReflection.reflection, *skinnedPsResult.reflection, nvDevice); + state.layout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_IBL_CSMLadder_NoMask", *skinnedVsForReflection.reflection, *skinnedPsResult.reflection, nvDevice); auto hudPsResult = shaderLoader->LoadPixelShader("bindless_skinned_hud", "main"); if (hudPsResult.handle) { state.hudPS = hudPsResult.handle; - state.hudLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_HUD_IBL_CSMLadder_HUDShadow", *skinnedVsForReflection.reflection, *hudPsResult.reflection, nvDevice); + state.hudLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_HUD_IBL_NoCSM_v3", *skinnedVsForReflection.reflection, *hudPsResult.reflection, nvDevice); } if (!state.hudLayout) state.hudLayout = state.layout; + auto scopePsResult = shaderLoader->LoadPixelShader("bindless_skinned_scope", "main"); + if (scopePsResult.handle) { + state.hudScopePS = scopePsResult.handle; + state.hudScopeLayout = cache.GetOrCreateBindingLayoutFromReflection( + "SkinningPass_HUDScope", *skinnedVsForReflection.reflection, *scopePsResult.reflection, nvDevice); + } + if (!state.hudScopeLayout) + state.hudScopeLayout = state.hudLayout; + auto buildPipelineDesc = [&](nvrhi::IShader* vs, nvrhi::IInputLayout* il, nvrhi::IShader* psOverride = nullptr) { nvrhi::GraphicsPipelineDesc pipeDesc; pipeDesc.VS = vs; @@ -138,14 +150,35 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf hudVariant.inputLayout = worldVariant.inputLayout; auto pipeDesc = buildPipelineDesc(worldVariant.vs, worldVariant.inputLayout, state.hudPS); pipeDesc.bindingLayouts[0] = state.hudLayout; + for (u32 rt = 0; rt < 4; ++rt) + { + pipeDesc.renderState.blendState.targets[rt].setBlendEnable(false); + pipeDesc.renderState.blendState.targets[rt].setColorWriteMask( + nvrhi::ColorMask::Red | nvrhi::ColorMask::Green | + nvrhi::ColorMask::Blue | nvrhi::ColorMask::Alpha); + } hudVariant.pipeline = cache.GetOrCreatePipeline(cacheName, pipeDesc, fbInfo, nvDevice); }; + auto initHudScopeVariant = [&](SkinningPipelineVariant& scopeVariant, const SkinningPipelineVariant& worldVariant, const char* cacheName) { + if (!worldVariant.pipeline || !state.hudScopePS || !state.hudScopeLayout) + return; + scopeVariant.vs = worldVariant.vs; + scopeVariant.inputLayout = worldVariant.inputLayout; + auto pipeDesc = buildPipelineDesc(worldVariant.vs, worldVariant.inputLayout, state.hudScopePS); + pipeDesc.bindingLayouts[0] = state.hudScopeLayout; + pipeDesc.renderState.blendState.targets[0].setBlendEnable(true); + pipeDesc.renderState.blendState.targets[0].setSrcBlend(nvrhi::BlendFactor::SrcAlpha); + pipeDesc.renderState.blendState.targets[0].setDestBlend(nvrhi::BlendFactor::OneMinusSrcAlpha); + pipeDesc.renderState.depthStencilState.depthWriteEnable = false; + scopeVariant.pipeline = cache.GetOrCreatePipeline(cacheName, pipeDesc, fbInfo, nvDevice); + }; + auto mdiPsResult = shaderLoader->LoadPixelShader("bindless_skinned_mdi", "main"); auto mdiVsForReflection = shaderLoader->LoadVertexShader("bindless_skinned_mdi", "main"); if (mdiPsResult.handle && mdiVsForReflection.handle) { state.mdiPS = mdiPsResult.handle; - state.mdiLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_MDI_IBL_CSMLadder", + state.mdiLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_MDI_IBL_CSMLadder_NoMask", *mdiVsForReflection.reflection, *mdiPsResult.reflection, nvDevice); } @@ -180,8 +213,8 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG16_SNORM).setOffset(20).setElementStride(stride), }; - initVariant(state.nonHQ, "bindless_skinned", "SkinningPass_nonHQ_IBL_CSMLadder", attribs, 5); - initMDIVariant(state.mdiNonHQ, "bindless_skinned_mdi", "SkinningPass_mdi_nonHQ_IBL_CSMLadder", attribs, 5); + initVariant(state.nonHQ, "bindless_skinned", "SkinningPass_nonHQ_IBL_CSMLadder_v2", attribs, 5); + initMDIVariant(state.mdiNonHQ, "bindless_skinned_mdi", "SkinningPass_mdi_nonHQ_IBL_CSMLadder_v2", attribs, 5); } { @@ -193,8 +226,8 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), }; - initVariant(state.hq1w, "bindless_skinned_hq", "SkinningPass_hq1w_IBL_CSMLadder", attribs, 5); - initMDIVariant(state.mdiHQ1w, "bindless_skinned_hq_mdi", "SkinningPass_mdi_hq1w_IBL_CSMLadder", attribs, 5); + initVariant(state.hq1w, "bindless_skinned_hq", "SkinningPass_hq1w_IBL_CSMLadder_v2", attribs, 5); + initMDIVariant(state.mdiHQ1w, "bindless_skinned_hq_mdi", "SkinningPass_mdi_hq1w_IBL_CSMLadder_v2", attribs, 5); } { @@ -207,8 +240,8 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("BLENDINDICES").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(36).setElementStride(stride), }; - initVariant(state.hq4w, "bindless_skinned_4w", "SkinningPass_hq4w_IBL_CSMLadder", attribs, 6); - initMDIVariant(state.mdiHQ4w, "bindless_skinned_4w_mdi", "SkinningPass_mdi_hq4w_IBL_CSMLadder", attribs, 6); + initVariant(state.hq4w, "bindless_skinned_4w", "SkinningPass_hq4w_IBL_CSMLadder_v2", attribs, 6); + initMDIVariant(state.mdiHQ4w, "bindless_skinned_4w_mdi", "SkinningPass_mdi_hq4w_IBL_CSMLadder_v2", attribs, 6); } { @@ -220,8 +253,8 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), }; - initVariant(state.hq2w, "bindless_skinned_2w", "SkinningPass_hq2w_IBL_CSMLadder", attribs, 5); - initMDIVariant(state.mdiHQ2w, "bindless_skinned_2w_mdi", "SkinningPass_mdi_hq2w_IBL_CSMLadder", attribs, 5); + initVariant(state.hq2w, "bindless_skinned_2w", "SkinningPass_hq2w_IBL_CSMLadder_v2", attribs, 5); + initMDIVariant(state.mdiHQ2w, "bindless_skinned_2w_mdi", "SkinningPass_mdi_hq2w_IBL_CSMLadder_v2", attribs, 5); } { @@ -233,15 +266,21 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), }; - initVariant(state.hq3w, "bindless_skinned_3w", "SkinningPass_hq3w_IBL_CSMLadder", attribs, 5); - initMDIVariant(state.mdiHQ3w, "bindless_skinned_3w_mdi", "SkinningPass_mdi_hq3w_IBL_CSMLadder", attribs, 5); + initVariant(state.hq3w, "bindless_skinned_3w", "SkinningPass_hq3w_IBL_CSMLadder_v2", attribs, 5); + initMDIVariant(state.mdiHQ3w, "bindless_skinned_3w_mdi", "SkinningPass_mdi_hq3w_IBL_CSMLadder_v2", attribs, 5); } - initHudVariant(state.hudNonHQ, state.nonHQ, "SkinningPass_hud_nonHQ_IBL_CSMLadder"); - initHudVariant(state.hudHQ1w, state.hq1w, "SkinningPass_hud_hq1w_IBL_CSMLadder"); - initHudVariant(state.hudHQ2w, state.hq2w, "SkinningPass_hud_hq2w_IBL_CSMLadder"); - initHudVariant(state.hudHQ3w, state.hq3w, "SkinningPass_hud_hq3w_IBL_CSMLadder"); - initHudVariant(state.hudHQ4w, state.hq4w, "SkinningPass_hud_hq4w_IBL_CSMLadder"); + initHudVariant(state.hudNonHQ, state.nonHQ, "SkinningPass_hud_nonHQ_Opaque_v3"); + initHudVariant(state.hudHQ1w, state.hq1w, "SkinningPass_hud_hq1w_Opaque_v3"); + initHudVariant(state.hudHQ2w, state.hq2w, "SkinningPass_hud_hq2w_Opaque_v3"); + initHudVariant(state.hudHQ3w, state.hq3w, "SkinningPass_hud_hq3w_Opaque_v3"); + initHudVariant(state.hudHQ4w, state.hq4w, "SkinningPass_hud_hq4w_Opaque_v3"); + + initHudScopeVariant(state.hudScopeNonHQ, state.nonHQ, "SkinningPass_hudScope_nonHQ"); + initHudScopeVariant(state.hudScopeHQ1w, state.hq1w, "SkinningPass_hudScope_hq1w"); + initHudScopeVariant(state.hudScopeHQ2w, state.hq2w, "SkinningPass_hudScope_hq2w"); + initHudScopeVariant(state.hudScopeHQ3w, state.hq3w, "SkinningPass_hudScope_hq3w"); + initHudScopeVariant(state.hudScopeHQ4w, state.hq4w, "SkinningPass_hudScope_hq4w"); // Skinned PatchList tessellation is NOT created on Apple/MoltenVK: Metal's // tessellation temp buffers have caused IOGPU kernel panics @@ -292,6 +331,57 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf Msg("* [SkinningPass] Skinned tessellation disabled on Apple/MoltenVK (IOGPU safety)"); #endif + { + auto depthPsResult = shaderLoader->LoadPixelShader("bindless_skinned_depth", "main"); + if (depthPsResult.handle && skinnedVsForReflection.handle) + { + state.depthPS = depthPsResult.handle; + state.depthLayout = cache.GetOrCreateBindingLayoutFromReflection( + "SkinningDepthPass", *skinnedVsForReflection.reflection, *depthPsResult.reflection, nvDevice); + + nvrhi::FramebufferInfoEx depthFb; + depthFb.depthFormat = nvrhi::Format::D32; + + auto initDepthVariant = [&](SkinningPipelineVariant& depthVar, + const SkinningPipelineVariant& src, + const char* cacheName) { + if (!state.depthPS || !state.depthLayout || !src.vs || !src.inputLayout) + return; + depthVar.vs = src.vs; + depthVar.inputLayout = src.inputLayout; + nvrhi::GraphicsPipelineDesc pipeDesc; + pipeDesc.VS = src.vs; + pipeDesc.PS = state.depthPS; + pipeDesc.inputLayout = src.inputLayout; + if (bindlessLayout) + pipeDesc.bindingLayouts = {state.depthLayout, bindlessLayout}; + else + pipeDesc.bindingLayouts = {state.depthLayout}; + pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; + pipeDesc.renderState.depthStencilState.depthTestEnable = true; + pipeDesc.renderState.depthStencilState.depthWriteEnable = true; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + pipeDesc.renderState.rasterState.frontCounterClockwise = false; + pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; + depthVar.pipeline = cache.GetOrCreatePipeline(cacheName, pipeDesc, depthFb, nvDevice); + }; + + initDepthVariant(state.depthNonHQ, state.nonHQ, "SkinningDepth_nonHQ_v1"); + initDepthVariant(state.depthHQ1w, state.hq1w, "SkinningDepth_hq1w_v1"); + initDepthVariant(state.depthHQ2w, state.hq2w, "SkinningDepth_hq2w_v1"); + initDepthVariant(state.depthHQ3w, state.hq3w, "SkinningDepth_hq3w_v1"); + initDepthVariant(state.depthHQ4w, state.hq4w, "SkinningDepth_hq4w_v1"); + if (state.depthNonHQ.pipeline || state.depthHQ1w.pipeline || state.depthHQ4w.pipeline) + Msg("* [SkinningPass] Skinned depth pipelines ready"); + else + Msg("! [SkinningPass] Skinned depth PSO create failed"); + } + else + { + Msg("! [SkinningPass] bindless_skinned_depth missing — skinned depth disabled"); + } + } + state.initialized = true; Msg("* [SkinningPass] Pipeline initialization complete"); } @@ -349,6 +439,13 @@ static nvrhi::IGraphicsPipeline* SelectSkinnedPipeline(const SkinningPassState& return SelectSkinnedPipelineFromVariants(state.nonHQ, state.hq1w, state.hq2w, state.hq3w, state.hq4w, vertexStride, renderMode); } +static nvrhi::IGraphicsPipeline* SelectSkinnedDepthPipeline(const SkinningPassState& state, u32 vertexStride, u16 renderMode) +{ + return SelectSkinnedPipelineFromVariants( + state.depthNonHQ, state.depthHQ1w, state.depthHQ2w, state.depthHQ3w, state.depthHQ4w, + vertexStride, renderMode); +} + static nvrhi::IGraphicsPipeline* SelectSkinnedTessPipeline(const SkinningPassState& state, u32 vertexStride, u16 renderMode) { return SelectSkinnedPipelineFromVariants(state.tessNonHQ, state.tessHQ1w, state.tessHQ2w, state.tessHQ3w, state.tessHQ4w, vertexStride, renderMode); @@ -360,6 +457,22 @@ static nvrhi::IGraphicsPipeline* SelectHUDSkinnedPipeline(const SkinningPassStat return hudPipe ? hudPipe : SelectSkinnedPipeline(state, vertexStride, renderMode); } +static nvrhi::IGraphicsPipeline* SelectHUDScopePipeline(const SkinningPassState& state, u32 vertexStride, u16 renderMode) +{ + auto* pipe = SelectSkinnedPipelineFromVariants( + state.hudScopeNonHQ, state.hudScopeHQ1w, state.hudScopeHQ2w, state.hudScopeHQ3w, state.hudScopeHQ4w, + vertexStride, renderMode); + return pipe ? pipe : SelectHUDSkinnedPipeline(state, vertexStride, renderMode); +} + +static bool IsScopeBatch(const GeometryBatch& batch) +{ + if (!batch.visual || !batch.visual->shaderName.size()) + return false; + const char* sh = batch.visual->shaderName.c_str(); + return strstr(sh, "scope") != nullptr || strstr(sh, "lense") != nullptr || strstr(sh, "lens") != nullptr; +} + static u32 GetSkinnedVertexFormatID(u16 renderMode, u32 vertexStride) { if (renderMode == RM_SKINNING_3B || renderMode == RM_SKINNING_3B_HQ) return VF_SKINNED_HQ3W; @@ -464,7 +577,10 @@ static void BindSkinnedLightingTextures( nvrhi::ITexture* envSky0, nvrhi::ITexture* envSky1, bool isHUD, - nvrhi::ITexture* localShadowAtlas = nullptr) + nvrhi::ITexture* localShadowAtlas = nullptr, + nvrhi::ITexture* const* shadowHZB = nullptr, + nvrhi::ITexture* shadowMask = nullptr, + nvrhi::ITexture* localShadowESM = nullptr) { auto& cache = framegraph::GetPassResourceCache(); nvrhi::ITexture* dummy2D = cache.GetDummyShadowMap2D(nvDevice); @@ -480,21 +596,34 @@ static void BindSkinnedLightingTextures( { nvrhi::ITexture* hudTex = hudShadowMap ? hudShadowMap : dummyArray; if (hudTex) - bsb.TextureSlot(24, hudTex); + bsb.Texture("g_HUDShadowMap", hudTex); } - (void)contactDepth; // history-only contact; depth no longer in forward layouts + (void)contactDepth; nvrhi::ITexture* hist = contactHistory ? contactHistory : cache.GetDummyContactHistory(nvDevice); if (hist) bsb.Texture("g_ContactHistory", hist); nvrhi::ITexture* localAtlas = localShadowAtlas ? localShadowAtlas : dummyArray; if (localAtlas) bsb.Texture("g_LocalShadowAtlas", localAtlas); + nvrhi::ITexture* localEsm = localShadowESM ? localShadowESM : cache.GetDummyLocalShadowESM(nvDevice); + if (localEsm) + bsb.Texture("g_LocalShadowESM", localEsm); + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + nvrhi::ITexture* dummyHzb = cache.GetDummyContactDepth(nvDevice); + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* hz = (shadowHZB && shadowHZB[i]) ? shadowHZB[i] : dummyHzb; + if (hz) + bsb.Texture(kHzb[i], hz); + } + (void)shadowMask; nvrhi::ITexture* sky0 = envSky0 ? envSky0 : cache.GetDummyCubeMap(nvDevice); nvrhi::ITexture* sky1 = envSky1 ? envSky1 : cache.GetDummyCubeMap(nvDevice); if (sky0) bsb.Texture("s_env0", sky0); if (sky1) bsb.Texture("s_env1", sky1); + BindIBLResources(bsb, GetCurrentIBLBindResources(), nvDevice); } static SkinnedPhaseContext BuildSkinnedPhaseContext( @@ -517,7 +646,10 @@ static SkinnedPhaseContext BuildSkinnedPhaseContext( nvrhi::ITexture* contactHistory, nvrhi::ITexture* envSky0, nvrhi::ITexture* envSky1, - nvrhi::ITexture* localShadowAtlas = nullptr) + nvrhi::ITexture* localShadowAtlas = nullptr, + nvrhi::ITexture* const* shadowHZB = nullptr, + nvrhi::ITexture* shadowMask = nullptr, + nvrhi::ITexture* localShadowESM = nullptr) { using namespace fg; using namespace fg::bindless; @@ -549,12 +681,14 @@ static SkinnedPhaseContext BuildSkinnedPhaseContext( bsb.ConstantBuffer("static_globals", staticGlobalsCB); bsb.BufferSRV("g_BoneMatrices", globalBoneBuffer); bsb.ConstantBuffer("SkinnedMaterialCB", materialIdCB); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); - bsb.BufferSRV("g_PaintSplats", splatBuffer); + BindBindlessMaterialTables(bsb); + BindPaintSplatBuffer(bsb, nvDevice, splatBuffer); bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); + if (ClusteredLightManager::Instance().GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", ClusteredLightManager::Instance().GetShadowDataBuffer()); bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); - BindSkinnedLightingTextures(bsb, nvDevice, shadowCascades, hudShadowMap, contactDepth, contactHistory, envSky0, envSky1, isHUD, localShadowAtlas); + BindSkinnedLightingTextures(bsb, nvDevice, shadowCascades, hudShadowMap, contactDepth, contactHistory, envSky0, envSky1, isHUD, localShadowAtlas, shadowHZB, shadowMask, localShadowESM); ctx.bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), activeLayout, nvDevice); return ctx; @@ -628,7 +762,10 @@ static void DrawSkinnedBatch( } else if (ctx.isHUD) { - pipeline = SelectHUDSkinnedPipeline(state, batch.vertexStride, batch.skinningRenderMode); + if (IsScopeBatch(batch) && state.hudScopePS) + pipeline = SelectHUDScopePipeline(state, batch.vertexStride, batch.skinningRenderMode); + else + pipeline = SelectHUDSkinnedPipeline(state, batch.vertexStride, batch.skinningRenderMode); } else { @@ -663,9 +800,189 @@ static void DrawSkinnedBatch( } } -// ═══════════════════════════════════════════════════════════════════════════ -// SKINNING PASS SETUP -// ═══════════════════════════════════════════════════════════════════════════ +framegraph::VirtualResourceHandle setupSkinnedDepthPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle depthInput, + const GeometryCollector* geometry, + MaterialCache* materialCache, + u32 width, + u32 height, + fg::GPUCullingManager* gpuCulling, + SkinningPassState* state, + decals::OverlayManager* overlayMgr) +{ + using namespace framegraph; + + if (!device || !state || !depthInput.is_valid() || !geometry) + return depthInput; + + { + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA8_UNORM); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA32_FLOAT); + fbInfo.depthFormat = nvrhi::Format::D32; + InitializeSkinningResources(device, fbInfo, *state); + } + + if (!state->depthLayout || !state->depthPS) + return depthInput; + + struct DepthPassData + { + VirtualResourceHandle depth; + fg::RenderDevice* device = nullptr; + const GeometryCollector* geometry = nullptr; + MaterialCache* materialCache = nullptr; + fg::GPUCullingManager* gpuCulling = nullptr; + SkinningPassState* passState = nullptr; + decals::OverlayManager* overlayMgr = nullptr; + u32 width = 0, height = 0; + }; + + auto& passData = fg.addCallbackPass( + "SkinnedDepth", + [&, width, height, geometry, materialCache, gpuCulling, state, overlayMgr, depthInput]( + FrameGraph& builder, PassHandle passHandle, DepthPassData& data) { + RenderPassBuilder passBuilder(builder, passHandle); + data.depth = passBuilder.readWrite(depthInput, ResourceState::DepthStencilWrite); + data.device = device; + data.geometry = geometry; + data.materialCache = materialCache; + data.gpuCulling = gpuCulling; + data.passState = state; + data.overlayMgr = overlayMgr; + data.width = width; + data.height = height; + }, + [](const DepthPassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + if (!data.passState || !data.geometry || !data.device) + return; + + u32 worldSkinnedCount = 0; + for (const auto& batch : data.geometry->GetBatches()) + { + if (batch.isSkinned) + ++worldSkinnedCount; + } + if (worldSkinnedCount == 0) + return; + + auto* depthRT = fgGraph.GetPhysicalTexture(data.depth); + if (!depthRT) + return; + + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + if (!nvDevice || !cmdList) + return; + + GPUCullingManager* gpuCullMgr = data.gpuCulling; + if (!gpuCullMgr || !gpuCullMgr->GetGlobalBoneBuffer()) + return; + + if (data.materialCache) + data.materialCache->FinalizePendingMaterials(ctx); + MaterialBuffer::Instance().Upload(ctx); + if (data.overlayMgr) + data.overlayMgr->UploadSplats(cmdList); + + nvrhi::FramebufferDesc fbDesc; + fbDesc.setDepthAttachment(depthRT); + auto& cache = GetPassResourceCache(); + auto framebuffer = cache.GetOrCreateFramebuffer("SkinnedDepth", fbDesc, nvDevice); + if (!framebuffer) + return; + + auto* backend = data.device->GetBackend(); + nvrhi::IDescriptorTable* bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; + + auto dynTransformsCB = cache.GetOrCreateVolatileCB( + "SkinnedDepth", "DynTransforms", sizeof(DynamicTransforms), data.device, 1024 * 8); + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + auto materialIdCB = cache.GetOrCreateVolatileCB( + "SkinnedDepth", "MaterialId", sizeof(SkinnedMaterialCB), data.device, 1024 * 8); + if (!dynTransformsCB || !staticGlobalsCB || !materialIdCB) + return; + + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* vsReflection = shaderLoader->GetCachedReflection("bindless_skinned", ".vs"); + auto* psReflection = shaderLoader->GetCachedReflection("bindless_skinned_depth", ".ps"); + if (!vsReflection || !psReflection || !data.passState->depthLayout) + return; + + nvrhi::IBuffer* splatBuffer = data.overlayMgr ? data.overlayMgr->GetSplatBuffer() : nullptr; + BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "SkinnedDepth"); + bsb.ConstantBuffer("dynamic_transforms", dynTransformsCB); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.BufferSRV("g_BoneMatrices", gpuCullMgr->GetGlobalBoneBuffer()); + bsb.ConstantBuffer("SkinnedMaterialCB", materialIdCB); + BindBindlessMaterialTables(bsb); + BindPaintSplatBuffer(bsb, nvDevice, splatBuffer); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.passState->depthLayout, nvDevice); + if (!bindingSet) + return; + + nvrhi::Viewport viewport( + 0.0f, static_cast(data.width), + 0.0f, static_cast(data.height), + 0.0f, 1.0f); + nvrhi::Rect scissor(data.width, data.height); + + for (const auto& batch : data.geometry->GetBatches()) + { + if (!batch.isSkinned || !batch.vertexBuffer || !batch.indexBuffer) + continue; + + nvrhi::IGraphicsPipeline* pipeline = SelectSkinnedDepthPipeline( + *data.passState, batch.vertexStride, batch.skinningRenderMode); + if (!pipeline) + continue; + + u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); + auto sr = GetSplatRange(batch, data.overlayMgr); + + DynamicTransforms dynTransData = {}; + FillDynamicTransforms(dynTransData, batch.worldMatrix); + cmdList->writeBuffer(dynTransformsCB, &dynTransData, sizeof(dynTransData)); + + SkinnedMaterialCB matIdData = {}; + matIdData.materialID = batch.bindlessMaterialID; + matIdData.skeletonBoneOffset = boneOffset; + matIdData.splatOffset = sr.offset; + matIdData.splatCount = sr.count; + cmdList->writeBuffer(materialIdCB, &matIdData, sizeof(matIdData)); + + nvrhi::GraphicsState gfxState; + gfxState.pipeline = pipeline; + gfxState.framebuffer = framebuffer; + gfxState.bindings = {bindingSet}; + if (bindlessTable) + gfxState.addBindingSet(bindlessTable); + gfxState.vertexBuffers = {{batch.vertexBuffer, 0, 0}}; + gfxState.indexBuffer = {batch.indexBuffer, nvrhi::Format::R16_UINT, 0}; + gfxState.viewport.addViewport(viewport); + gfxState.viewport.addScissorRect(scissor); + + cmdList->setGraphicsState(gfxState); + cmdList->drawIndexed( + nvrhi::DrawArguments() + .setVertexCount(batch.indexCount) + .setStartIndexLocation(batch.startIndex) + .setStartVertexLocation(batch.baseVertex)); + } + }); + + return passData.depth; +} framegraph::DefaultOutputLayout setupSkinningPass( framegraph::FrameGraph& fg, @@ -688,7 +1005,10 @@ framegraph::DefaultOutputLayout setupSkinningPass( nvrhi::ITexture* envSky1, nvrhi::ITexture* hudShadowMap, nvrhi::ITexture* const* shadowCascadesIn, - nvrhi::ITexture* localShadowAtlas) + nvrhi::ITexture* localShadowAtlas, + nvrhi::ITexture* const* shadowHZBIn, + nvrhi::ITexture* shadowMask, + nvrhi::ITexture* localShadowESM) { using namespace framegraph; @@ -709,7 +1029,7 @@ framegraph::DefaultOutputLayout setupSkinningPass( // SETUP LAMBDA // ═══════════════════════════════════════════════════════ [&, width, height, gpuCulling, skinnedDrawArgs, state, overlayMgr, - shadowMapArray, shadowMapHandle, contactDepth, contactHistory, envSky0, envSky1, hudShadowMap, shadowCascadesIn, localShadowAtlas](FrameGraph& builder, PassHandle passHandle, SkinningPassData& data) { + shadowMapArray, shadowMapHandle, contactDepth, contactHistory, envSky0, envSky1, hudShadowMap, shadowCascadesIn, localShadowAtlas, shadowHZBIn, shadowMask, localShadowESM](FrameGraph& builder, PassHandle passHandle, SkinningPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.width = width; @@ -723,9 +1043,14 @@ framegraph::DefaultOutputLayout setupSkinningPass( data.overlayMgr = overlayMgr; data.shadowMapArray = shadowMapArray; for (int i = 0; i < 3; ++i) + { data.shadowCascades[i] = (shadowCascadesIn && shadowCascadesIn[i]) ? shadowCascadesIn[i] : shadowMapArray; + data.shadowHZB[i] = (shadowHZBIn && shadowHZBIn[i]) ? shadowHZBIn[i] : nullptr; + } data.hudShadowMap = hudShadowMap; data.localShadowAtlas = localShadowAtlas; + data.localShadowESM = localShadowESM; + data.shadowMask = shadowMask; data.contactDepth = contactDepth; data.contactHistory = contactHistory; data.envSky0 = envSky0; @@ -871,7 +1196,8 @@ framegraph::DefaultOutputLayout setupSkinningPass( dynTransformsCB, staticGlobalsCB, materialIdCB, globalBoneBuffer, bindlessTable, bindlessLayout, splatBuffer, worldViewport, scissor, false, - data.shadowCascades, data.hudShadowMap, data.contactDepth, data.contactHistory, data.envSky0, data.envSky1, data.localShadowAtlas); + data.shadowCascades, data.hudShadowMap, data.contactDepth, data.contactHistory, data.envSky0, data.envSky1, data.localShadowAtlas, + data.shadowHZB, data.shadowMask, data.localShadowESM); const bool cullActive = data.skinnedDrawArgs.is_valid() && gpuCullMgr->IsSkinnedCullingEnabled() @@ -903,15 +1229,17 @@ framegraph::DefaultOutputLayout setupSkinningPass( framegraph::BindingSetBuilder bsb(*mdiVsRefl, *mdiPsRefl, nvDevice, "Skinning.MDI"); bsb.ConstantBuffer("static_globals", staticGlobalsCB); bsb.BufferSRV("g_BoneMatrices", globalBoneBuffer); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); - bsb.BufferSRV("g_PaintSplats", splatBuffer); + BindBindlessMaterialTables(bsb); + BindPaintSplatBuffer(bsb, nvDevice, splatBuffer); bsb.BufferSRV("g_SkinnedRecords", bucket.recordsBuffer); bsb.BufferSRV("g_SkinnedCompactIndices", bucket.compactBatchIndicesBuffer); bsb.BufferSRV("g_SkinnedCompactMaterialIDs", bucket.compactMaterialIDBuffer); bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); + if (ClusteredLightManager::Instance().GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", ClusteredLightManager::Instance().GetShadowDataBuffer()); bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); - BindSkinnedLightingTextures(bsb, nvDevice, data.shadowCascades, data.hudShadowMap, data.contactDepth, data.contactHistory, data.envSky0, data.envSky1, false, data.localShadowAtlas); + BindSkinnedLightingTextures(bsb, nvDevice, data.shadowCascades, data.hudShadowMap, data.contactDepth, data.contactHistory, data.envSky0, data.envSky1, false, data.localShadowAtlas, data.shadowHZB, data.shadowMask, data.localShadowESM); auto& cache = framegraph::GetPassResourceCache(); nvrhi::BindingSetHandle mdiBindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.passState->mdiLayout, nvDevice); @@ -960,35 +1288,9 @@ framegraph::DefaultOutputLayout setupSkinningPass( } } - // ═══════════════════════════════════════════════════════ - // PHASE 2: HUD SKINNED MESHES (depth [0.0, 0.1]) - // ═══════════════════════════════════════════════════════ - if (hasHUDSkinned) { - nvrhi::Viewport hudViewport( - 0.0f, static_cast(rtDesc.width), - 0.0f, static_cast(rtDesc.height), - 0.0f, 0.1f - ); - - SkinnedPhaseContext hudCtx = BuildSkinnedPhaseContext( - *data.passState, nvDevice, framebuffer, - dynTransformsCB, staticGlobalsCB, materialIdCB, - globalBoneBuffer, bindlessTable, bindlessLayout, splatBuffer, - hudViewport, scissor, true, - data.shadowCascades, data.hudShadowMap, data.contactDepth, data.contactHistory, data.envSky0, data.envSky1, data.localShadowAtlas); - - for (const auto& batch : *data.hudBatches) { - Fmatrix adjustedWorldMatrix = ApplyHUDFOVAdjustment(batch.worldMatrix); - u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); - - DrawSkinnedBatch(*data.passState, cmdList, nvDevice, hudCtx, - batch, adjustedWorldMatrix, boneOffset, {0, 0}); - } - } } ); - // Return outputs DefaultOutputLayout outputs; outputs.albedo = passData.color; outputs.normal = passData.normal; @@ -998,4 +1300,260 @@ framegraph::DefaultOutputLayout setupSkinningPass( return outputs; } +framegraph::VirtualResourceHandle setupHudOverlayPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle normal, + framegraph::VirtualResourceHandle baseColor, + framegraph::VirtualResourceHandle worldPos, + const xr_vector* hudBatches, + MaterialCache* materialCache, + u32 width, + u32 height, + fg::GPUCullingManager* gpuCulling, + SkinningPassState* state, + nvrhi::ITexture* shadowMapArray, + nvrhi::ITexture* contactDepth, + nvrhi::ITexture* contactHistory, + nvrhi::ITexture* envSky0, + nvrhi::ITexture* envSky1, + nvrhi::ITexture* hudShadowMap, + nvrhi::ITexture* const* shadowCascadesIn, + nvrhi::ITexture* localShadowAtlas, + nvrhi::ITexture* const* shadowHZBIn, + nvrhi::ITexture* shadowMask, + nvrhi::ITexture* localShadowESM, + bool rtgiGuidePass) +{ + using namespace framegraph; + + if (!device || !state || !hudBatches || hudBatches->empty() || !sceneColor.is_valid() || !depth.is_valid()) + return sceneColor; + + { + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA8_UNORM); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA32_FLOAT); + fbInfo.depthFormat = nvrhi::Format::D32; + InitializeSkinningResources(device, fbInfo, *state); + } + + struct HudOverlayPassData { + VirtualResourceHandle color; + VirtualResourceHandle normal; + VirtualResourceHandle baseColor; + VirtualResourceHandle worldPos; + VirtualResourceHandle depth; + fg::RenderDevice* device = nullptr; + const xr_vector* hudBatches = nullptr; + MaterialCache* materialCache = nullptr; + fg::GPUCullingManager* gpuCulling = nullptr; + SkinningPassState* passState = nullptr; + u32 width = 0; + u32 height = 0; + nvrhi::ITexture* shadowCascades[3] = {}; + nvrhi::ITexture* hudShadowMap = nullptr; + nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* localShadowESM = nullptr; + nvrhi::ITexture* shadowHZB[3] = {}; + nvrhi::ITexture* shadowMask = nullptr; + nvrhi::ITexture* contactDepth = nullptr; + nvrhi::ITexture* contactHistory = nullptr; + nvrhi::ITexture* envSky0 = nullptr; + nvrhi::ITexture* envSky1 = nullptr; + bool rtgiGuidePass = false; + }; + + auto& passData = fg.addCallbackPass( + rtgiGuidePass ? "HUD RTGI Guide" : "HUD Overlay", + [&, width, height, gpuCulling, state, materialCache, shadowMapArray, contactDepth, contactHistory, + envSky0, envSky1, hudShadowMap, shadowCascadesIn, localShadowAtlas, shadowHZBIn, shadowMask, localShadowESM, rtgiGuidePass]( + FrameGraph& builder, PassHandle passHandle, HudOverlayPassData& data) { + RenderPassBuilder passBuilder(builder, passHandle); + data.width = width; + data.height = height; + data.device = device; + data.hudBatches = hudBatches; + data.materialCache = materialCache; + data.gpuCulling = gpuCulling; + data.passState = state; + data.rtgiGuidePass = rtgiGuidePass; + for (int i = 0; i < 3; ++i) + { + data.shadowCascades[i] = (shadowCascadesIn && shadowCascadesIn[i]) ? shadowCascadesIn[i] : shadowMapArray; + data.shadowHZB[i] = (shadowHZBIn && shadowHZBIn[i]) ? shadowHZBIn[i] : nullptr; + } + data.hudShadowMap = hudShadowMap; + data.localShadowAtlas = localShadowAtlas; + data.localShadowESM = localShadowESM; + data.shadowMask = shadowMask; + data.contactDepth = contactDepth; + data.contactHistory = contactHistory; + data.envSky0 = envSky0; + data.envSky1 = envSky1; + + data.color = passBuilder.readWrite(sceneColor, ResourceState::RenderTarget); + data.depth = passBuilder.readWrite(depth, ResourceState::DepthStencilWrite); + if (normal.is_valid()) + data.normal = passBuilder.readWrite(normal, ResourceState::RenderTarget); + if (baseColor.is_valid()) + data.baseColor = passBuilder.readWrite(baseColor, ResourceState::RenderTarget); + if (worldPos.is_valid()) + data.worldPos = passBuilder.readWrite(worldPos, ResourceState::RenderTarget); + }, + [](const HudOverlayPassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + if (!data.passState || !data.hudBatches || data.hudBatches->empty()) + return; + if (!data.gpuCulling || !data.gpuCulling->GetGlobalBoneBuffer()) + return; + if (!TerrainMaterialBuffer::Instance().GetBuffer()) + return; + + auto* colorRT = fgGraph.GetPhysicalTexture(data.color); + auto* depthRT = fgGraph.GetPhysicalTexture(data.depth); + auto* normalRT = data.normal.is_valid() ? fgGraph.GetPhysicalTexture(data.normal) : nullptr; + auto* baseColorRT = data.baseColor.is_valid() ? fgGraph.GetPhysicalTexture(data.baseColor) : nullptr; + auto* worldPosRT = data.worldPos.is_valid() ? fgGraph.GetPhysicalTexture(data.worldPos) : nullptr; + if (!colorRT || !depthRT || !normalRT || !baseColorRT || !worldPosRT) + return; + + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + if (!nvDevice || !cmdList || !data.passState->initialized) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(colorRT); + if (normalRT) + fbDesc.addColorAttachment(normalRT); + if (baseColorRT) + fbDesc.addColorAttachment(baseColorRT); + if (worldPosRT) + fbDesc.addColorAttachment(worldPosRT); + fbDesc.setDepthAttachment(depthRT); + auto framebuffer = framegraph::GetPassResourceCache().GetOrCreateFramebuffer( + data.rtgiGuidePass ? "HudRtgiGuidePass" : "HudOverlayPass", fbDesc, nvDevice); + if (!framebuffer) + return; + + GPUCullingManager* gpuCullMgr = data.gpuCulling; + nvrhi::IBuffer* globalBoneBuffer = gpuCullMgr->GetGlobalBoneBuffer(); + auto& cache = framegraph::GetPassResourceCache(); + auto dynTransformsCB = cache.GetOrCreateVolatileCB("HudOverlay", "DynTransforms", sizeof(DynamicTransforms), data.device, 1024 * 8); + auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + auto materialIdCB = cache.GetOrCreateVolatileCB("HudOverlay", "MaterialId", sizeof(SkinnedMaterialCB), data.device, 1024 * 8); + + { + StaticGlobals sg = BuildStaticGlobals(2.0f, data.rtgiGuidePass); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + + if (data.materialCache) + data.materialCache->FinalizePendingMaterials(ctx); + MaterialBuffer::Instance().Upload(ctx); + + auto* backend = data.device->GetBackend(); + nvrhi::IDescriptorTable* bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; + nvrhi::IBindingLayout* bindlessLayout = backend ? backend->GetBindlessLayout() : nullptr; + + const auto& rtDesc = colorRT->getDesc(); + nvrhi::Viewport hudViewport( + 0.0f, static_cast(rtDesc.width), + 0.0f, static_cast(rtDesc.height), + 0.0f, 0.1f); + nvrhi::Rect scissor(rtDesc.width, rtDesc.height); + + bool hasScopeBatch = false; + for (const auto& batch : *data.hudBatches) { + if (IsScopeBatch(batch)) { hasScopeBatch = true; break; } + } + + if (hasScopeBatch && data.passState->hudScopePS) + { + auto& st = *data.passState; + const u32 w = rtDesc.width; + const u32 h = rtDesc.height; + if (!st.secondVP || st.secondVP->getDesc().width != w || st.secondVP->getDesc().height != h) + { + nvrhi::TextureDesc td = colorRT->getDesc(); + td.width = w; + td.height = h; + td.debugName = "rt_SecondVP"; + td.isRenderTarget = false; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + st.secondVP = nvDevice->createTexture(td); + } + if (st.secondVP) + { + cmdList->copyTexture( + st.secondVP, nvrhi::TextureSlice(), + colorRT, nvrhi::TextureSlice()); + } + } + + SkinnedPhaseContext hudCtx = BuildSkinnedPhaseContext( + *data.passState, nvDevice, framebuffer, + dynTransformsCB, staticGlobalsCB, materialIdCB, + globalBoneBuffer, bindlessTable, bindlessLayout, nullptr, + hudViewport, scissor, true, + data.shadowCascades, data.hudShadowMap, data.contactDepth, data.contactHistory, data.envSky0, data.envSky1, data.localShadowAtlas, + data.shadowHZB, data.shadowMask, data.localShadowESM); + + SkinnedPhaseContext scopeCtx = hudCtx; + if (hasScopeBatch && data.passState->hudScopePS && data.passState->hudScopeLayout && data.passState->secondVP) + { + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* vsReflection = shaderLoader->GetCachedReflection("bindless_skinned", ".vs"); + auto* psReflection = shaderLoader->GetCachedReflection("bindless_skinned_scope", ".ps"); + if (vsReflection && psReflection) + { + struct alignas(16) ScopeCB { Fvector4 hud; Fvector4 zoom; }; + ScopeCB scb{}; + if (g_pGamePersistent && g_pGamePersistent->m_pGShaderConstants) + { + scb.hud = g_pGamePersistent->m_pGShaderConstants->hud_params; + scb.zoom.set(0.f, 0.f, 1.f, 0.f); + } + else + scb.hud.set(0.f, 0.f, 0.015f, 1.f); + + auto* scopeParamsCB = cache.GetOrCreateVolatileCB( + "HudOverlay", "ScopeParams", sizeof(ScopeCB), data.device); + if (scopeParamsCB) + cmdList->writeBuffer(scopeParamsCB, &scb, sizeof(scb)); + + framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "HudOverlay.Scope"); + bsb.ConstantBuffer("dynamic_transforms", dynTransformsCB); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.BufferSRV("g_BoneMatrices", globalBoneBuffer); + bsb.ConstantBuffer("SkinnedMaterialCB", materialIdCB); + BindBindlessMaterialTables(bsb); + if (scopeParamsCB) + bsb.ConstantBuffer("ScopeParams", scopeParamsCB); + bsb.Texture("s_vp2", data.passState->secondVP); + scopeCtx.bindingSet = cache.GetOrCreateBindingSet( + bsb.Build(), data.passState->hudScopeLayout, nvDevice); + } + } + + for (const auto& batch : *data.hudBatches) { + Fmatrix adjustedWorldMatrix = ApplyHUDFOVAdjustment(batch.worldMatrix); + u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); + const SkinnedPhaseContext& drawCtx = + (IsScopeBatch(batch) && scopeCtx.bindingSet) ? scopeCtx : hudCtx; + + DrawSkinnedBatch(*data.passState, cmdList, nvDevice, drawCtx, + batch, adjustedWorldMatrix, boneOffset, {0, 0}); + } + } + ); + + return passData.color; +} + } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h index be953d09308..e4258b217e2 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h @@ -30,13 +30,6 @@ namespace xray::render::fg::passes { // ═══════════════════════════════════════════════════════════════════════════ // SKINNING PASS - Consolidated skinned mesh rendering // ═══════════════════════════════════════════════════════════════════════════ -// Renders all skinned meshes in two phases: -// 1. World Phase - NPCs, monsters, etc. with normal depth [0.0, 1.0] -// 2. HUD Phase - First-person weapons/hands with compressed depth [0.0, 0.1] -// -// This pass consolidates all skinned mesh rendering that was previously split -// between ForwardColorPass (world skinned) and HUDPass (HUD skinned). - struct SkinningPipelineVariant { nvrhi::GraphicsPipelineHandle pipeline; nvrhi::InputLayoutHandle inputLayout; @@ -57,8 +50,10 @@ struct SkinningPassState { SkinningPipelineVariant tessHQ4w; nvrhi::BindingLayoutHandle layout; nvrhi::BindingLayoutHandle hudLayout; + nvrhi::BindingLayoutHandle hudScopeLayout; nvrhi::ShaderHandle ps; nvrhi::ShaderHandle hudPS; + nvrhi::ShaderHandle hudScopePS; nvrhi::ShaderHandle tessHS; nvrhi::ShaderHandle tessDS; SkinningPipelineVariant mdiNonHQ; @@ -73,6 +68,19 @@ struct SkinningPassState { SkinningPipelineVariant hudHQ2w; SkinningPipelineVariant hudHQ3w; SkinningPipelineVariant hudHQ4w; + SkinningPipelineVariant hudScopeNonHQ; + SkinningPipelineVariant hudScopeHQ1w; + SkinningPipelineVariant hudScopeHQ2w; + SkinningPipelineVariant hudScopeHQ3w; + SkinningPipelineVariant hudScopeHQ4w; + SkinningPipelineVariant depthNonHQ; + SkinningPipelineVariant depthHQ1w; + SkinningPipelineVariant depthHQ2w; + SkinningPipelineVariant depthHQ3w; + SkinningPipelineVariant depthHQ4w; + nvrhi::BindingLayoutHandle depthLayout; + nvrhi::ShaderHandle depthPS; + nvrhi::TextureHandle secondVP; nvrhi::SamplerHandle linearSampler; bool initialized = false; }; @@ -100,20 +108,34 @@ struct SkinningPassData { nvrhi::ITexture* shadowCascades[3] = {}; nvrhi::ITexture* hudShadowMap = nullptr; nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* localShadowESM = nullptr; + nvrhi::ITexture* shadowHZB[3] = {}; + nvrhi::ITexture* shadowMask = nullptr; nvrhi::ITexture* contactDepth = nullptr; nvrhi::ITexture* contactHistory = nullptr; nvrhi::ITexture* envSky0 = nullptr; nvrhi::ITexture* envSky1 = nullptr; }; -// Main skinning pass setup function -// Renders world skinned meshes followed by HUD skinned meshes +framegraph::VirtualResourceHandle setupSkinnedDepthPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle depthInput, + const GeometryCollector* geometry, + MaterialCache* materialCache, + u32 width, + u32 height, + fg::GPUCullingManager* gpuCulling, + SkinningPassState* state, + decals::OverlayManager* overlayMgr = nullptr +); + framegraph::DefaultOutputLayout setupSkinningPass( framegraph::FrameGraph& fg, fg::RenderDevice* device, const framegraph::DefaultOutputLayout& inputs, const GeometryCollector* geometry, // Contains world skinned batches - const xr_vector* hudBatches, // HUD skinned batches (weapons, hands) + const xr_vector* hudBatches, // unused when HUD is deferred; pass nullptr MaterialCache* materialCache, u32 width, u32 height, @@ -129,7 +151,38 @@ framegraph::DefaultOutputLayout setupSkinningPass( nvrhi::ITexture* envSky1 = nullptr, nvrhi::ITexture* hudShadowMap = nullptr, nvrhi::ITexture* const* shadowCascades = nullptr, - nvrhi::ITexture* localShadowAtlas = nullptr + nvrhi::ITexture* localShadowAtlas = nullptr, + nvrhi::ITexture* const* shadowHZB = nullptr, + nvrhi::ITexture* shadowMask = nullptr, + nvrhi::ITexture* localShadowESM = nullptr +); + +framegraph::VirtualResourceHandle setupHudOverlayPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle normal, + framegraph::VirtualResourceHandle baseColor, + framegraph::VirtualResourceHandle worldPos, + const xr_vector* hudBatches, + MaterialCache* materialCache, + u32 width, + u32 height, + fg::GPUCullingManager* gpuCulling, + SkinningPassState* state, + nvrhi::ITexture* shadowMapArray = nullptr, + nvrhi::ITexture* contactDepth = nullptr, + nvrhi::ITexture* contactHistory = nullptr, + nvrhi::ITexture* envSky0 = nullptr, + nvrhi::ITexture* envSky1 = nullptr, + nvrhi::ITexture* hudShadowMap = nullptr, + nvrhi::ITexture* const* shadowCascades = nullptr, + nvrhi::ITexture* localShadowAtlas = nullptr, + nvrhi::ITexture* const* shadowHZB = nullptr, + nvrhi::ITexture* shadowMask = nullptr, + nvrhi::ITexture* localShadowESM = nullptr, + bool rtgiGuidePass = false ); } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/SkyPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SkyPassSetup.cpp index 9767de3d8e6..b4dffd2b82e 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkyPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SkyPassSetup.cpp @@ -42,6 +42,7 @@ framegraph::VirtualResourceHandle setupSkyPass( if (!environment) return; data.renderer->DrawSky(cmdList, framebuffer, environment, data.width, data.height); + data.renderer->DrawClouds(cmdList, framebuffer, environment, data.width, data.height); }); return passData.colorOutput; diff --git a/src/Layers/xrRender/FrameGraphPasses/SmokeTrailPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SmokeTrailPassSetup.cpp index 3ede1d80c59..8b921a13efa 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SmokeTrailPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SmokeTrailPassSetup.cpp @@ -472,9 +472,9 @@ DefaultOutputLayout setupSmokeTrailPass( auto* psReflection = shaderLoader->GetCachedReflection("smoke_trail", ".ps"); BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "SmokeTrail.Draw"); bsb.ConstantBuffer("static_globals", staticGlobalsCB) - .ConstantBuffer("TrailParams", trailParamsCB) - .BufferSRV("g_Materials", bindless::MaterialBuffer::Instance().GetBuffer()) - .BufferSRV("g_ControlPoints", mgr->GetCompactBuffer()) + .ConstantBuffer("TrailParams", trailParamsCB); + BindBindlessMaterialTables(bsb); + bsb.BufferSRV("g_ControlPoints", mgr->GetCompactBuffer()) .BufferSRV("g_TrailState", mgr->GetStateBuffer()) .Texture("g_Perlin4D", data.perlin4dVolume); auto bindDesc = bsb.Build(); diff --git a/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.cpp index c42ab1dba2f..f3483191784 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.cpp @@ -10,6 +10,7 @@ #include "Layers/xrRender/RenderContext/RenderDevice.h" #include "Layers/xrRender/xrRender_console.h" #include "xrEngine/IGame_Persistent.h" +#include "xrEngine/IGame_Level.h" #include "xrEngine/Environment.h" #include "xrEngine/device.h" @@ -20,13 +21,13 @@ using namespace framegraph; struct alignas(16) SunShaftsCBGPU { - Fvector4 shaft_params; // intensity, quality, samples, scale + Fvector4 shaft_params; }; static_assert(sizeof(SunShaftsCBGPU) == 16); struct alignas(16) SunShaftsCombineCBGPU { - Fvector4 combine_params; // xy = half-res texel size, zw = unused + Fvector4 combine_params; }; static_assert(sizeof(SunShaftsCombineCBGPU) == 16); @@ -35,10 +36,10 @@ struct SunShaftsPassData VirtualResourceHandle depth; VirtualResourceHandle worldPos; VirtualResourceHandle shadowMap; - VirtualResourceHandle output; // half-res shafts + VirtualResourceHandle output; nvrhi::ITexture* shadowMapArray = nullptr; nvrhi::ITexture* shadowCascades[3] = {}; - u32 width = 0; // march (half) resolution + u32 width = 0; u32 height = 0; float intensity = 0.f; u32 quality = 0; @@ -48,9 +49,9 @@ struct SunShaftsPassData struct SunShaftsCombineData { VirtualResourceHandle sceneColor; - VirtualResourceHandle shafts; // half-res - VirtualResourceHandle output; // full-res scene + shafts - u32 width = 0; // full resolution + VirtualResourceHandle shafts; + VirtualResourceHandle output; + u32 width = 0; u32 height = 0; u32 halfWidth = 0; u32 halfHeight = 0; @@ -59,8 +60,18 @@ struct SunShaftsCombineData void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state) { - if (state.initialized || !device) + constexpr u32 kPipeVersion = 6; + if (!device) return; + if (state.initialized && state.pipeline && state.combinePipeline && state.pipeVersion == kPipeVersion) + return; + + state.initialized = false; + state.pipeVersion = 0; + state.pipeline = nullptr; + state.combinePipeline = nullptr; + state.layout = nullptr; + state.combineLayout = nullptr; auto* loader = GEnv.Render->GetShaderLoader(); if (!loader) @@ -69,6 +80,7 @@ void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state) return; } + BindingSetBuilder::InvalidateReflectionCache(); auto vs = loader->LoadVertexShader("fullscreen"); auto ps = loader->LoadPixelShader("volumetric/sunshafts"); if (!vs.handle || !ps.handle || !ps.reflection) @@ -78,9 +90,19 @@ void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state) return; } + state.marchVsReflection = loader->GetCachedReflection("fullscreen", ".vs"); + state.marchPsReflection = loader->GetCachedReflection("volumetric/sunshafts", ".ps"); + if (!state.marchVsReflection || !state.marchPsReflection) + { + Msg("! [SunShafts] Missing cached reflection (vs=%p ps=%p)", + state.marchVsReflection, state.marchPsReflection); + state.initialized = true; + return; + } + auto& cache = GetPassResourceCache(); state.layout = cache.GetOrCreateBindingLayoutFromReflection( - "SunShafts_CSMLadder", *vs.reflection, *ps.reflection, device); + "SunShafts_CSMLadder_v4", *state.marchVsReflection, *state.marchPsReflection, device); nvrhi::GraphicsPipelineDesc desc; desc.setVertexShader(vs.handle); @@ -94,28 +116,35 @@ void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state) nvrhi::FramebufferInfoEx fbInfo; fbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); - state.pipeline = cache.GetOrCreatePipeline("SunShafts", desc, fbInfo, device); + state.pipeline = cache.GetOrCreatePipeline("SunShafts_v4", desc, fbInfo, device); - // Full-res combine pipeline: upsample + blur half-res shafts, add to scene. auto combinePs = loader->LoadPixelShader("volumetric/sunshafts_combine"); if (combinePs.handle && combinePs.reflection) { + state.combinePsReflection = loader->GetCachedReflection("volumetric/sunshafts_combine", ".ps"); state.combineLayout = cache.GetOrCreateBindingLayoutFromReflection( - "SunShaftsCombine", *vs.reflection, *combinePs.reflection, device); + "SunShaftsCombine_v4", *state.marchVsReflection, *state.combinePsReflection, device); nvrhi::GraphicsPipelineDesc cdesc; cdesc.setVertexShader(vs.handle); cdesc.setPixelShader(combinePs.handle); cdesc.addBindingLayout(state.combineLayout); cdesc.setPrimType(nvrhi::PrimitiveType::TriangleList); - cdesc.renderState.blendState.targets[0].setBlendEnable(false); + auto& bt = cdesc.renderState.blendState.targets[0]; + bt.setBlendEnable(true); + bt.setSrcBlend(nvrhi::BlendFactor::One); + bt.setDestBlend(nvrhi::BlendFactor::One); + bt.setBlendOp(nvrhi::BlendOp::Add); + bt.setSrcBlendAlpha(nvrhi::BlendFactor::One); + bt.setDestBlendAlpha(nvrhi::BlendFactor::Zero); + bt.setBlendOpAlpha(nvrhi::BlendOp::Add); cdesc.renderState.depthStencilState.setDepthTestEnable(false); cdesc.renderState.depthStencilState.setDepthWriteEnable(false); cdesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); nvrhi::FramebufferInfoEx cfbInfo; cfbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); - state.combinePipeline = cache.GetOrCreatePipeline("SunShaftsCombine", cdesc, cfbInfo, device); + state.combinePipeline = cache.GetOrCreatePipeline("SunShaftsCombine_v4", cdesc, cfbInfo, device); } else { @@ -123,23 +152,34 @@ void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state) } state.initialized = true; - Msg("* [SunShafts] Pass initialized (march=%d combine=%d)", - state.pipeline ? 1 : 0, state.combinePipeline ? 1 : 0); + state.pipeVersion = kPipeVersion; + Msg("* [SunShafts] Pass initialized v4 (march=%d combine=%d cbs=%u srvs=%u)", + state.pipeline ? 1 : 0, state.combinePipeline ? 1 : 0, + (u32)state.marchPsReflection->constantLayout.constantBuffers.buffers.size(), + (u32)state.marchPsReflection->rtBindings.inputTextures.size()); +} + +float ResolveSunShaftsIntensity() +{ + if (!ps_r_sun_shafts) + return 0.f; + if (Device.dwPrecacheFrame) + return 0.f; + if (!g_pGamePersistent || !g_pGameLevel || g_pGamePersistent->IsLoadingScreenShown()) + return 0.f; + + float intensity = g_pGamePersistent->Environment().CurrentEnv.m_fSunShaftsIntensity; + if (SunshaftsIntensity > 0.f) + intensity = SunshaftsIntensity; + intensity *= std::max(ps_r_sun_shafts_scale, 0.f); + return intensity; } static bool NeedSunShafts(float& outIntensity, u32& outQuality) { - outIntensity = 0.f; outQuality = ps_r_sun_shafts; - if (!ps_r_sun_shafts) - return false; - if (!g_pGamePersistent) - return false; - const auto& env = g_pGamePersistent->Environment().CurrentEnv; - outIntensity = env.m_fSunShaftsIntensity; - if (outIntensity < 0.0001f) - return false; - return true; + outIntensity = ResolveSunShaftsIntensity(); + return outIntensity >= 1e-4f; } VirtualResourceHandle setupSunShaftsPass( @@ -155,16 +195,26 @@ VirtualResourceHandle setupSunShaftsPass( SunShaftsPassState& passState, nvrhi::ITexture* const* shadowCascades) { + float intensity = 0.f; + u32 quality = 0; + if (!NeedSunShafts(intensity, quality) || !shadowMapArray) + return sceneColor; + if (device && device->GetNVRHIDevice()) InitializeSunShaftsPass(device->GetNVRHIDevice(), passState); - float intensity = 0.f; - u32 quality = 0; - if (!NeedSunShafts(intensity, quality) || !passState.pipeline || - !passState.combinePipeline || !shadowMapArray) + if (!passState.pipeline || !passState.combinePipeline) return sceneColor; - // Original renders the volumetric sun into a reduced-res buffer, then combines. + { + static float s_lastLogged = -1.f; + if (fabsf(intensity - s_lastLogged) > 0.01f) + { + s_lastLogged = intensity; + Msg("* [SunShafts] active intensity=%.3f quality=%u", intensity, quality); + } + } + const u32 halfW = (width > 1) ? (width / 2) : 1; const u32 halfH = (height > 1) ? (height / 2) : 1; @@ -184,11 +234,10 @@ VirtualResourceHandle setupSunShaftsPass( outDesc.height = height; outDesc.format = nvrhi::Format::RGBA16_FLOAT; outDesc.isRenderTarget = true; - outDesc.isTransient = true; + outDesc.isTransient = false; outDesc.debugName = "rt_SunShafts"; auto output = fg.CreateTexture("rt_SunShafts", outDesc); - // ── Pass 1: half-res ray march (shafts term only) ───────────────────────── fg.addCallbackPass( "SunShaftsMarch", [=, &passState](FrameGraph& builder, PassHandle passHandle, SunShaftsPassData& data) { @@ -217,14 +266,14 @@ VirtualResourceHandle setupSunShaftsPass( auto* depthTex = fg.GetPhysicalTexture(data.depth); auto* worldPosTex = fg.GetPhysicalTexture(data.worldPos); auto* outTex = fg.GetPhysicalTexture(data.output); - if (!depthTex || !worldPosTex || !outTex || !data.shadowMapArray) + if (!depthTex || !worldPosTex || !outTex) return; auto& cache = GetPassResourceCache(); auto staticGlobalsCB = cache.GetOrCreateVolatileCB( "Frame", "StaticGlobals", sizeof(StaticGlobals), ctx->GetDevice()); auto shaftCB = cache.GetOrCreateVolatileCB( - "SunShafts", "SunShaftsCB", sizeof(SunShaftsCBGPU), ctx->GetDevice()); + "SunShafts", "SunShaftsCB_v4", sizeof(SunShaftsCBGPU), ctx->GetDevice()); StaticGlobals sg = BuildStaticGlobals(); cmd->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); @@ -235,13 +284,10 @@ VirtualResourceHandle setupSunShaftsPass( cb.shaft_params.set(data.intensity, float(data.quality), samples, scale); cmd->writeBuffer(shaftCB, &cb, sizeof(cb)); - auto* loader = GEnv.Render->GetShaderLoader(); - auto* vsRefl = loader->GetCachedReflection("fullscreen", ".vs"); - auto* psRefl = loader->GetCachedReflection("volumetric/sunshafts", ".ps"); - if (!vsRefl || !psRefl) + if (!ps->marchVsReflection || !ps->marchPsReflection) return; - BindingSetBuilder bsb(*vsRefl, *psRefl, nv, "SunShaftsMarch"); + BindingSetBuilder bsb(*ps->marchVsReflection, *ps->marchPsReflection, nv, "SunShaftsMarch"); bsb.ConstantBuffer("static_globals", staticGlobalsCB) .ConstantBuffer("SunShaftsCB", shaftCB) .Texture("g_Depth", depthTex) @@ -257,6 +303,16 @@ VirtualResourceHandle setupSunShaftsPass( if (t) bsb.Texture(kNames[i], t); } + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + nvrhi::ITexture* dummyHzb = smpCache.GetDummyContactDepth(nv); + for (u32 i = 0; i < 3; ++i) + { + if (dummyHzb) + bsb.Texture(kHzb[i], dummyHzb); + } + nvrhi::ITexture* contactHist = smpCache.GetDummyContactHistory(nv); + if (contactHist) + bsb.Texture("g_ContactHistory", contactHist); } auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->layout, nv); if (!bindingSet) @@ -264,7 +320,7 @@ VirtualResourceHandle setupSunShaftsPass( nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(outTex); - auto fb = cache.GetOrCreateFramebuffer("SunShaftsMarch", fbDesc, nv); + auto fb = cache.GetOrCreateFramebuffer("SunShaftsMarch_v4", fbDesc, nv); nvrhi::Viewport vp(0.f, float(data.width), 0.f, float(data.height), 0.f, 1.f); nvrhi::GraphicsState state; @@ -276,7 +332,6 @@ VirtualResourceHandle setupSunShaftsPass( cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); }); - // ── Pass 2: full-res combine (upsample + blur + add to scene) ───────────── auto& combineData = fg.addCallbackPass( "SunShaftsCombine", [=, &passState](FrameGraph& builder, PassHandle passHandle, SunShaftsCombineData& data) { @@ -286,9 +341,9 @@ VirtualResourceHandle setupSunShaftsPass( data.halfWidth = halfW; data.halfHeight = halfH; data.passState = &passState; - data.sceneColor = pb.read(sceneColor, ResourceState::ShaderResource); + data.sceneColor = pb.read(sceneColor, ResourceState::CopySource); data.shafts = pb.read(shaftsHalf, ResourceState::ShaderResource); - data.output = pb.write(output, ResourceState::RenderTarget); + data.output = pb.write(output, ResourceState::CopyDest); }, [](const SunShaftsCombineData& data, const FrameGraph& fg, fg::RenderContext* ctx) { auto* ps = data.passState; @@ -303,24 +358,26 @@ VirtualResourceHandle setupSunShaftsPass( if (!colorTex || !shaftsTex || !outTex) return; + cmd->setTextureState(colorTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(outTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(outTex, nvrhi::TextureSlice(), colorTex, nvrhi::TextureSlice()); + cmd->setTextureState(outTex, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); + cmd->setTextureState(shaftsTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + auto& cache = GetPassResourceCache(); auto combineCB = cache.GetOrCreateVolatileCB( - "SunShafts", "SunShaftsCombineCB", sizeof(SunShaftsCombineCBGPU), ctx->GetDevice()); + "SunShafts", "SunShaftsCombineCB_v4", sizeof(SunShaftsCombineCBGPU), ctx->GetDevice()); SunShaftsCombineCBGPU cb{}; cb.combine_params.set( 1.f / float(data.halfWidth), 1.f / float(data.halfHeight), 0.f, 0.f); cmd->writeBuffer(combineCB, &cb, sizeof(cb)); - auto* loader = GEnv.Render->GetShaderLoader(); - auto* vsRefl = loader->GetCachedReflection("fullscreen", ".vs"); - auto* psRefl = loader->GetCachedReflection("volumetric/sunshafts_combine", ".ps"); - if (!vsRefl || !psRefl) + if (!ps->marchVsReflection || !ps->combinePsReflection) return; - BindingSetBuilder bsb(*vsRefl, *psRefl, nv, "SunShaftsCombine"); + BindingSetBuilder bsb(*ps->marchVsReflection, *ps->combinePsReflection, nv, "SunShaftsCombine"); bsb.ConstantBuffer("SunShaftsCombineCB", combineCB) - .Texture("g_Color", colorTex) .Texture("g_Shafts", shaftsTex); auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->combineLayout, nv); if (!bindingSet) @@ -328,7 +385,7 @@ VirtualResourceHandle setupSunShaftsPass( nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(outTex); - auto fb = cache.GetOrCreateFramebuffer("SunShaftsCombine", fbDesc, nv); + auto fb = cache.GetOrCreateFramebuffer("SunShaftsCombine_v4", fbDesc, nv); nvrhi::Viewport vp(0.f, float(data.width), 0.f, float(data.height), 0.f, 1.f); nvrhi::GraphicsState state; @@ -338,6 +395,7 @@ VirtualResourceHandle setupSunShaftsPass( state.addBindingSet(bindingSet); cmd->setGraphicsState(state); cmd->draw(nvrhi::DrawArguments().setVertexCount(3)); + cmd->setTextureState(outTex, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); }); return combineData.output; diff --git a/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h index 291cfc2e98b..c3a81dc450a 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h @@ -7,6 +7,7 @@ namespace xray::render::framegraph { class FrameGraph; +struct ExtractedReflection; } namespace xray::render::fg @@ -19,11 +20,15 @@ namespace xray::render::fg::passes struct SunShaftsPassState { - nvrhi::GraphicsPipelineHandle pipeline; // half-res march (shafts-only) + nvrhi::GraphicsPipelineHandle pipeline; nvrhi::BindingLayoutHandle layout; - nvrhi::GraphicsPipelineHandle combinePipeline; // full-res upsample + add to scene + nvrhi::GraphicsPipelineHandle combinePipeline; nvrhi::BindingLayoutHandle combineLayout; + framegraph::ExtractedReflection* marchVsReflection = nullptr; + framegraph::ExtractedReflection* marchPsReflection = nullptr; + framegraph::ExtractedReflection* combinePsReflection = nullptr; bool initialized = false; + u32 pipeVersion = 0; }; framegraph::VirtualResourceHandle setupSunShaftsPass( @@ -41,4 +46,6 @@ framegraph::VirtualResourceHandle setupSunShaftsPass( void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state); +float ResolveSunShaftsIntensity(); + } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.cpp index e884b73bf4c..5df18949069 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.cpp @@ -10,6 +10,8 @@ extern ENGINE_API float ps_r_taa_sharpness; extern ENGINE_API int ps_r_taa; +extern ENGINE_API int ps_r_taa_jitter; +extern ENGINE_API int ps_r_upscale; namespace xray::render::fg::passes { @@ -22,23 +24,49 @@ float g_taa_jitter_prev_py = 0.f; Fmatrix g_taa_unjittered_full_transform; Fmatrix g_taa_unjittered_inv_full_transform; +static float Halton(u32 index, u32 base) +{ + float f = 1.f; + float r = 0.f; + while (index > 0) + { + f /= float(base); + r += f * float(index % base); + index /= base; + } + return r; +} + void ApplyTAAJitter() { - // Keep temporal reprojection in the unjittered camera space. - g_taa_unjittered_full_transform.mul(Device.mProject, Device.mView); - g_taa_unjittered_inv_full_transform.invert_44(g_taa_unjittered_full_transform); + g_taa_unjittered_full_transform = Device.mFullTransform; + g_taa_unjittered_inv_full_transform = Device.mInvFullTransform; g_taa_jitter_prev_px = g_taa_jitter_px; g_taa_jitter_prev_py = g_taa_jitter_py; - g_taa_jitter_px = 0.f; - g_taa_jitter_py = 0.f; - // Full projection jitter currently shifts the whole frame in this pipeline. - // Keep TAA temporal accumulation enabled, but leave the camera projection stable. - - Device.mFullTransform = g_taa_unjittered_full_transform; - Device.mInvFullTransform = g_taa_unjittered_inv_full_transform; - if (GEnv.Render) - GEnv.Render->SetCacheXform(Device.mView, Device.mProject); + + const bool wantJitter = (ps_r_taa_jitter != 0) && (ps_r_taa != 0 || ps_r_upscale != 0); + if (!wantJitter) + { + g_taa_jitter_px = 0.f; + g_taa_jitter_py = 0.f; + return; + } + + const u32 phase = Device.dwFrame % 16u; + const float jx = Halton(phase + 1, 2) - 0.5f; + const float jy = Halton(phase + 1, 3) - 0.5f; + g_taa_jitter_px = jx; + g_taa_jitter_py = jy; + + const float w = float(std::max(1u, Device.dwWidth)); + const float h = float(std::max(1u, Device.dwHeight)); + Fmatrix jitterMat; + jitterMat.identity(); + jitterMat._31 = (jx * 2.f) / w; + jitterMat._32 = (jy * 2.f) / h; + Device.mFullTransform.mul(jitterMat, g_taa_unjittered_full_transform); + Device.mInvFullTransform.invert(Device.mFullTransform); } namespace @@ -101,7 +129,7 @@ void InitializeTAA(nvrhi::IDevice* device, TAAPassState& state) } auto& cache = GetPassResourceCache(); state.layout = cache.GetOrCreateBindingLayoutFromReflection( - "TAA_v4", *vs.reflection, *ps.reflection, device); + "TAA_v5", *vs.reflection, *ps.reflection, device); if (state.layout) { nvrhi::GraphicsPipelineDesc desc; @@ -115,7 +143,7 @@ void InitializeTAA(nvrhi::IDevice* device, TAAPassState& state) desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); nvrhi::FramebufferInfoEx fb; fb.addColorFormat(nvrhi::Format::RGBA16_FLOAT); - state.pipeline = cache.GetOrCreatePipeline("TAA_v4", desc, fb, device); + state.pipeline = cache.GetOrCreatePipeline("TAA_v5", desc, fb, device); } state.initialized = true; } @@ -153,6 +181,21 @@ framegraph::VirtualResourceHandle setupTAAPass( if (!state.history[0] || !state.history[1]) return sceneColor; + { + const Fvector camPos = Device.vCameraPosition; + const Fvector camDir = Device.vCameraDirection; + if (state.hasCameraHistory) + { + const float posDelta = camPos.distance_to(state.prevCameraPos); + const float dirDot = std::clamp(camDir.dotproduct(state.prevCameraDir), -1.f, 1.f); + if (posDelta > 8.f || dirDot < 0.7f) + state.hasHistory = false; + } + state.prevCameraPos = camPos; + state.prevCameraDir = camDir; + state.hasCameraHistory = true; + } + const u32 readIdx = state.historyIndex; const u32 writeIdx = 1u - readIdx; diff --git a/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.h index 41c5d30f053..26249e29ac9 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.h @@ -20,6 +20,9 @@ struct TAAPassState u32 historyIndex = 0; bool hasHistory = false; bool initialized = false; + Fvector prevCameraPos = {0, 0, 0}; + Fvector prevCameraDir = {0, 0, 1}; + bool hasCameraHistory = false; }; struct alignas(16) TAAParamsCB diff --git a/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.cpp index c4290ab48e0..11837b8dc05 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.cpp @@ -10,6 +10,7 @@ #include "Layers/xrRender/RenderContext/RenderContext.h" #include "Layers/xrRender/FrameGraph/ShaderLoader.h" #include "Layers/xrRender/RenderContext/RenderDevice.h" +#include "Layers/xrRender/xrRender_console.h" namespace xray::render::fg { extern xray::render::FrameGraphRenderer RImplementation; @@ -44,7 +45,7 @@ void InitializeTonemapPass(nvrhi::IDevice* device, TonemapPassState& state) { auto& cache = framegraph::GetPassResourceCache(); state.bindingLayout = cache.GetOrCreateBindingLayoutFromReflection( - "TonemapPass_ACES", *vsResult.reflection, *psResult.reflection, device); + "TonemapPass_Filmic", *vsResult.reflection, *psResult.reflection, device); if (state.bindingLayout) { nvrhi::GraphicsPipelineDesc pipeDesc; @@ -63,7 +64,7 @@ void InitializeTonemapPass(nvrhi::IDevice* device, TonemapPassState& state) { fbFmt = GEnv.Backend->GetBackBuffer()->getDesc().format; fbInfo.addColorFormat(fbFmt); - state.pipeline = cache.GetOrCreatePipeline("TonemapPass_ACES", pipeDesc, fbInfo, device); + state.pipeline = cache.GetOrCreatePipeline("TonemapPass_Filmic", pipeDesc, fbInfo, device); } } } @@ -100,7 +101,7 @@ framegraph::VirtualResourceHandle setupTonemapPass( auto& passData = fg.addCallbackPass( "Tonemap", - [hdrInput, exposureTexture, outputTarget, hasExposure, hasOutputTarget, width, height, &tonemapState, exposureState](FrameGraph& builder, PassHandle passHandle, TonemapPassData& data) { + [hdrInput, exposureTexture, outputTarget, hasExposure, hasOutputTarget, width, height, &tonemapState, exposureState, device](FrameGraph& builder, PassHandle passHandle, TonemapPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.width = width; @@ -108,6 +109,7 @@ framegraph::VirtualResourceHandle setupTonemapPass( data.hasExposure = hasExposure; data.passState = &tonemapState; data.exposurePassState = exposureState; + data.device = device; data.hdrInput = passBuilder.read(hdrInput, ResourceState::ShaderResource); @@ -172,7 +174,27 @@ framegraph::VirtualResourceHandle setupTonemapPass( if (!vsRefl || !psRefl) return; + struct TonemapCB { + float white; + float contrast; + float pad0; + float pad1; + } tmCB{}; + tmCB.white = ps_r_tonemap_white; + tmCB.contrast = ps_r_tonemap_contrast; + + nvrhi::BufferHandle tonemapCB = nullptr; + if (data.device) + { + tonemapCB = cache.GetOrCreateVolatileCB( + "TonemapPass", "TonemapParams", sizeof(TonemapCB), data.device); + if (tonemapCB) + cmdList->writeBuffer(tonemapCB, &tmCB, sizeof(tmCB)); + } + framegraph::BindingSetBuilder bsb(*vsRefl, *psRefl, device, "Tonemap"); + if (tonemapCB) + bsb.ConstantBuffer("TonemapParams", tonemapCB); bsb.Texture("t_hdr", hdrTexture); bsb.Texture("t_exposure", exposureTex); auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->bindingLayout, device); @@ -181,7 +203,7 @@ framegraph::VirtualResourceHandle setupTonemapPass( nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(ldrTexture); - auto framebuffer = cache.GetOrCreateFramebuffer("TonemapPass_ACES", fbDesc, device); + auto framebuffer = cache.GetOrCreateFramebuffer("TonemapPass_Filmic", fbDesc, device); nvrhi::Viewport viewport; viewport.minX = 0; diff --git a/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.h index 07674234f2c..44a61dcd2a1 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/TonemapPassSetup.h @@ -33,6 +33,7 @@ struct TonemapPassData { u32 height; TonemapPassState* passState; const ExposurePassState* exposurePassState; + fg::RenderDevice* device = nullptr; }; // Lambda-based tonemap pass setup diff --git a/src/Layers/xrRender/FrameGraphPasses/TrailPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/TrailPassSetup.cpp index 2047ca77b7e..9144262b27a 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TrailPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/TrailPassSetup.cpp @@ -386,9 +386,9 @@ TrailPassOutput setupTrailPass( auto* psReflection = shaderLoader->GetCachedReflection("trail", ".ps"); BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Trail"); bsb.ConstantBuffer("static_globals", staticGlobalsCB) - .ConstantBuffer("TrailParams", trailParamsCB) - .BufferSRV("g_Materials", matBuffer.GetBuffer()) - .BufferSRV("g_ControlPoints", st.controlPointBuffer) + .ConstantBuffer("TrailParams", trailParamsCB); + BindBindlessMaterialTables(bsb); + bsb.BufferSRV("g_ControlPoints", st.controlPointBuffer) .BufferSRV("g_TrailState", st.dummyStateBuffer); auto bindDesc = bsb.Build(); auto bindingSet = cache.GetOrCreateBindingSet(bindDesc, st.layout, nvDevice); diff --git a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp index 26308202c73..86b9529a5a5 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "TransparentPassSetup.h" +#include "IBLPrefilterPassSetup.h" #include "ShaderConstants.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/IPass.h" @@ -9,6 +10,7 @@ #include "Layers/xrRender/RenderContext/RenderDevice.h" #include "Layers/xrRender/Backend/D3D12Backend.h" #include "Layers/xrRender/Bindless/MaterialBuffer.h" +#include "Layers/xrRender/Bindless/TerrainMaterialBuffer.h" #include "Layers/xrRender/Bindless/VariantTextureBuffer.h" #include "Layers/xrRender/ShaderVariant/VariantPSOCache.h" #include "Layers/xrRender/FrameGraph/PassResourceCache.h" @@ -19,6 +21,7 @@ #include "Layers/xrRender/ResourceManager/TextureManager.h" #include "xrEngine/Environment.h" #include "xrEngine/IGame_Persistent.h" +#include "Layers/xrRender/xrRender_console.h" namespace xray::render::fg::passes { @@ -44,7 +47,7 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame state.ps = psResult.handle; auto& cache = framegraph::GetPassResourceCache(); - state.layout = cache.GetOrCreateBindingLayoutFromReflection("TransparentPass_CSMLadder", *vsResult.reflection, *psResult.reflection, nvDevice); + state.layout = cache.GetOrCreateBindingLayoutFromReflection("TransparentPass_v2_TranspCB", *vsResult.reflection, *psResult.reflection, nvDevice); if (!state.layout) return; @@ -76,6 +79,8 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; pipeDesc.renderState.rasterState.frontCounterClockwise = false; pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; + pipeDesc.renderState.rasterState.depthBias = -16; + pipeDesc.renderState.rasterState.slopeScaledDepthBias = -2.f; auto& rt0 = pipeDesc.renderState.blendState.targets[0]; rt0.blendEnable = true; @@ -85,11 +90,26 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame rt0.srcBlendAlpha = nvrhi::BlendFactor::One; rt0.destBlendAlpha = nvrhi::BlendFactor::InvSrcAlpha; rt0.blendOpAlpha = nvrhi::BlendOp::Add; + for (u32 rt = 1; rt < 4; ++rt) + pipeDesc.renderState.blendState.targets[rt].setColorWriteMask(nvrhi::ColorMask(0)); - state.pipeline = cache.GetOrCreatePipeline("TransparentPass_CSMLadder", pipeDesc, fbInfo, nvDevice); + state.pipeline = cache.GetOrCreatePipeline("TransparentPass_v3_AlphaBias", pipeDesc, fbInfo, nvDevice); if (!state.pipeline) return; + { + nvrhi::GraphicsPipelineDesc mulDesc = pipeDesc; + auto& mulRt = mulDesc.renderState.blendState.targets[0]; + mulRt.blendEnable = true; + mulRt.srcBlend = nvrhi::BlendFactor::DstColor; + mulRt.destBlend = nvrhi::BlendFactor::SrcColor; + mulRt.blendOp = nvrhi::BlendOp::Add; + mulRt.srcBlendAlpha = nvrhi::BlendFactor::Zero; + mulRt.destBlendAlpha = nvrhi::BlendFactor::One; + mulRt.blendOpAlpha = nvrhi::BlendOp::Add; + state.multiplyPipeline = cache.GetOrCreatePipeline("TransparentPass_v3_MultiplyBias", mulDesc, fbInfo, nvDevice); + } + QueryBindingLayoutFromPipeline(state.pipeline, state.layout); // Dedicated water PSO (soft + foam + SSR) @@ -101,7 +121,7 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame state.waterVs = waterVs.handle; state.waterPs = waterPs.handle; state.waterLayout = cache.GetOrCreateBindingLayoutFromReflection( - "TransparentWater_v3", *waterVs.reflection, *waterPs.reflection, nvDevice); + "TransparentWater_v18_Murk", *waterVs.reflection, *waterPs.reflection, nvDevice); if (state.waterLayout) { nvrhi::GraphicsPipelineDesc waterDesc = pipeDesc; @@ -113,7 +133,14 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame else waterDesc.bindingLayouts = {state.waterLayout}; waterDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; - state.waterPipeline = cache.GetOrCreatePipeline("TransparentWater_v3", waterDesc, fbInfo, nvDevice); + for (u32 rt = 1; rt < 4; ++rt) + { + waterDesc.renderState.blendState.targets[rt].blendEnable = false; + waterDesc.renderState.blendState.targets[rt].setColorWriteMask( + nvrhi::ColorMask::Red | nvrhi::ColorMask::Green | + nvrhi::ColorMask::Blue | nvrhi::ColorMask::Alpha); + } + state.waterPipeline = cache.GetOrCreatePipeline("TransparentWater_v18_Murk", waterDesc, fbInfo, nvDevice); } waterVs.reflection = nullptr; waterPs.reflection = nullptr; @@ -122,6 +149,54 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame Msg("! [TransparentPass] Water PSO unavailable — water clipped from forward"); } + { + auto waterdVs = shaderLoader->LoadVertexShader("waterd", "main"); + auto waterdPs = shaderLoader->LoadPixelShader("waterd", "main"); + if (waterdVs.handle && waterdPs.handle && waterdVs.reflection && waterdPs.reflection) + { + state.waterDistortVs = waterdVs.handle; + state.waterDistortPs = waterdPs.handle; + state.waterDistortLayout = cache.GetOrCreateBindingLayoutFromReflection( + "TransparentWaterDistort_v6_Amp", *waterdVs.reflection, *waterdPs.reflection, nvDevice); + if (state.waterDistortLayout) + { + state.waterDistortInputLayout = + nvDevice->createInputLayout(attrs, attrCount, state.waterDistortVs); + nvrhi::GraphicsPipelineDesc distortDesc; + distortDesc.VS = state.waterDistortVs; + distortDesc.PS = state.waterDistortPs; + distortDesc.inputLayout = state.waterDistortInputLayout; + if (bindlessLayout) + distortDesc.bindingLayouts = {state.waterDistortLayout, bindlessLayout}; + else + distortDesc.bindingLayouts = {state.waterDistortLayout}; + distortDesc.primType = nvrhi::PrimitiveType::TriangleList; + distortDesc.renderState.depthStencilState.depthTestEnable = true; + distortDesc.renderState.depthStencilState.depthWriteEnable = false; + distortDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + distortDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + auto& drt = distortDesc.renderState.blendState.targets[0]; + drt.blendEnable = true; + drt.srcBlend = nvrhi::BlendFactor::One; + drt.destBlend = nvrhi::BlendFactor::One; + drt.blendOp = nvrhi::BlendOp::Add; + drt.srcBlendAlpha = nvrhi::BlendFactor::One; + drt.destBlendAlpha = nvrhi::BlendFactor::One; + drt.blendOpAlpha = nvrhi::BlendOp::Add; + + nvrhi::FramebufferInfoEx distortFb; + distortFb.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + distortFb.depthFormat = nvrhi::Format::D32; + state.waterDistortPipeline = cache.GetOrCreatePipeline( + "TransparentWaterDistort_v6_Amp", distortDesc, distortFb, nvDevice); + } + waterdVs.reflection = nullptr; + waterdPs.reflection = nullptr; + } + if (!state.waterDistortPipeline) + Msg("! [TransparentPass] Water distort PSO unavailable"); + } + if (auto* resMgr = device->GetFGResourceManager()) { if (auto* texMgr = resMgr->GetTextureManager()) @@ -132,7 +207,8 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame } state.initialized = true; - Msg("* [TransparentPass] Pipeline initialized (water=%d)", state.waterPipeline ? 1 : 0); + Msg("* [TransparentPass] Pipeline initialized (water=%d waterd=%d)", + state.waterPipeline ? 1 : 0, state.waterDistortPipeline ? 1 : 0); } framegraph::DefaultOutputLayout setupTransparentPass( @@ -175,6 +251,25 @@ framegraph::DefaultOutputLayout setupTransparentPass( data.baseColor = passBuilder.readWrite(inputs.baseColor, ResourceState::RenderTarget); if (inputs.worldPos.is_valid()) data.worldPos = passBuilder.readWrite(inputs.worldPos, ResourceState::RenderTarget); + for (u32 i = 0; i < 3; ++i) { + if (config.shadowHZBHandles[i].is_valid()) + passBuilder.read(config.shadowHZBHandles[i], ResourceState::ShaderResource); + } + if (config.shadowMaskHandle.is_valid()) + passBuilder.read(config.shadowMaskHandle, ResourceState::ShaderResource); + + if (state.waterDistortPipeline) + { + framegraph::ResourceDesc distDesc; + distDesc.type = framegraph::ResourceDesc::Type::Texture2D; + distDesc.width = width; + distDesc.height = height; + distDesc.format = nvrhi::Format::RGBA16_FLOAT; + distDesc.isRenderTarget = true; + distDesc.isTransient = true; + distDesc.debugName = "rt_Distortion"; + data.distortion = passBuilder.createTexture("rt_Distortion", distDesc); + } }, [](const TransparentPassData& data, @@ -212,13 +307,26 @@ framegraph::DefaultOutputLayout setupTransparentPass( if (!data.passState->initialized || !data.passState->pipeline) return; + if (data.distortion.is_valid()) + { + if (auto* distortRT = fg.GetPhysicalTexture(data.distortion)) + cmdList->clearTextureFloat( + distortRT, nvrhi::AllSubresources, nvrhi::Color(0.f, 0.f, 0.f, 0.f)); + } + using namespace fg::bindless; auto& matBuffer = MaterialBuffer::Instance(); auto lightingCB = cache.GetOrCreateVolatileCB("TransparentPass", "LightingCB", sizeof(LightingConstants), data.device); auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + auto transparentDrawCB = cache.GetOrCreateVolatileCB("Frame", "TransparentDrawCB", 16, data.device); auto drawIndexBuffer = GetOrCreateDrawIndexBuffer("TransparentPass", nvDevice); + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + auto lightingData = FillLightingConstants(); cmdList->writeBuffer(lightingCB, &lightingData, sizeof(lightingData)); @@ -242,112 +350,151 @@ framegraph::DefaultOutputLayout setupTransparentPass( if (!vsReflection || !psReflection) return; - framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Transparent"); - bsb.ConstantBuffer("static_globals", staticGlobalsCB); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); - bsb.BufferSRV("g_InstanceData", cfg.instanceBuffer); - bsb.BufferSRV("g_CompactBatchIndices", cfg.compactBatchIndicesBuffer); - bsb.BufferSRV("g_CompactMaterialIDs", cfg.compactMaterialIDBuffer); - bsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); - bsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); - bsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); - { - static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; - for (u32 i = 0; i < 3; ++i) - { - nvrhi::ITexture* t = cfg.shadowCascades[i] ? cfg.shadowCascades[i] - : (i == 0 && cfg.shadowMapArray ? cfg.shadowMapArray : dummy2D); - if (t) - bsb.Texture(kNames[i], t); - } - } - { - nvrhi::ITexture* contactHist = cfg.contactHistory - ? cfg.contactHistory - : passCache.GetDummyContactHistory(nvDevice); - if (contactHist) - bsb.Texture("g_ContactHistory", contactHist); - } - { - nvrhi::ITexture* localAtlas = cfg.localShadowAtlas - ? cfg.localShadowAtlas - : passCache.GetDummyShadowMap(nvDevice); - if (localAtlas) - bsb.Texture("g_LocalShadowAtlas", localAtlas); - } - if (sky0) - bsb.Texture("s_env0", sky0); - if (sky1) - bsb.Texture("s_env1", sky1); - - auto bindingSet = passCache.GetOrCreateBindingSet(bsb.Build(), data.passState->layout, nvDevice); - if (!bindingSet) - { - Msg("! [TransparentPass] Binding set create failed — skip draw"); - return; - } - - nvrhi::GraphicsState gfxState; - gfxState.pipeline = data.passState->pipeline; - gfxState.framebuffer = framebuffer; - gfxState.bindings = { bindingSet }; - auto* backend = data.device->GetBackend(); nvrhi::IBindingSet* bindlessTable = nullptr; - if (backend) { + if (backend) bindlessTable = backend->GetBindlessDescriptorTable(); + + const auto& rtDesc = colorRT->getDesc(); + nvrhi::Viewport viewport(0.0f, static_cast(rtDesc.width), 0.0f, static_cast(rtDesc.height), 0.0f, 1.0f); + + auto drawTransparentBatch = [&](nvrhi::IGraphicsPipeline* pipeline, u32 blendPass) { + if (!pipeline) + return; + + u32 passMode[4] = { blendPass, 0, 0, 0 }; + cmdList->writeBuffer(transparentDrawCB, passMode, sizeof(passMode)); + + framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Transparent"); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.ConstantBuffer("TransparentDrawCB", transparentDrawCB); + BindBindlessMaterialTables(bsb); + bsb.BufferSRV("g_InstanceData", cfg.instanceBuffer); + bsb.BufferSRV("g_CompactBatchIndices", cfg.compactBatchIndicesBuffer); + bsb.BufferSRV("g_CompactMaterialIDs", cfg.compactMaterialIDBuffer); + bsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); + if (clm.GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", clm.GetShadowDataBuffer()); + bsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); + bsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); + { + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = cfg.shadowCascades[i] ? cfg.shadowCascades[i] + : (i == 0 && cfg.shadowMapArray ? cfg.shadowMapArray : dummy2D); + if (t) + bsb.Texture(kNames[i], t); + } + } + { + nvrhi::ITexture* contactHist = cfg.contactHistory + ? cfg.contactHistory + : passCache.GetDummyContactHistory(nvDevice); + if (contactHist) + bsb.Texture("g_ContactHistory", contactHist); + } + { + nvrhi::ITexture* localAtlas = cfg.localShadowAtlas + ? cfg.localShadowAtlas + : passCache.GetDummyShadowMap(nvDevice); + if (localAtlas) + bsb.Texture("g_LocalShadowAtlas", localAtlas); + nvrhi::ITexture* localEsm = cfg.localShadowESM + ? cfg.localShadowESM + : passCache.GetDummyLocalShadowESM(nvDevice); + if (localEsm) + bsb.Texture("g_LocalShadowESM", localEsm); + } + { + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + nvrhi::ITexture* dummyHzb = passCache.GetDummyContactDepth(nvDevice); + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* hz = cfg.shadowHZB[i] ? cfg.shadowHZB[i] : dummyHzb; + if (hz) + bsb.Texture(kHzb[i], hz); + } + } + { + nvrhi::ITexture* shadowMask = cfg.shadowMask + ? cfg.shadowMask + : passCache.GetDummyContactHistory(nvDevice); + if (shadowMask) + bsb.Texture("g_ShadowMask", shadowMask); + } + if (sky0) + bsb.Texture("s_env0", sky0); + if (sky1) + bsb.Texture("s_env1", sky1); + BindIBLResources(bsb, GetCurrentIBLBindResources(), nvDevice); + + auto bindingSet = passCache.GetOrCreateBindingSet(bsb.Build(), data.passState->layout, nvDevice); + if (!bindingSet) + return; + + nvrhi::GraphicsState gfxState; + gfxState.pipeline = pipeline; + gfxState.framebuffer = framebuffer; + gfxState.bindings = { bindingSet }; if (bindlessTable) gfxState.addBindingSet(bindlessTable); - } - - gfxState.vertexBuffers = { - {cfg.megaVertexBuffer, 0, 0}, - {drawIndexBuffer, 1, 0} + gfxState.vertexBuffers = { + {cfg.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0} + }; + gfxState.indexBuffer = { cfg.megaIndexBuffer, nvrhi::Format::R32_UINT, 0 }; + gfxState.indirectParams = cfg.compactDrawArgsBuffer; + gfxState.indirectCountBuffer = cfg.compactCountBuffer; + gfxState.viewport.addViewport(viewport); + gfxState.viewport.addScissorRect(nvrhi::Rect(rtDesc.width, rtDesc.height)); + + if (cfg.variantPartition.Enabled()) { + auto* backendDev = data.device->GetBackend(); + + VariantPartitionDrawConfig vpCfg; + vpCfg.defaultPipeline = pipeline; + vpCfg.inputLayout = data.passState->inputLayout; + vpCfg.passLayout = data.passState->layout; + vpCfg.bindlessLayout = backendDev ? backendDev->GetBindlessLayout() : nullptr; + vpCfg.bindlessTable = bindlessTable; + vpCfg.sampler = data.passState->sampler + ? data.passState->sampler.Get() + : passCache.GetLinearWrapSampler(nvDevice); + vpCfg.staticGlobalsCB = staticGlobalsCB; + vpCfg.lightingCB = lightingCB; + vpCfg.materialBuffer = matBuffer.GetBuffer(); + vpCfg.variantTexBuffer = variantTexBuffer.GetBuffer(); + vpCfg.instanceBuffer = cfg.instanceBuffer; + vpCfg.megaVertexBuffer = cfg.megaVertexBuffer; + vpCfg.shadowMapArray = cfg.shadowCascades[0] ? cfg.shadowCascades[0] : cfg.shadowMapArray; + for (u32 i = 0; i < 3; ++i) + { + vpCfg.shadowCascades[i] = cfg.shadowCascades[i]; + vpCfg.shadowHZB[i] = cfg.shadowHZB[i]; + } + vpCfg.localShadowAtlas = cfg.localShadowAtlas; + vpCfg.localShadowESM = cfg.localShadowESM; + vpCfg.contactHistory = cfg.contactHistory; + vpCfg.shadowMask = cfg.shadowMask; + vpCfg.envSky0 = sky0; + vpCfg.envSky1 = sky1; + vpCfg.lightDataBuffer = clm.GetLightDataBuffer(); + vpCfg.shadowDataBuffer = clm.GetShadowDataBuffer(); + vpCfg.clusterGridBuffer = clm.GetClusterGridBuffer(); + vpCfg.lightIndexListBuffer = clm.GetLightIndexListBuffer(); + vpCfg.partition = cfg.variantPartition; + vpCfg.selectTransparent = true; + + DrawVariantPartition(cmdList, nvDevice, framebuffer, gfxState, vpCfg); + } else { + cmdList->setGraphicsState(gfxState); + DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, cfg.objectCount); + } }; - gfxState.indexBuffer = { cfg.megaIndexBuffer, nvrhi::Format::R32_UINT, 0 }; - gfxState.indirectParams = cfg.compactDrawArgsBuffer; - gfxState.indirectCountBuffer = cfg.compactCountBuffer; - const auto& rtDesc = colorRT->getDesc(); - nvrhi::Viewport viewport(0.0f, static_cast(rtDesc.width), 0.0f, static_cast(rtDesc.height), 0.0f, 1.0f); - gfxState.viewport.addViewport(viewport); - gfxState.viewport.addScissorRect(nvrhi::Rect(rtDesc.width, rtDesc.height)); - - if (cfg.variantPartition.Enabled()) { - auto* backendDev = data.device->GetBackend(); - - VariantPartitionDrawConfig vpCfg; - vpCfg.defaultPipeline = data.passState->pipeline.Get(); - vpCfg.inputLayout = data.passState->inputLayout; - vpCfg.passLayout = data.passState->layout; - vpCfg.bindlessLayout = backendDev ? backendDev->GetBindlessLayout() : nullptr; - vpCfg.bindlessTable = bindlessTable; - vpCfg.sampler = data.passState->sampler - ? data.passState->sampler.Get() - : passCache.GetLinearWrapSampler(nvDevice); - vpCfg.staticGlobalsCB = staticGlobalsCB; - vpCfg.lightingCB = lightingCB; - vpCfg.materialBuffer = matBuffer.GetBuffer(); - vpCfg.variantTexBuffer = variantTexBuffer.GetBuffer(); - vpCfg.instanceBuffer = cfg.instanceBuffer; - vpCfg.megaVertexBuffer = cfg.megaVertexBuffer; - vpCfg.shadowMapArray = cfg.shadowCascades[0] ? cfg.shadowCascades[0] : cfg.shadowMapArray; - for (u32 i = 0; i < 3; ++i) - vpCfg.shadowCascades[i] = cfg.shadowCascades[i]; - vpCfg.localShadowAtlas = cfg.localShadowAtlas; - vpCfg.envSky0 = sky0; - vpCfg.envSky1 = sky1; - vpCfg.lightDataBuffer = clm.GetLightDataBuffer(); - vpCfg.clusterGridBuffer = clm.GetClusterGridBuffer(); - vpCfg.lightIndexListBuffer = clm.GetLightIndexListBuffer(); - vpCfg.partition = cfg.variantPartition; - vpCfg.selectTransparent = true; - - DrawVariantPartition(cmdList, nvDevice, framebuffer, gfxState, vpCfg); - } else { - cmdList->setGraphicsState(gfxState); - DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, cfg.objectCount); - } + drawTransparentBatch(data.passState->pipeline.Get(), 1); + drawTransparentBatch(data.passState->multiplyPipeline.Get(), 2); // Second draw: dedicated water PSO (soft depth + foam + SSR) if (data.passState->waterPipeline && data.passState->waterLayout) @@ -371,13 +518,13 @@ framegraph::DefaultOutputLayout setupTransparentPass( data.passState->waterSsrColor = nvDevice->createTexture(td); } nvrhi::ITexture* ssrColor = data.passState->waterSsrColor; - // Unbind RT/DSV → Copy → SRV so water SSR can sample scene (Metal/MoltenVK). + nvrhi::ITexture* sceneDepthSrv = nullptr; + // Single ping-pong: unbind → copy color+depth → restore for water draw if (ssrColor) { cmdList->setTextureState(colorRT, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); cmdList->setTextureState(ssrColor, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); cmdList->copyTexture(ssrColor, nvrhi::TextureSlice(), colorRT, nvrhi::TextureSlice()); - cmdList->setTextureState(ssrColor, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); } const auto& ddesc = depthRT->getDesc(); @@ -396,7 +543,7 @@ framegraph::DefaultOutputLayout setupTransparentPass( td.keepInitialState = true; data.passState->waterSceneDepth = nvDevice->createTexture(td); } - nvrhi::ITexture* sceneDepthSrv = data.passState->waterSceneDepth; + sceneDepthSrv = data.passState->waterSceneDepth; if (sceneDepthSrv) { cmdList->setTextureState(depthRT, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); @@ -404,27 +551,54 @@ framegraph::DefaultOutputLayout setupTransparentPass( cmdList->copyTexture(sceneDepthSrv, nvrhi::TextureSlice(), depthRT, nvrhi::TextureSlice()); cmdList->setTextureState(sceneDepthSrv, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); } - // Restore attachments for the water draw that follows - cmdList->setTextureState(colorRT, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); - if (normalRT) - cmdList->setTextureState(normalRT, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); - if (baseColorRT) - cmdList->setTextureState(baseColorRT, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); + + auto* worldPosRT = data.worldPos.is_valid() ? fg.GetPhysicalTexture(data.worldPos) : nullptr; if (worldPosRT) - cmdList->setTextureState(worldPosRT, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); - // Transparent pass declares depth as read-only (test, no write) + { + const auto& wpDesc = worldPosRT->getDesc(); + if (!data.passState->waterSceneWorldPos || + data.passState->waterSceneWorldPos->getDesc().width != wpDesc.width || + data.passState->waterSceneWorldPos->getDesc().height != wpDesc.height) + { + nvrhi::TextureDesc td = wpDesc; + td.debugName = "WaterSoft_WorldPos"; + td.isRenderTarget = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + data.passState->waterSceneWorldPos = nvDevice->createTexture(td); + } + if (data.passState->waterSceneWorldPos) + { + nvrhi::ITexture* wpCopy = data.passState->waterSceneWorldPos; + cmdList->setTextureState( + worldPosRT, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmdList->setTextureState( + wpCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmdList->copyTexture( + wpCopy, nvrhi::TextureSlice(), worldPosRT, nvrhi::TextureSlice()); + cmdList->setTextureState( + wpCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + } + + if (ssrColor) + cmdList->setTextureState(ssrColor, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmdList->setTextureState(colorRT, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); cmdList->setTextureState(depthRT, nvrhi::AllSubresources, nvrhi::ResourceStates::DepthRead); + if (worldPosRT) + cmdList->setTextureState( + worldPosRT, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); auto waterCB = cache.GetOrCreateVolatileCB( "TransparentWater", "WaterParams", sizeof(Fvector4), data.device); Fvector4 wi{}; - wi.set(1.f, 1.f, 1.f, 1.f); + float intens = 1.f; if (g_pGamePersistent) - { - const auto& env = g_pGamePersistent->Environment().CurrentEnv; - float intens = std::max(env.fog_color.x, std::max(env.fog_color.y, env.fog_color.z)); - wi.set(std::max(intens, 0.35f), intens, intens, 1.f); - } + intens = g_pGamePersistent->Environment().CurrentEnv.m_fWaterIntensity; + intens = std::clamp(intens, 0.f, 1.f); + const float softOn = ps_r2_ls_flags.test(R2FLAG_SOFT_WATER) ? 1.f : 0.f; + wi.set(intens, intens, intens, softOn); cmdList->writeBuffer(waterCB, &wi, sizeof(wi)); nvrhi::ITexture* foam = data.passState->foamTexture; @@ -433,9 +607,9 @@ framegraph::DefaultOutputLayout setupTransparentPass( framegraph::BindingSetBuilder wbsb(*waterVsRefl, *waterPsRefl, nvDevice, "Transparent.Water"); wbsb.ConstantBuffer("static_globals", staticGlobalsCB) - .ConstantBuffer("WaterParams", waterCB) - .BufferSRV("g_Materials", matBuffer.GetBuffer()) - .BufferSRV("g_InstanceData", cfg.instanceBuffer) + .ConstantBuffer("WaterParams", waterCB); + BindBindlessMaterialTables(wbsb); + wbsb.BufferSRV("g_InstanceData", cfg.instanceBuffer) .BufferSRV("g_CompactBatchIndices", cfg.compactBatchIndicesBuffer) .BufferSRV("g_CompactMaterialIDs", cfg.compactMaterialIDBuffer); if (sky0) wbsb.Texture("s_env0", sky0); @@ -443,19 +617,98 @@ framegraph::DefaultOutputLayout setupTransparentPass( if (foam) wbsb.Texture("s_leaves", foam); if (ssrColor) wbsb.Texture("g_SceneColor", ssrColor); if (sceneDepthSrv) wbsb.Texture("g_SceneDepth", sceneDepthSrv); + else wbsb.Texture("g_SceneDepth", passCache.GetDummyContactDepth(nvDevice)); + if (data.passState->waterSceneWorldPos) + wbsb.Texture("g_UnderWorldPos", data.passState->waterSceneWorldPos); + else + wbsb.Texture("g_UnderWorldPos", passCache.GetDummyContactHistory(nvDevice)); auto waterSet = passCache.GetOrCreateBindingSet( wbsb.Build(), data.passState->waterLayout, nvDevice); if (waterSet) { - nvrhi::GraphicsState waterGfx = gfxState; + nvrhi::GraphicsState waterGfx; waterGfx.pipeline = data.passState->waterPipeline; + waterGfx.framebuffer = framebuffer; waterGfx.bindings = {waterSet}; if (bindlessTable) waterGfx.addBindingSet(bindlessTable); + waterGfx.vertexBuffers = { + {cfg.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0} + }; + waterGfx.indexBuffer = { cfg.megaIndexBuffer, nvrhi::Format::R32_UINT, 0 }; + waterGfx.indirectParams = cfg.compactDrawArgsBuffer; + waterGfx.indirectCountBuffer = cfg.compactCountBuffer; + waterGfx.viewport.addViewport(viewport); + waterGfx.viewport.addScissorRect(nvrhi::Rect(rtDesc.width, rtDesc.height)); cmdList->setGraphicsState(waterGfx); DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, cfg.objectCount); } + + if (data.passState->waterDistortPipeline && data.passState->waterDistortLayout && + data.distortion.is_valid()) + { + auto* distortRT = fg.GetPhysicalTexture(data.distortion); + auto* waterdVsRefl = shaderLoader->GetCachedReflection("waterd", ".vs"); + auto* waterdPsRefl = shaderLoader->GetCachedReflection("waterd", ".ps"); + if (distortRT && waterdVsRefl && waterdPsRefl) + { + nvrhi::FramebufferDesc distortFbDesc; + distortFbDesc.addColorAttachment(distortRT); + distortFbDesc.setDepthAttachment(depthRT); + auto distortFB = passCache.GetOrCreateFramebuffer( + "TransparentWaterDistort", distortFbDesc, nvDevice); + if (distortFB) + { + framegraph::BindingSetBuilder dbsb( + *waterdVsRefl, *waterdPsRefl, nvDevice, "Transparent.WaterDistort"); + dbsb.ConstantBuffer("static_globals", staticGlobalsCB) + .ConstantBuffer("WaterParams", waterCB); + BindBindlessMaterialTables(dbsb); + dbsb.BufferSRV("g_InstanceData", cfg.instanceBuffer) + .BufferSRV("g_CompactBatchIndices", cfg.compactBatchIndicesBuffer) + .BufferSRV("g_CompactMaterialIDs", cfg.compactMaterialIDBuffer); + if (sceneDepthSrv) + dbsb.Texture("g_SceneDepth", sceneDepthSrv); + else + dbsb.Texture("g_SceneDepth", passCache.GetDummyContactDepth(nvDevice)); + dbsb.Texture("g_SceneColor", ssrColor + ? ssrColor + : passCache.GetDummyContactHistory(nvDevice)); + if (data.passState->waterSceneWorldPos) + dbsb.Texture("g_UnderWorldPos", data.passState->waterSceneWorldPos); + else + dbsb.Texture("g_UnderWorldPos", passCache.GetDummyContactHistory(nvDevice)); + + auto distortSet = passCache.GetOrCreateBindingSet( + dbsb.Build(), data.passState->waterDistortLayout, nvDevice); + if (distortSet) + { + nvrhi::GraphicsState dgfx; + dgfx.pipeline = data.passState->waterDistortPipeline; + dgfx.framebuffer = distortFB; + dgfx.bindings = {distortSet}; + if (bindlessTable) + dgfx.addBindingSet(bindlessTable); + dgfx.vertexBuffers = { + {cfg.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0} + }; + dgfx.indexBuffer = { + cfg.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + dgfx.indirectParams = cfg.compactDrawArgsBuffer; + dgfx.indirectCountBuffer = cfg.compactCountBuffer; + dgfx.viewport.addViewport(viewport); + dgfx.viewport.addScissorRect( + nvrhi::Rect(rtDesc.width, rtDesc.height)); + cmdList->setGraphicsState(dgfx); + DrawIndexedIndirectCountOrFallback( + cmdList, 0, 0, cfg.objectCount); + } + } + } + } } } } @@ -467,6 +720,7 @@ framegraph::DefaultOutputLayout setupTransparentPass( outputs.baseColor = passData.baseColor; outputs.worldPos = passData.worldPos; outputs.depth = passData.depth; + outputs.distortion = passData.distortion; return outputs; } diff --git a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.h index c7c0549e41e..8b2e64356b8 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.h @@ -34,10 +34,15 @@ struct TransparentPassConfig { nvrhi::ITexture* shadowMapArray = nullptr; nvrhi::ITexture* shadowCascades[3] = {}; nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* localShadowESM = nullptr; nvrhi::ITexture* envSky0 = nullptr; nvrhi::ITexture* envSky1 = nullptr; nvrhi::ITexture* contactDepth = nullptr; // unused (history-only contact) nvrhi::ITexture* contactHistory = nullptr; // g_ContactHistory @ t28 + nvrhi::ITexture* shadowHZB[3] = {}; + framegraph::VirtualResourceHandle shadowHZBHandles[3]; + nvrhi::ITexture* shadowMask = nullptr; + framegraph::VirtualResourceHandle shadowMaskHandle; bool IsValid() const { return objectCount > 0 && compactDrawArgsBuffer && megaVertexBuffer && megaIndexBuffer; @@ -46,18 +51,25 @@ struct TransparentPassConfig { struct TransparentPassState { nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::GraphicsPipelineHandle multiplyPipeline; nvrhi::BindingLayoutHandle layout; nvrhi::GraphicsPipelineHandle waterPipeline; nvrhi::BindingLayoutHandle waterLayout; + nvrhi::GraphicsPipelineHandle waterDistortPipeline; + nvrhi::BindingLayoutHandle waterDistortLayout; nvrhi::InputLayoutHandle inputLayout; + nvrhi::InputLayoutHandle waterDistortInputLayout; nvrhi::SamplerHandle sampler; nvrhi::ShaderHandle vs; nvrhi::ShaderHandle ps; nvrhi::ShaderHandle waterVs; nvrhi::ShaderHandle waterPs; + nvrhi::ShaderHandle waterDistortVs; + nvrhi::ShaderHandle waterDistortPs; nvrhi::ITexture* foamTexture = nullptr; - nvrhi::TextureHandle waterSsrColor; // opaque HDR snapshot for SSR - nvrhi::TextureHandle waterSceneDepth; // depth copy (cannot sample DSV while attached) + nvrhi::TextureHandle waterSsrColor; + nvrhi::TextureHandle waterSceneDepth; + nvrhi::TextureHandle waterSceneWorldPos; bool initialized = false; }; @@ -67,6 +79,7 @@ struct TransparentPassData { framegraph::VirtualResourceHandle normal; framegraph::VirtualResourceHandle baseColor; framegraph::VirtualResourceHandle worldPos; + framegraph::VirtualResourceHandle distortion; fg::RenderDevice* device; TransparentPassConfig config; TransparentPassState* passState; diff --git a/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.cpp index f37510cc5be..863097abb34 100644 --- a/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.cpp @@ -1,7 +1,12 @@ #include "stdafx.h" #include "VolumetricPassSetup.h" #include "ParticlePassSetup.h" +#include "ShaderConstants.h" +#include "TAAPassSetup.h" +#include "SunShaftsPassSetup.h" #include "Layers/xrRender/Volumetrics/VolumetricRenderer.h" +#include "Layers/xrRender/Volumetrics/IVolumetricSource.h" +#include "Layers/xrRender/ClusteredLightManager.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" #include "Layers/xrRender/FrameGraph/PassResourceCache.h" @@ -11,9 +16,12 @@ #include "Layers/xrRender/RenderContext/RenderDevice.h" #include "Layers/xrRender/xrRender_console.h" #include "xrEngine/IGame_Persistent.h" +#include "xrEngine/IGame_Level.h" #include "xrEngine/Environment.h" #include "xrEngine/device.h" +extern ENGINE_API int ps_r_upscale; + namespace fg { extern xray::render::FrameGraphRenderer RImplementation; @@ -53,7 +61,7 @@ void FillFroxelParams(FroxelParamsCB& cb, const VolumetricRenderer& vol) cb.sunDir.set(-env.sun_dir.x, -env.sun_dir.y, -env.sun_dir.z); cb.sunDir.normalize_safe(); cb.sunColor.set(env.sun_color.x, env.sun_color.y, env.sun_color.z); - cb.sunIntensity = std::max(env.m_fSunShaftsIntensity, 0.15f); + cb.sunIntensity = ResolveSunShaftsIntensity(); const float n = env.fog_near; const float f = std::max(env.fog_far, n + 1.0f); @@ -97,6 +105,49 @@ nvrhi::ComputePipelineHandle LoadComputePipe( return cache.GetOrCreateComputePipeline(passName, desc, device); } +void BindVolumetricLighting( + BindingSetBuilder& bsb, + nvrhi::IDevice* nv, + nvrhi::IBuffer* staticGlobalsCB, + nvrhi::IBuffer* paramsCB, + nvrhi::ITexture* froxel, + const VolumetricLightingInputs& lighting) +{ + bsb.ConstantBuffer("static_globals", staticGlobalsCB) + .ConstantBuffer("FroxelParams", paramsCB) + .TextureUAV("u_FroxelVolume", froxel); + + auto& cache = GetPassResourceCache(); + nvrhi::ITexture* dummy2D = cache.GetDummyShadowMap2D(nv); + nvrhi::ITexture* dummyHzb = cache.GetDummyContactDepth(nv); + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = lighting.shadowCascades[i] + ? lighting.shadowCascades[i] + : (i == 0 && lighting.shadowMapArray ? lighting.shadowMapArray : dummy2D); + if (t) + bsb.Texture(kNames[i], t); + nvrhi::ITexture* hz = lighting.shadowHZB[i] ? lighting.shadowHZB[i] : dummyHzb; + if (hz) + bsb.Texture(kHzb[i], hz); + } + nvrhi::ITexture* contactHist = cache.GetDummyContactHistory(nv); + if (contactHist) + bsb.Texture("g_ContactHistory", contactHist); + + if (lighting.lightManager) + { + if (auto* lights = lighting.lightManager->GetLightDataBuffer()) + bsb.BufferSRV("g_LightData", lights); + if (auto* grid = lighting.lightManager->GetClusterGridBuffer()) + bsb.BufferSRV("g_ClusterGrid", grid); + if (auto* idx = lighting.lightManager->GetLightIndexListBuffer()) + bsb.BufferSRV("g_LightIndexList", idx); + } +} + } // namespace void InitializeVolumetricPass(nvrhi::IDevice* device, VolumetricPassState& state) @@ -108,6 +159,7 @@ void InitializeVolumetricPass(nvrhi::IDevice* device, VolumetricPassState& state state.fogPipeline = LoadComputePipe("VolFog", "volumetric\\world_fog", device, state.fogLayout); state.lightPipeline = LoadComputePipe("VolLight", "volumetric\\apply_lighting", device, state.lightLayout); state.particlePipeline = LoadComputePipe("VolParticle", "volumetric\\particle_inject", device, state.particleLayout); + state.lightShaftPipeline = LoadComputePipe("VolLightShaft", "volumetric\\light_shafts", device, state.lightShaftLayout); state.temporalPipeline = LoadComputePipe("VolTemporal", "volumetric\\temporal_reproject", device, state.temporalLayout); auto* loader = GEnv.Render->GetShaderLoader(); @@ -178,6 +230,15 @@ void InitializeVolumetricPass(nvrhi::IDevice* device, VolumetricPassState& state bd.debugName = "VolParticlePoints"; state.particlePointsCB = device->createBuffer(bd); } + { + nvrhi::BufferDesc cb; + cb.byteSize = sizeof(LightShaftParamsCB); + cb.isConstantBuffer = true; + cb.isVolatile = true; + cb.maxVersions = 16; + cb.debugName = "VolLightShaftCB"; + state.lightShaftCB = device->createBuffer(cb); + } { nvrhi::SamplerDesc s; @@ -190,10 +251,11 @@ void InitializeVolumetricPass(nvrhi::IDevice* device, VolumetricPassState& state state.marchPipeline && state.paramsCB; state.initialized = true; - Msg("* [VolumetricPass] Init: compute=%s temporal=%s particles=%s", + Msg("* [VolumetricPass] Init: compute=%s temporal=%s particles=%s shafts=%s", state.computeEnabled ? "OK" : "FAIL", state.temporalPipeline ? "OK" : "off", - state.particlePipeline ? "OK" : "off"); + state.particlePipeline ? "OK" : "off", + state.lightShaftPipeline ? "OK" : "off"); } void ShutdownVolumetricPass(VolumetricPassState& state) @@ -202,12 +264,14 @@ void ShutdownVolumetricPass(VolumetricPassState& state) state.fogPipeline = nullptr; state.lightPipeline = nullptr; state.particlePipeline = nullptr; + state.lightShaftPipeline = nullptr; state.temporalPipeline = nullptr; state.marchPipeline = nullptr; state.clearLayout = nullptr; state.fogLayout = nullptr; state.lightLayout = nullptr; state.particleLayout = nullptr; + state.lightShaftLayout = nullptr; state.temporalLayout = nullptr; state.marchLayout = nullptr; state.linearSampler = nullptr; @@ -215,6 +279,7 @@ void ShutdownVolumetricPass(VolumetricPassState& state) state.temporalCB = nullptr; state.particleParamsCB = nullptr; state.particlePointsCB = nullptr; + state.lightShaftCB = nullptr; state.hasPrevVP = false; state.initialized = false; state.computeEnabled = false; @@ -229,7 +294,8 @@ VirtualResourceHandle setupVolumetricPass( u32 width, u32 height, VolumetricPassState& state, - const xr_vector* particleBatches) + const xr_vector* particleBatches, + const VolumetricLightingInputs* lighting) { if (!volumetric || !volumetric->IsReady() || !device) return sceneColor; @@ -242,6 +308,10 @@ VirtualResourceHandle setupVolumetricPass( volumetric->BeginFrame(); + VolumetricLightingInputs lightingCopy{}; + if (lighting) + lightingCopy = *lighting; + nvrhi::ITexture* froxelTex = volumetric->GetFroxelVolume(); ResourceDesc froxelDesc; froxelDesc.type = ResourceDesc::Type::Texture3D; @@ -265,7 +335,7 @@ VirtualResourceHandle setupVolumetricPass( auto& passData = fg.addCallbackPass( "VolumetricFog", - [&, sceneColor, depth, froxelHandle, width, height, volumetric, particleBatches]( + [&, sceneColor, depth, froxelHandle, width, height, volumetric, particleBatches, lightingCopy]( FrameGraph& builder, PassHandle passHandle, VolumetricPassData& data) { RenderPassBuilder pb(builder, passHandle); data.sceneInput = pb.read(sceneColor, ResourceState::ShaderResource); @@ -275,6 +345,7 @@ VirtualResourceHandle setupVolumetricPass( data.volumetric = volumetric; data.passState = &state; data.particleBatches = particleBatches; + data.lighting = lightingCopy; data.width = width; data.height = height; }, @@ -302,6 +373,7 @@ VirtualResourceHandle setupVolumetricPass( const u32 gy = (VolumetricRenderer::kFroxelY + 7) / 8; const u32 gz = VolumetricRenderer::kFroxelZ; + auto& cache = GetPassResourceCache(); auto dispatchUAV = [&](nvrhi::IComputePipeline* pipe, nvrhi::IBindingLayout* layout) { if (!pipe || !layout) return; @@ -310,7 +382,7 @@ VirtualResourceHandle setupVolumetricPass( nvrhi::BindingSetItem::ConstantBuffer(5, st.paramsCB), nvrhi::BindingSetItem::Texture_UAV(0, froxel), }; - auto bs = nv->createBindingSet(bsDesc, layout); + auto bs = cache.GetOrCreateBindingSet(bsDesc, layout, nv); if (!bs) return; nvrhi::ComputeState cs; @@ -322,26 +394,71 @@ VirtualResourceHandle setupVolumetricPass( cmd->setTextureState(froxel, nvrhi::TextureSubresourceSet{}, nvrhi::ResourceStates::UnorderedAccess); dispatchUAV(st.clearPipeline, st.clearLayout); - dispatchUAV(st.fogPipeline, st.fogLayout); - // Particle density inject (batch centers as smoke blobs) - if (st.particlePipeline && st.particleLayout && st.particlePointsCB && - st.particleParamsCB && data.particleBatches && !data.particleBatches->empty()) + auto* loader = GEnv.Render->GetShaderLoader(); + const auto& sources = vol.GetSources(); + for (auto* source : sources) { - ParticleInjectPointGPU points[256]; - u32 count = 0; - for (const auto& batch : *data.particleBatches) + if (!source) + continue; + const char* shaderName = source->GetShaderName(); + if (!shaderName) + continue; + + if (strstr(shaderName, "world_fog")) + { + dispatchUAV(st.fogPipeline, st.fogLayout); + } + else if (strstr(shaderName, "light_shafts") && st.lightShaftPipeline && st.lightShaftLayout && st.lightShaftCB) { - if (count >= 256 || batch.particleCount == 0 || batch.isHUDMode) + LightShaftParamsCB shaft{}; + shaft.sunDir = params.sunDir; + shaft.intensity = params.sunIntensity; + shaft.albedo = params.fogAlbedo; + shaft.densityBoost = params.fogDensity * 0.5f; + shaft.intensity = ResolveSunShaftsIntensity(); + if (shaft.intensity < 1e-4f) continue; - auto& p = points[count++]; - p.position = batch.worldMatrix.c; - p.radius = 1.5f + 0.02f * float(std::min(batch.particleCount, 64u)); - p.albedo.set(0.7f, 0.7f, 0.75f); - p.density = 0.08f * float(std::min(batch.particleCount, 32u)); + cmd->writeBuffer(st.lightShaftCB, &shaft, sizeof(shaft)); + + nvrhi::BindingSetDesc bsDesc; + bsDesc.bindings = { + nvrhi::BindingSetItem::ConstantBuffer(5, st.paramsCB), + nvrhi::BindingSetItem::ConstantBuffer(6, st.lightShaftCB), + nvrhi::BindingSetItem::Texture_UAV(0, froxel), + }; + auto bs = cache.GetOrCreateBindingSet(bsDesc, st.lightShaftLayout, nv); + if (bs) + { + nvrhi::ComputeState cs; + cs.pipeline = st.lightShaftPipeline; + cs.bindings = {bs}; + cmd->setComputeState(cs); + cmd->dispatch(gx, gy, gz); + } } - if (count > 0) + else if (strstr(shaderName, "particle") && st.particlePipeline && st.particleLayout && + st.particlePointsCB && st.particleParamsCB) { + ParticleInjectPointGPU points[256]; + u32 count = 0; + if (data.particleBatches) + { + for (const auto& batch : *data.particleBatches) + { + if (count >= 256 || batch.particleCount == 0 || batch.isHUDMode) + continue; + if (batch.lightingMode != PS::ParticleLightingMode::Volumetric) + continue; + auto& p = points[count++]; + p.position = batch.worldMatrix.c; + p.radius = 1.5f + 0.02f * float(std::min(batch.particleCount, 64u)); + p.albedo.set(0.7f, 0.7f, 0.75f); + p.density = 0.08f * float(std::min(batch.particleCount, 32u)); + } + } + if (count == 0) + continue; cmd->writeBuffer(st.particlePointsCB, points, sizeof(ParticleInjectPointGPU) * count); ParticleInjectParamsCB pp{}; pp.particleCount = count; @@ -354,7 +471,7 @@ VirtualResourceHandle setupVolumetricPass( nvrhi::BindingSetItem::StructuredBuffer_SRV(0, st.particlePointsCB), nvrhi::BindingSetItem::Texture_UAV(0, froxel), }; - auto bs = nv->createBindingSet(bsDesc, st.particleLayout); + auto bs = cache.GetOrCreateBindingSet(bsDesc, st.particleLayout, nv); if (bs) { nvrhi::ComputeState cs; @@ -366,16 +483,39 @@ VirtualResourceHandle setupVolumetricPass( } } - dispatchUAV(st.lightPipeline, st.lightLayout); + if (st.lightPipeline && st.lightLayout && loader) + { + auto* csRefl = loader->GetCachedReflection("volumetric\\apply_lighting", ".cs"); + if (csRefl) + { + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(StaticGlobals), ctx->GetDevice()); + StaticGlobals sg = BuildStaticGlobals(); + cmd->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + + BindingSetBuilder bsb(*csRefl, nv, "VolLight"); + BindVolumetricLighting(bsb, nv, staticGlobalsCB, st.paramsCB, froxel, data.lighting); + auto bs = cache.GetOrCreateBindingSet(bsb.Build(), st.lightLayout, nv); + if (bs) + { + nvrhi::ComputeState cs; + cs.pipeline = st.lightPipeline; + cs.bindings = {bs}; + cmd->setComputeState(cs); + cmd->dispatch(gx, gy, gz); + } + } + } - // Temporal accumulate from previous frame nvrhi::ITexture* prevFroxel = vol.GetPrevFroxelVolume(); - if (st.temporalPipeline && st.temporalLayout && st.temporalCB && prevFroxel && + const bool allowVolTemporal = ps_r_upscale == 0; + if (allowVolTemporal && st.temporalPipeline && st.temporalLayout && st.temporalCB && prevFroxel && vol.HasFroxelHistory() && st.hasPrevVP) { TemporalParamsCB tcb{}; tcb.prevVP = st.prevVP; tcb.temporalAlpha = 0.88f; + tcb.frameIndex = Device.dwFrame; cmd->writeBuffer(st.temporalCB, &tcb, sizeof(tcb)); cmd->setTextureState(prevFroxel, nvrhi::TextureSubresourceSet{}, @@ -389,7 +529,7 @@ VirtualResourceHandle setupVolumetricPass( nvrhi::BindingSetItem::Texture_UAV(0, froxel), nvrhi::BindingSetItem::Sampler(0, st.linearSampler), }; - auto bs = nv->createBindingSet(bsDesc, st.temporalLayout); + auto bs = cache.GetOrCreateBindingSet(bsDesc, st.temporalLayout, nv); if (bs) { nvrhi::ComputeState cs; @@ -400,7 +540,6 @@ VirtualResourceHandle setupVolumetricPass( } } - // Copy current → prev for next frame if (prevFroxel) { cmd->setTextureState(froxel, nvrhi::TextureSubresourceSet{}, @@ -411,7 +550,7 @@ VirtualResourceHandle setupVolumetricPass( vol.SwapFroxelHistory(); } - st.prevVP = Device.mFullTransform; + st.prevVP = g_taa_unjittered_full_transform; st.hasPrevVP = true; cmd->setTextureState(froxel, nvrhi::TextureSubresourceSet{}, nvrhi::ResourceStates::ShaderResource); @@ -432,7 +571,7 @@ VirtualResourceHandle setupVolumetricPass( nvrhi::BindingSetItem::Texture_SRV(2, froxel), nvrhi::BindingSetItem::Sampler(0, st.linearSampler), }; - auto marchSet = nv->createBindingSet(marchBs, st.marchLayout); + auto marchSet = cache.GetOrCreateBindingSet(marchBs, st.marchLayout, nv); if (!marchSet) return; diff --git a/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.h index ab3fdbee3f4..efd03409c25 100644 --- a/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.h @@ -13,14 +13,14 @@ namespace xray::render::fg { class RenderDevice; class VolumetricRenderer; +class ClusteredLightManager; } namespace xray::render::fg::passes { -struct ParticleBatch; // ParticlePassSetup.h +struct ParticleBatch; -// Must match volumetric/shared_froxel.h FroxelParams (b5) struct alignas(16) FroxelParamsCB { u32 dims[3]; @@ -51,7 +51,8 @@ struct alignas(16) TemporalParamsCB { Fmatrix prevVP; float temporalAlpha; - float pad[3]; + u32 frameIndex; + float pad[2]; }; static_assert(sizeof(TemporalParamsCB) % 16 == 0); @@ -70,18 +71,29 @@ struct alignas(16) ParticleInjectParamsCB u32 pad[3]; }; +struct alignas(16) LightShaftParamsCB +{ + Fvector sunDir; + float intensity; + Fvector albedo; + float densityBoost; +}; +static_assert(sizeof(LightShaftParamsCB) % 16 == 0); + struct VolumetricPassState { nvrhi::ComputePipelineHandle clearPipeline; nvrhi::ComputePipelineHandle fogPipeline; nvrhi::ComputePipelineHandle lightPipeline; nvrhi::ComputePipelineHandle particlePipeline; + nvrhi::ComputePipelineHandle lightShaftPipeline; nvrhi::ComputePipelineHandle temporalPipeline; nvrhi::GraphicsPipelineHandle marchPipeline; nvrhi::BindingLayoutHandle clearLayout; nvrhi::BindingLayoutHandle fogLayout; nvrhi::BindingLayoutHandle lightLayout; nvrhi::BindingLayoutHandle particleLayout; + nvrhi::BindingLayoutHandle lightShaftLayout; nvrhi::BindingLayoutHandle temporalLayout; nvrhi::BindingLayoutHandle marchLayout; nvrhi::SamplerHandle linearSampler; @@ -89,12 +101,21 @@ struct VolumetricPassState nvrhi::BufferHandle temporalCB; nvrhi::BufferHandle particleParamsCB; nvrhi::BufferHandle particlePointsCB; + nvrhi::BufferHandle lightShaftCB; Fmatrix prevVP; bool hasPrevVP = false; bool initialized = false; bool computeEnabled = false; }; +struct VolumetricLightingInputs +{ + nvrhi::ITexture* shadowMapArray = nullptr; + nvrhi::ITexture* shadowCascades[3] = {}; + nvrhi::ITexture* shadowHZB[3] = {}; + ClusteredLightManager* lightManager = nullptr; +}; + struct VolumetricPassData { framegraph::VirtualResourceHandle sceneInput; @@ -104,6 +125,7 @@ struct VolumetricPassData VolumetricRenderer* volumetric = nullptr; VolumetricPassState* passState = nullptr; const xr_vector* particleBatches = nullptr; + VolumetricLightingInputs lighting{}; u32 width = 0; u32 height = 0; }; @@ -117,7 +139,8 @@ framegraph::VirtualResourceHandle setupVolumetricPass( u32 width, u32 height, VolumetricPassState& state, - const xr_vector* particleBatches = nullptr); + const xr_vector* particleBatches = nullptr, + const VolumetricLightingInputs* lighting = nullptr); void InitializeVolumetricPass(nvrhi::IDevice* device, VolumetricPassState& state); void ShutdownVolumetricPass(VolumetricPassState& state); diff --git a/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.cpp index 7ae6a137ed3..4b5c8deaadf 100644 --- a/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.cpp @@ -10,9 +10,13 @@ #include "Layers/xrRender/ResourceManager/FGResourceManager.h" #include "Layers/xrRender/ResourceManager/TextureManager.h" #include "Layers/xrRender/xrRender_console.h" +#include "Layers/xrRender/FrameGraphPasses/TAAPassSetup.h" #include "xrEngine/Environment.h" #include "xrEngine/IGame_Persistent.h" +extern ENGINE_API int ps_r_rt_gi; +extern ENGINE_API int ps_r_path_tracer; + namespace xray::render::fg::passes { using namespace framegraph; @@ -34,6 +38,7 @@ struct WetPassData VirtualResourceHandle colorIn; VirtualResourceHandle normal; VirtualResourceHandle worldPos; + VirtualResourceHandle baseColor; VirtualResourceHandle patched; VirtualResourceHandle colorOut; VirtualResourceHandle normalOut; @@ -45,6 +50,7 @@ struct WetPassData WetSurfacesExtras extras; bool hasRainSM = false; bool hasSceneReflection = false; + bool hasBaseColor = false; }; static void BlitColor(nvrhi::ICommandList* cmdList, nvrhi::ITexture* dst, nvrhi::ITexture* src) @@ -64,7 +70,10 @@ static WetConstantsGPU MakeWetConstants(const Fmatrix& rainSampleVP) ? g_pGamePersistent->Environment().CurrentEnv.rain_density : 0.f; wet.RainDensity.set(rainDensity, rainDensity, 0.f, 0.f); - wet.EyePos.set(Device.vCameraPosition.x, Device.vCameraPosition.y, Device.vCameraPosition.z, 0.f); + const bool rtNoDistFade = (ps_r_rt_gi != 0) || (ps_r_path_tracer != 0); + wet.EyePos.set( + Device.vCameraPosition.x, Device.vCameraPosition.y, Device.vCameraPosition.z, + rtNoDistFade ? 1.f : 0.f); if (g_pGamePersistent) { const auto& env = g_pGamePersistent->Environment().CurrentEnv; @@ -72,12 +81,12 @@ static WetConstantsGPU MakeWetConstants(const Fmatrix& rainSampleVP) } const float t = Device.fTimeGlobal; wet.Timers.set(t, t * 10.f, t / 10.f, _sin(t)); - wet.m_VP = Device.mFullTransform; + wet.m_VP = g_taa_unjittered_full_transform; wet.m_RainSampleVP = rainSampleVP; return wet; } -constexpr u32 kWetPipeVersion = 7; +constexpr u32 kWetPipeVersion = 25; void InitializeWetSurfacesPass(nvrhi::IDevice* device, WetSurfacesPassState& state) { @@ -109,12 +118,12 @@ void InitializeWetSurfacesPass(nvrhi::IDevice* device, WetSurfacesPassState& sta auto& cache = GetPassResourceCache(); state.layout = cache.GetOrCreateBindingLayoutFromReflection( - "WetApply_v7", *vs.reflection, *applyPs.reflection, device); + "WetApply_v25_SpecUnion", *vs.reflection, *applyPs.reflection, device); if (patchPs.handle && patchPs.reflection) { state.patchLayout = cache.GetOrCreateBindingLayoutFromReflection( - "WetPatch_v7", *vs.reflection, *patchPs.reflection, device); + "WetPatch_v24", *vs.reflection, *patchPs.reflection, device); } else { @@ -124,7 +133,7 @@ void InitializeWetSurfacesPass(nvrhi::IDevice* device, WetSurfacesPassState& sta if (writePs.handle && writePs.reflection) { state.writeNormalLayout = cache.GetOrCreateBindingLayoutFromReflection( - "WetWriteNormal_v7", *vs.reflection, *writePs.reflection, device); + "WetWriteNormal_v24", *vs.reflection, *writePs.reflection, device); } nvrhi::FramebufferInfoEx fbInfo; @@ -145,21 +154,59 @@ void InitializeWetSurfacesPass(nvrhi::IDevice* device, WetSurfacesPassState& sta pipe = cache.GetOrCreatePipeline(name, desc, fbInfo, device); }; - makePipe(applyPs, state.layout, state.pipeline, "WetApply_v7"); - makePipe(patchPs, state.patchLayout, state.patchPipeline, "WetPatch_v7"); - makePipe(writePs, state.writeNormalLayout, state.writeNormalPipeline, "WetWriteNormal_v7"); + makePipe(applyPs, state.layout, state.pipeline, "WetApply_v25_SpecUnion"); + makePipe(patchPs, state.patchLayout, state.patchPipeline, "WetPatch_v24"); + makePipe(writePs, state.writeNormalLayout, state.writeNormalPipeline, "WetWriteNormal_v24"); state.initialized = true; state.pipeVersion = kWetPipeVersion; if (state.pipeline && state.layout) { - Msg("* [WetSurfaces] Rebuild v9: apply=%d patch=%d write=%d (SSR-stable normals, non-transient RT)", + Msg("* [WetSurfaces] Rebuild v25: apply=%d patch=%d write=%d", !!state.pipeline, !!state.patchPipeline, !!state.writeNormalPipeline); } else Msg("! [WetSurfaces] Pipeline create failed — wet disabled"); } +namespace +{ +bool EnsureWetTarget( + nvrhi::IDevice* nvDevice, + nvrhi::TextureHandle& slot, + u32 width, + u32 height, + const char* name) +{ + if (!nvDevice || width == 0 || height == 0) + return false; + + if (slot) + { + const auto& d = slot->getDesc(); + if (d.width == width && d.height == height) + return true; + } + + nvrhi::TextureDesc desc; + desc.width = width; + desc.height = height; + desc.format = nvrhi::Format::RGBA16_FLOAT; + desc.isRenderTarget = true; + desc.isShaderResource = true; + desc.debugName = name; + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + slot = nvDevice->createTexture(desc); + if (!slot) + { + Msg("! [WetSurfaces] Failed to create %s %ux%u RGBA16F", name, width, height); + return false; + } + return true; +} +} // namespace + DefaultOutputLayout setupWetSurfacesPass( FrameGraph& fg, fg::RenderDevice* device, @@ -173,6 +220,7 @@ DefaultOutputLayout setupWetSurfacesPass( static bool s_loggedNoFlag = false; static bool s_loggedMissingInputs = false; static bool s_loggedNoPipelines = false; + static bool s_loggedNoTargets = false; if (!ps_r2_ls_flags.test(R3FLAG_DYN_WET_SURF)) { @@ -187,7 +235,6 @@ DefaultOutputLayout setupWetSurfacesPass( const float rainDensity = g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.rain_density : 0.f; - // Rain-only (SSFX/CoP behavior) — no wet rewrite in dry weather if (rainDensity < 0.001f) return outputs; @@ -204,8 +251,11 @@ DefaultOutputLayout setupWetSurfacesPass( return outputs; } - if (device && device->GetNVRHIDevice()) - InitializeWetSurfacesPass(device->GetNVRHIDevice(), passState); + if (!device || !device->GetNVRHIDevice()) + return outputs; + + nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); + InitializeWetSurfacesPass(nvDevice, passState); if (!passState.pipeline || !passState.layout) { @@ -218,28 +268,57 @@ DefaultOutputLayout setupWetSurfacesPass( return outputs; } + const bool doWriteNormal = passState.writeNormalPipeline && passState.writeNormalLayout + && passState.patchPipeline && passState.patchLayout; + + const bool needResize = passState.texWidth != width || passState.texHeight != height; + if (needResize) + { + passState.patched = nullptr; + passState.color = nullptr; + passState.normal = nullptr; + passState.texWidth = 0; + passState.texHeight = 0; + } + + if (!EnsureWetTarget(nvDevice, passState.patched, width, height, "rt_WetPatched") || + !EnsureWetTarget(nvDevice, passState.color, width, height, "rt_WetColor") || + (doWriteNormal && !EnsureWetTarget(nvDevice, passState.normal, width, height, "rt_WetNormal"))) + { + passState.patched = nullptr; + passState.color = nullptr; + passState.normal = nullptr; + passState.texWidth = 0; + passState.texHeight = 0; + if (!s_loggedNoTargets) + { + Msg("! [WetSurfaces] RT alloc failed — wet disabled this frame"); + s_loggedNoTargets = true; + } + return outputs; + } + passState.texWidth = width; + passState.texHeight = height; + s_loggedNoTargets = false; + ResourceDesc texDesc; texDesc.type = ResourceDesc::Type::Texture2D; texDesc.width = width; texDesc.height = height; texDesc.format = nvrhi::Format::RGBA16_FLOAT; texDesc.isRenderTarget = true; - // Non-transient: aliased wet normal/color made fullscreen SSR hard-switch by - // camera/map position (same class of bug as rt_SceneReflection). texDesc.isTransient = false; texDesc.debugName = "rt_WetPatched"; - auto patched = fg.CreateTexture("rt_WetPatched", texDesc); + auto patched = fg.ImportTexture("rt_WetPatched", passState.patched.Get(), texDesc); texDesc.debugName = "rt_WetColor"; - auto outColor = fg.CreateTexture("rt_WetColor", texDesc); + auto outColor = fg.ImportTexture("rt_WetColor", passState.color.Get(), texDesc); - const bool doWriteNormal = passState.writeNormalPipeline && passState.writeNormalLayout - && passState.patchPipeline && passState.patchLayout; VirtualResourceHandle outNormal{}; if (doWriteNormal) { texDesc.debugName = "rt_WetNormal"; - outNormal = fg.CreateTexture("rt_WetNormal", texDesc); + outNormal = fg.ImportTexture("rt_WetNormal", passState.normal.Get(), texDesc); } const bool hasRainSM = extras.rainSMValid && extras.rainSM.is_valid(); @@ -261,6 +340,9 @@ DefaultOutputLayout setupWetSurfacesPass( data.colorIn = passBuilder.read(inputs.albedo, ResourceState::ShaderResource); data.normal = passBuilder.read(inputs.normal, ResourceState::ShaderResource); data.worldPos = passBuilder.read(inputs.worldPos, ResourceState::ShaderResource); + data.hasBaseColor = inputs.baseColor.is_valid(); + if (data.hasBaseColor) + data.baseColor = passBuilder.read(inputs.baseColor, ResourceState::ShaderResource); if (hasRainSM) data.rainSM = passBuilder.read(extras.rainSM, ResourceState::ShaderResource); if (hasSceneReflection) @@ -287,6 +369,7 @@ DefaultOutputLayout setupWetSurfacesPass( auto* normalTex = fgGraph.GetPhysicalTexture(data.normal); auto* worldPosTex = fgGraph.GetPhysicalTexture(data.worldPos); auto* patchedTex = fgGraph.GetPhysicalTexture(data.patched); + auto* baseTex = data.hasBaseColor ? fgGraph.GetPhysicalTexture(data.baseColor) : nullptr; if (!colorIn || !normalTex || !worldPosTex || !patchedTex || !colorOut) return; @@ -355,6 +438,8 @@ DefaultOutputLayout setupWetSurfacesPass( waterFall = waterRipple; if (!puddlesPerlin) puddlesPerlin = waterRipple; + if (!baseTex) + baseTex = cache.GetDummyContactHistory(nvDevice); nvrhi::Viewport viewport; viewport.minX = 0; @@ -375,7 +460,8 @@ DefaultOutputLayout setupWetSurfacesPass( .Texture("g_RainShadow", rainSM) .Texture("g_Water", waterRipple) .Texture("g_WaterFall", waterFall) - .Texture("g_PuddlesPerlin", puddlesPerlin); + .Texture("g_PuddlesPerlin", puddlesPerlin) + .Texture("g_Base", baseTex); auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->patchLayout, nvDevice); if (bindingSet) { @@ -415,7 +501,7 @@ DefaultOutputLayout setupWetSurfacesPass( { nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(normalOut); - auto framebuffer = cache.GetOrCreateFramebuffer("WetWriteNormal_v1d", fbDesc, nvDevice); + auto framebuffer = cache.GetOrCreateFramebuffer("WetWriteNormal_v1f", fbDesc, nvDevice); if (framebuffer) { nvrhi::GraphicsState state; @@ -457,7 +543,8 @@ DefaultOutputLayout setupWetSurfacesPass( .Texture("g_Patched", patchedForApply) .Texture("g_WorldPos", worldPosTex) .Texture("g_WaterFall", waterFall) - .Texture("g_SceneReflection", sceneRefl); + .Texture("g_SceneReflection", sceneRefl) + .Texture("g_Base", baseTex); auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->layout, nvDevice); if (!bindingSet) { diff --git a/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.h index a92e63ae009..75dd3544a56 100644 --- a/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/WetSurfacesPassSetup.h @@ -16,12 +16,17 @@ namespace xray::render::fg { namespace xray::render::fg::passes { struct WetSurfacesPassState { - nvrhi::GraphicsPipelineHandle pipeline; // apply + nvrhi::GraphicsPipelineHandle pipeline; nvrhi::BindingLayoutHandle layout; nvrhi::GraphicsPipelineHandle patchPipeline; nvrhi::BindingLayoutHandle patchLayout; nvrhi::GraphicsPipelineHandle writeNormalPipeline; nvrhi::BindingLayoutHandle writeNormalLayout; + nvrhi::TextureHandle patched; + nvrhi::TextureHandle color; + nvrhi::TextureHandle normal; + u32 texWidth = 0; + u32 texHeight = 0; bool initialized = false; u32 pipeVersion = 0; }; diff --git a/src/Layers/xrRender/GPUCullingManager.cpp b/src/Layers/xrRender/GPUCullingManager.cpp index 97e89726fc9..eeb7d511167 100644 --- a/src/Layers/xrRender/GPUCullingManager.cpp +++ b/src/Layers/xrRender/GPUCullingManager.cpp @@ -1,6 +1,7 @@ // xrRender/GPUCullingManager.cpp #include "stdafx.h" #include "GPUCullingManager.h" +#include "ClusteredLightManager.h" #include "xrCore/Profiler/Profiler.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" @@ -484,7 +485,7 @@ void GPUCullingManager::CreateBuffers(fg::RenderDevice* device) // TESSELLATION CULLING SET (opaque bump# / PN+HM) // ─────────────────────────────────────────────────────── { - const u32 maxTess = 4096; + const u32 maxTess = m_maxObjects; m_tessSet.maxObjects = maxTess; nvrhi::BufferDesc desc; @@ -1744,6 +1745,12 @@ void GPUCullingManager::Shutdown() m_cullParamsCB = fg::BufferHandle(); m_cullPipeline = nullptr; m_cullLayout = nullptr; + m_lightShadowCullPipeline = nullptr; + m_lightShadowCullLayout = nullptr; + m_lightShadowCullParamsCB = nullptr; + m_lightShadowCullReady = false; + m_localShadowCullSlots.clear(); + m_localShadowCullSlotCount = 0; m_pointSampler = nullptr; m_compactParamsCB = fg::BufferHandle(); @@ -1826,6 +1833,8 @@ void GPUCullingManager::Shutdown() // Visibility buffer m_staticTerrainDrawArgsUploaded = false; + m_terrainDataCached = false; + m_lastMaterialCountForStatic = 0; m_staticDataCached = false; m_initialized = false; @@ -1968,34 +1977,108 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr const auto& batches = geometry->GetBatches(); u32 totalBatches = static_cast(batches.size()); + const bool haveStaticSource = m_staticBatchSource && !m_staticBatchSource->empty(); - if (totalBatches == 0) { - m_staticSet.objectCount = 0; + if (totalBatches == 0 && !(haveStaticSource && !m_staticDataCached)) { m_dynamicSet.objectCount = 0; m_tessSet.objectCount = 0; - m_objectCount = 0; + m_transparentSet.objectCount = 0; + m_transientDataCached = false; + m_transientUploadFingerprint = 0; + if (!m_staticDataCached) + { + m_staticSet.objectCount = 0; + m_objectCount = 0; + } + else + { + m_objectCount = m_staticSet.objectCount; + } return; } - // Tess routing depends on MaterialBuffer.tessMethod, which fills in as textures load. - // Rebuild static lists every frame while tessellation is enabled. - // Apple/MoltenVK: never route into PatchList (IOGPU kernel panic risk). #if defined(XR_PLATFORM_APPLE) const bool tessFlagOn = false; #else const bool tessFlagOn = ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION); #endif - if (tessFlagOn) + { + const u32 matCount = bindless::MaterialBuffer::Instance().GetMaterialCount(); + if (matCount != m_lastMaterialCountForStatic) + { + m_staticDataCached = false; + m_staticSet.objectsUploaded = false; + m_terrainDataCached = false; + m_staticTerrainDrawArgsUploaded = false; + m_transientDataCached = false; + m_lastMaterialCountForStatic = matCount; + } + } + if (tessFlagOn != m_lastTessFlagOn) + { m_staticDataCached = false; + m_staticSet.objectsUploaded = false; + m_transientDataCached = false; + m_lastTessFlagOn = tessFlagOn; + } + + auto hashMix = [](u64 h, u64 v) -> u64 { + return h ^ (v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2)); + }; + auto hashFloat = [&](u64 h, float f) -> u64 { + u32 bits = 0; + memcpy(&bits, &f, sizeof(bits)); + return hashMix(h, bits); + }; + + u64 transientFp = 0; + transientFp = hashMix(transientFp, tessFlagOn ? 1u : 0u); + transientFp = hashMix(transientFp, bindless::MaterialBuffer::Instance().GetMaterialCount()); + { + constexpr u32 kMaxTessIndexCount = 8192; + u32 transientCount = 0; + for (u32 i = 0; i < totalBatches; ++i) + { + const auto& batch = batches[i]; + if (batch.isSkinned || batch.isTerrain) + continue; + + bool routeTess = false; + if (tessFlagOn && !batch.IsStrictB2F()) + { + const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(batch.bindlessMaterialID); + routeTess = mat && mat->tessMethod != 0 && batch.indexCount > 0 && + batch.indexCount <= kMaxTessIndexCount; + } + const bool isTransient = batch.IsStrictB2F() || routeTess || !batch.isStatic; + if (!isTransient) + continue; + + ++transientCount; + transientFp = hashMix(transientFp, reinterpret_cast(batch.visual)); + transientFp = hashMix(transientFp, batch.bindlessMaterialID); + transientFp = hashMix(transientFp, batch.indexCount); + transientFp = hashMix(transientFp, batch.isStatic ? 1u : 0u); + transientFp = hashMix(transientFp, batch.IsStrictB2F() ? 1u : 0u); + transientFp = hashMix(transientFp, routeTess ? 1u : 0u); + transientFp = hashFloat(transientFp, batch.worldMatrix._11); + transientFp = hashFloat(transientFp, batch.worldMatrix._22); + transientFp = hashFloat(transientFp, batch.worldMatrix._33); + transientFp = hashFloat(transientFp, batch.worldMatrix._41); + transientFp = hashFloat(transientFp, batch.worldMatrix._42); + transientFp = hashFloat(transientFp, batch.worldMatrix._43); + transientFp = hashFloat(transientFp, batch.worldBoundsRadius); + } + transientFp = hashMix(transientFp, transientCount); + } + const bool skipTransientRebuild = + m_transientDataCached && transientFp == m_transientUploadFingerprint; R_ASSERT2(m_staticSet.objectBuffer && m_staticSet.drawArgsBuffer && m_staticSet.visibilityBuffer, "Static GPU culling buffers not initialized"); R_ASSERT2(m_dynamicSet.objectBuffer && m_dynamicSet.drawArgsBuffer && m_dynamicSet.visibilityBuffer, "Dynamic GPU culling buffers not initialized"); - // Build object data, draw args, and material ID arrays - // NOTE: Skip skinned batches - they use separate per-draw rendering with bone matrices - // NOTE: Terrain batches are tracked separately for terrain shader rendering if (!m_staticDataCached) { m_staticObjectData.clear(); m_staticObjectData.reserve(totalBatches); @@ -2009,36 +2092,37 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr m_staticBatchVertexCounts.reserve(totalBatches); } - m_dynamicObjectData.clear(); - m_dynamicObjectData.reserve(totalBatches); - m_dynamicDrawArgsData.clear(); - m_dynamicDrawArgsData.reserve(totalBatches); - m_dynamicMaterialIDData.clear(); - m_dynamicMaterialIDData.reserve(totalBatches); - m_dynamicInstanceData.clear(); - m_dynamicInstanceData.reserve(totalBatches); + if (!skipTransientRebuild) { + m_dynamicObjectData.clear(); + m_dynamicObjectData.reserve(totalBatches); + m_dynamicDrawArgsData.clear(); + m_dynamicDrawArgsData.reserve(totalBatches); + m_dynamicMaterialIDData.clear(); + m_dynamicMaterialIDData.reserve(totalBatches); + m_dynamicInstanceData.clear(); + m_dynamicInstanceData.reserve(totalBatches); - // Terrain-specific arrays - m_terrainObjectData.clear(); - m_terrainObjectData.reserve(totalBatches / 4); - m_terrainDrawArgsData.clear(); - m_terrainDrawArgsData.reserve(totalBatches / 4); - m_terrainMaterialIDData.clear(); - m_terrainMaterialIDData.reserve(totalBatches / 4); - m_terrainInstanceData.clear(); - m_terrainInstanceData.reserve(totalBatches / 4); + m_transparentObjectData.clear(); + m_transparentDrawArgsData.clear(); + m_transparentMaterialIDData.clear(); + m_transparentInstanceData.clear(); - // Transparent-specific arrays - m_transparentObjectData.clear(); - m_transparentDrawArgsData.clear(); - m_transparentMaterialIDData.clear(); - m_transparentInstanceData.clear(); + m_tessObjectData.clear(); + m_tessDrawArgsData.clear(); + m_tessMaterialIDData.clear(); + m_tessInstanceData.clear(); + } - // Tessellation-specific arrays (rebuilt every frame like transparent) - m_tessObjectData.clear(); - m_tessDrawArgsData.clear(); - m_tessMaterialIDData.clear(); - m_tessInstanceData.clear(); + if (!m_terrainDataCached) { + m_terrainObjectData.clear(); + m_terrainObjectData.reserve(totalBatches / 4); + m_terrainDrawArgsData.clear(); + m_terrainDrawArgsData.reserve(totalBatches / 4); + m_terrainMaterialIDData.clear(); + m_terrainMaterialIDData.reserve(totalBatches / 4); + m_terrainInstanceData.clear(); + m_terrainInstanceData.reserve(totalBatches / 4); + } auto appendBatch = [&](const GeometryBatch& batch, xr_vector& objectData, @@ -2181,95 +2265,117 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr ZoneScopedN("Upload::Rebuild"); u32 badMatSkipped = 0; const u32 terrainMatCount = bindless::TerrainMaterialBuffer::Instance().GetMaterialCount(); + constexpr u32 kMaxTessIndexCount = 8192; + + auto appendTerrain = [&](const GeometryBatch& batch) { + if (batch.terrainMaterialID == UINT32_MAX) + { + badMatSkipped++; + return; + } + GPUObjectData obj; + obj.position = batch.worldBoundsCenter; + obj.radius = batch.worldBoundsRadius; + obj.batchIndex = static_cast(m_terrainObjectData.size()); + obj.flags = GPU_OBJECT_OPAQUE; + obj.pad0 = 0.0f; + obj.pad1 = 0.0f; + m_terrainObjectData.push_back(obj); + + IndirectDrawArgs args; + args.indexCountPerInstance = batch.indexCount; + args.instanceCount = 0; + if (batch.megaBufferAlloc.valid) { + args.startIndexLocation = batch.megaBufferAlloc.indexOffset; + args.baseVertexLocation = static_cast(batch.megaBufferAlloc.vertexOffset); + } else { + args.startIndexLocation = batch.startIndex; + args.baseVertexLocation = batch.baseVertex; + } + args.startInstanceLocation = static_cast(m_terrainDrawArgsData.size()); + m_terrainDrawArgsData.push_back(args); + + m_terrainMaterialIDData.push_back(batch.terrainMaterialID); + + GPUInstanceData inst; + inst.world = batch.worldMatrix; + inst.materialID = batch.terrainMaterialID; + inst.flags = GPU_OBJECT_OPAQUE; + inst.pad0 = 0.0f; + inst.pad1 = 0.0f; + m_terrainInstanceData.push_back(inst); + }; + + auto isTessBatch = [&](const GeometryBatch& batch) -> bool { + if (!tessFlagOn || batch.IsStrictB2F()) + return false; + const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(batch.bindlessMaterialID); + return mat && mat->tessMethod != 0 && batch.indexCount > 0 && + batch.indexCount <= kMaxTessIndexCount; + }; + + if (haveStaticSource && (!m_staticDataCached || !m_terrainDataCached)) + { + ZoneScopedN("Upload::StaticFromCache"); + for (const auto& batch : *m_staticBatchSource) + { + if (batch.isSkinned) + continue; + if (batch.isTerrain) + { + if (!m_terrainDataCached) + appendTerrain(batch); + continue; + } + if (m_staticDataCached || batch.IsStrictB2F() || isTessBatch(batch)) + continue; + if (appendBatch(batch, m_staticObjectData, m_staticDrawArgsData, m_staticMaterialIDData, m_staticInstanceData)) + m_staticBatchVertexCounts.push_back(batch.megaBufferAlloc.valid ? batch.megaBufferAlloc.vertexCount : 0); + else + badMatSkipped++; + } + } + for (u32 i = 0; i < totalBatches; i++) { const auto& batch = batches[i]; - // Skip skinned batches - they use a separate per-draw rendering path - // with bone matrices and cannot use the GPU-driven multi-draw system if (batch.isSkinned) continue; - // Route terrain batches to separate arrays for terrain shader rendering if (batch.isTerrain) { - // Only reject completely unregistered IDs. Do not compare against - // GetMaterialCount() — pending terrain slots are valid after Register. - if (batch.terrainMaterialID == UINT32_MAX) - { - badMatSkipped++; + if (m_terrainDataCached || haveStaticSource) continue; - } - // ───────────────────────────────────────────────────── - // TERRAIN BATCH - Goes to terrain arrays - // ───────────────────────────────────────────────────── - GPUObjectData obj; - obj.position = batch.worldBoundsCenter; - obj.radius = batch.worldBoundsRadius; - obj.batchIndex = static_cast(m_terrainObjectData.size()); - obj.flags = GPU_OBJECT_OPAQUE; // Terrain is always opaque - obj.pad0 = 0.0f; - obj.pad1 = 0.0f; - m_terrainObjectData.push_back(obj); - - // Terrain draw args - IndirectDrawArgs args; - args.indexCountPerInstance = batch.indexCount; - args.instanceCount = 0; // Set by culling shader if visible - if (batch.megaBufferAlloc.valid) { - args.startIndexLocation = batch.megaBufferAlloc.indexOffset; - args.baseVertexLocation = static_cast(batch.megaBufferAlloc.vertexOffset); - } else { - args.startIndexLocation = batch.startIndex; - args.baseVertexLocation = batch.baseVertex; - } - // CRITICAL: StartInstanceLocation provides draw index to shader - // This indexes into terrain material/instance buffers - args.startInstanceLocation = static_cast(m_terrainDrawArgsData.size()); - m_terrainDrawArgsData.push_back(args); - - // Terrain material ID (index into TerrainMaterialBuffer, not regular MaterialBuffer) - m_terrainMaterialIDData.push_back(batch.terrainMaterialID); - - // Terrain instance data (world transform) - GPUInstanceData inst; - inst.world = batch.worldMatrix; - inst.materialID = batch.terrainMaterialID; // Terrain material ID - inst.flags = GPU_OBJECT_OPAQUE; // Terrain is always opaque - inst.pad0 = 0.0f; - inst.pad1 = 0.0f; - m_terrainInstanceData.push_back(inst); + appendTerrain(batch); continue; } if (batch.IsStrictB2F()) { - if (!appendBatch(batch, m_transparentObjectData, m_transparentDrawArgsData, m_transparentMaterialIDData, m_transparentInstanceData)) + if (!skipTransientRebuild && + !appendBatch(batch, m_transparentObjectData, m_transparentDrawArgsData, m_transparentMaterialIDData, m_transparentInstanceData)) badMatSkipped++; continue; } - // Route model tess materials to a dedicated PatchList draw set. - // Skip huge meshes: MoltenVK tess expands patches and can exceed its - // ~131k temporary-vertex cap (view-dependent freeze until camera moves). - constexpr u32 kMaxTessIndexCount = 8192; - if (tessFlagOn) + if (isTessBatch(batch)) { - const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(batch.bindlessMaterialID); - if (mat && mat->tessMethod != 0 && batch.indexCount > 0 && - batch.indexCount <= kMaxTessIndexCount) + if (!skipTransientRebuild && + m_tessObjectData.size() < m_tessSet.maxObjects && + appendBatch(batch, m_tessObjectData, m_tessDrawArgsData, m_tessMaterialIDData, m_tessInstanceData)) { - if (!appendBatch(batch, m_tessObjectData, m_tessDrawArgsData, m_tessMaterialIDData, m_tessInstanceData)) - badMatSkipped++; continue; } + if (skipTransientRebuild) + continue; } if (batch.isStatic) { - if (!m_staticDataCached) { + if (!m_staticDataCached && !haveStaticSource) { if (appendBatch(batch, m_staticObjectData, m_staticDrawArgsData, m_staticMaterialIDData, m_staticInstanceData)) m_staticBatchVertexCounts.push_back(batch.megaBufferAlloc.valid ? batch.megaBufferAlloc.vertexCount : 0); else badMatSkipped++; } - } else { + } else if (!skipTransientRebuild) { if (!appendBatch(batch, m_dynamicObjectData, m_dynamicDrawArgsData, m_dynamicMaterialIDData, m_dynamicInstanceData)) badMatSkipped++; } @@ -2369,7 +2475,7 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr Msg("* [GPUCulling] Static object data uploaded: %u objects", m_staticSet.objectCount); } - if (m_dynamicSet.objectCount > 0) { + if (!skipTransientRebuild && m_dynamicSet.objectCount > 0) { ZoneScopedN("Upload::DynamicWrite"); R_ASSERT2(m_dynamicObjectData.size() >= m_dynamicSet.objectCount, "Dynamic object data smaller than count"); R_ASSERT2(m_dynamicDrawArgsData.size() >= m_dynamicSet.objectCount, "Dynamic draw args data smaller than count"); @@ -2417,15 +2523,11 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr R_ASSERT2(m_terrainMaterialIDData.size() >= m_terrainObjectCount, "Terrain material ID data smaller than object count"); R_ASSERT2(m_terrainInstanceData.size() >= m_terrainObjectCount, "Terrain instance data smaller than object count"); - // Terrain object data (bounding spheres) - still uploaded every frame - // TODO: Terrain is static, could cache this too with dirty flag - cmdList->writeBuffer(m_terrainObjectBuffer, - m_terrainObjectData.data(), - m_terrainObjectCount * sizeof(GPUObjectData)); - cmdList->setBufferState(m_terrainObjectBuffer, nvrhi::ResourceStates::ShaderResource); - - // Terrain draw args, material IDs, instance data - uploaded ONCE (visibility buffer handles culling) if (!m_staticTerrainDrawArgsUploaded) { + cmdList->writeBuffer(m_terrainObjectBuffer, + m_terrainObjectData.data(), + m_terrainObjectCount * sizeof(GPUObjectData)); + cmdList->setBufferState(m_terrainObjectBuffer, nvrhi::ResourceStates::ShaderResource); // Set instanceCount=1 for all terrain (apply visibility pass will set actual visibility) for (auto& args : m_terrainDrawArgsData) { args.instanceCount = 1; @@ -2477,6 +2579,7 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr } m_staticTerrainDrawArgsUploaded = true; + m_terrainDataCached = true; Msg("* [GPUCulling] Static terrain draw args uploaded: %u objects (visibility buffer mode)", m_terrainObjectCount); } } @@ -2488,7 +2591,8 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr // ───────────────────────────────────────────────────── m_transparentSet.objectCount = std::min(static_cast(m_transparentObjectData.size()), m_transparentSet.maxObjects); - if (m_transparentSet.objectCount > 0 && m_transparentSet.objectBuffer && m_transparentSet.drawArgsBuffer) { + if (!skipTransientRebuild && m_transparentSet.objectCount > 0 && + m_transparentSet.objectBuffer && m_transparentSet.drawArgsBuffer) { ZoneScopedN("Upload::TransparentWrite"); for (auto& args : m_transparentDrawArgsData) args.instanceCount = 1; @@ -2513,17 +2617,13 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr m_transparentSet.objectCount * sizeof(GPUInstanceData)); cmdList->setBufferState(m_transparentSet.instanceBuffer, nvrhi::ResourceStates::ShaderResource); - // View-independent foliage shadow casting: trees/bushes are alpha-blended and - // live here, so the shadow pass draws this set (foliage-only) into the CSM. EnsureShadowCasterBuffers(cmdList, m_device->GetNVRHIDevice(), m_transparentSet, "Transparent"); } - // ───────────────────────────────────────────────────── - // TESSELLATION GPU UPLOAD (per-frame) - // ───────────────────────────────────────────────────── m_tessSet.objectCount = std::min(static_cast(m_tessObjectData.size()), m_tessSet.maxObjects); - if (m_tessSet.objectCount > 0 && m_tessSet.objectBuffer && m_tessSet.drawArgsBuffer) { + if (!skipTransientRebuild && m_tessSet.objectCount > 0 && + m_tessSet.objectBuffer && m_tessSet.drawArgsBuffer) { ZoneScopedN("Upload::TessWrite"); for (u32 i = 0; i < m_tessSet.objectCount; ++i) { @@ -2561,20 +2661,36 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr cmdList->setBufferState(m_tessBatchIndicesBuffer, nvrhi::ResourceStates::ShaderResource); } } + + if (!skipTransientRebuild) + { + m_transientUploadFingerprint = transientFp; + m_transientDataCached = true; + } } void GPUCullingManager::InvalidateStaticCullingData() { m_staticDataCached = false; + m_terrainDataCached = false; + m_staticTerrainDrawArgsUploaded = false; + m_lastMaterialCountForStatic = 0; m_staticSet.objectsUploaded = false; m_staticSet.drawArgsUploaded = false; m_staticSet.objectCount = 0; + m_transientDataCached = false; + m_transientUploadFingerprint = 0; m_staticObjectData.clear(); m_staticDrawArgsData.clear(); m_staticMaterialIDData.clear(); m_staticInstanceData.clear(); m_staticBatchVertexCounts.clear(); + m_terrainObjectData.clear(); + m_terrainDrawArgsData.clear(); + m_terrainMaterialIDData.clear(); + m_terrainInstanceData.clear(); + m_terrainObjectCount = 0; Msg("* [GPUCulling] Static culling data invalidated"); } @@ -2619,6 +2735,8 @@ void GPUCullingManager::InvalidateShadersAndPipelines() m_staticDataCached = false; m_staticTerrainDrawArgsUploaded = false; + m_terrainDataCached = false; + m_lastMaterialCountForStatic = 0; m_staticSet.objectsUploaded = false; m_staticSet.drawArgsUploaded = false; m_dynamicSet.objectsUploaded = false; @@ -2905,6 +3023,7 @@ void GPUCullingManager::EnsureShadowCasterBuffers(nvrhi::ICommandList* cmdList, if (!set.shadowIndicesBuffer || set.shadowBuffersCapacity < set.objectCount) { const u32 capacity = set.objectCount; + set.shadowCountBuffer = nullptr; char nameBuf[128]; nvrhi::BufferDesc idxDesc; @@ -2912,7 +3031,8 @@ void GPUCullingManager::EnsureShadowCasterBuffers(nvrhi::ICommandList* cmdList, idxDesc.debugName = nameBuf; idxDesc.byteSize = u64(capacity) * sizeof(u32); idxDesc.structStride = sizeof(u32); - idxDesc.initialState = nvrhi::ResourceStates::ShaderResource; + idxDesc.canHaveUAVs = true; + idxDesc.initialState = nvrhi::ResourceStates::UnorderedAccess; idxDesc.keepInitialState = true; set.shadowIndicesBuffer = nvDevice->createBuffer(idxDesc); @@ -2931,8 +3051,9 @@ void GPUCullingManager::EnsureShadowCasterBuffers(nvrhi::ICommandList* cmdList, cntDesc.debugName = nameBuf; cntDesc.byteSize = sizeof(u32); cntDesc.canHaveRawViews = true; + cntDesc.canHaveUAVs = true; cntDesc.isDrawIndirectArgs = true; - cntDesc.initialState = nvrhi::ResourceStates::IndirectArgument; + cntDesc.initialState = nvrhi::ResourceStates::UnorderedAccess; cntDesc.keepInitialState = true; set.shadowCountBuffer = nvDevice->createBuffer(cntDesc); } @@ -2943,8 +3064,10 @@ void GPUCullingManager::EnsureShadowCasterBuffers(nvrhi::ICommandList* cmdList, argsDesc.debugName = nameBuf; argsDesc.byteSize = u64(capacity) * sizeof(IndirectDrawArgs); argsDesc.canHaveRawViews = true; + argsDesc.canHaveUAVs = true; argsDesc.isDrawIndirectArgs = true; - argsDesc.initialState = nvrhi::ResourceStates::IndirectArgument; + argsDesc.structStride = sizeof(IndirectDrawArgs); + argsDesc.initialState = nvrhi::ResourceStates::UnorderedAccess; argsDesc.keepInitialState = true; set.shadowCompactDrawArgsBuffer = nvDevice->createBuffer(argsDesc); } @@ -2954,7 +3077,8 @@ void GPUCullingManager::EnsureShadowCasterBuffers(nvrhi::ICommandList* cmdList, matDesc.debugName = nameBuf; matDesc.byteSize = u64(capacity) * sizeof(u32); matDesc.structStride = sizeof(u32); - matDesc.initialState = nvrhi::ResourceStates::ShaderResource; + matDesc.canHaveUAVs = true; + matDesc.initialState = nvrhi::ResourceStates::UnorderedAccess; matDesc.keepInitialState = true; set.shadowCompactMaterialIDBuffer = nvDevice->createBuffer(matDesc); } @@ -2968,7 +3092,7 @@ void GPUCullingManager::EnsureShadowCasterBuffers(nvrhi::ICommandList* cmdList, } void GPUCullingManager::BuildLightFrustumCasters(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, - const Fmatrix& lightVP, float radiusInflate) + const Fmatrix& lightVP, float radiusInflate, float skipNearCameraClearance) { if (!cmdList || !nvDevice) return; @@ -2978,6 +3102,7 @@ void GPUCullingManager::BuildLightFrustumCasters(nvrhi::ICommandList* cmdList, n CFrustum frustum; Fmatrix vp = lightVP; frustum.CreateFromMatrix(vp, FRUSTUM_P_LRTB | FRUSTUM_P_FAR); + const Fvector camPos = Device.vCameraPosition; auto filterSet = [&](CullSetBuffers& set, const xr_vector& objects, @@ -3009,14 +3134,20 @@ void GPUCullingManager::BuildLightFrustumCasters(nvrhi::ICommandList* cmdList, n const float r = o.radius * radiusInflate; if (!frustum.testSphere_dirty(o.position, r)) continue; + // Local atlas: drop large casters that reach into the camera bubble. + // Those walls fill sideways OMNIPART faces → hard "shadows in your face". + // CSM/rain keep skipNearCameraClearance=0. + if (skipNearCameraClearance > 0.f && r > 0.45f) + { + const float camDist = camPos.distance_to(o.position); + if (camDist - r < skipNearCameraClearance) + continue; + } ids.push_back(i); if (i < drawArgs.size()) { IndirectDrawArgs a = drawArgs[i]; a.instanceCount = 1; - // DRAWINDEX must index the compact buffers (0..count-1), not the - // source object slot. Keeping the original startInstanceLocation - // made foliage sample wrong materials → solid triangle shadows. a.startInstanceLocation = static_cast(args.size()); args.push_back(a); } @@ -3038,6 +3169,535 @@ void GPUCullingManager::BuildLightFrustumCasters(nvrhi::ICommandList* cmdList, n filterSet(m_transparentSet, m_transparentObjectData, m_transparentDrawArgsData, m_transparentMaterialIDData, "Transparent"); } +bool GPUCullingManager::EnsureLightShadowCullPipeline(nvrhi::IDevice* nvDevice) +{ + if (m_lightShadowCullReady) + return m_lightShadowCullPipeline != nullptr && m_lightShadowCullParamsCB != nullptr; + if (!nvDevice || !GEnv.Render || !GEnv.Render->GetShaderLoader()) + return false; + + auto csShader = GEnv.Render->GetShaderLoader()->LoadComputeShader("light_shadow_cull"); + auto* csRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("light_shadow_cull", ".cs"); + if (!csRefl || !csShader.handle) + { + static bool s_once = false; + if (!s_once) + { + Msg("! [GPUCulling] light_shadow_cull.cs missing — falling back to CPU light frustum cull"); + s_once = true; + } + m_lightShadowCullReady = true; + return false; + } + + auto& cache = framegraph::GetPassResourceCache(); + m_lightShadowCullLayout = cache.GetOrCreateBindingLayoutFromReflection( + "GPUCull_LightShadowCull", *csRefl, nvDevice); + if (!m_lightShadowCullLayout) + { + m_lightShadowCullReady = true; + return false; + } + + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = csShader.handle; + pipeDesc.bindingLayouts = {m_lightShadowCullLayout}; + m_lightShadowCullPipeline = nvDevice->createComputePipeline(pipeDesc); + if (!m_lightShadowCullPipeline) + { + m_lightShadowCullReady = true; + return false; + } + + if (!m_lightShadowCullParamsCB) + { + nvrhi::BufferDesc cbDesc; + cbDesc.byteSize = sizeof(Fvector4) * 6 + sizeof(u32) * 4; + cbDesc.isConstantBuffer = true; + cbDesc.isVolatile = true; + cbDesc.maxVersions = 8192; + cbDesc.debugName = "GPUCull_LightShadowCullParams"; + m_lightShadowCullParamsCB = nvDevice->createBuffer(cbDesc); + } + m_lightShadowCullReady = true; + if (m_lightShadowCullPipeline && m_lightShadowCullParamsCB) + Msg("* [GPUCulling] light_shadow_cull pipeline ready"); + return m_lightShadowCullPipeline != nullptr && m_lightShadowCullParamsCB != nullptr; +} + +bool GPUCullingManager::EnsureLocalShadowCullSlots(nvrhi::IDevice* nvDevice, u32 /*slotCount*/) +{ + if (!nvDevice) + return false; + if (m_localShadowCullSlotCount >= kLocalShadowCullMaxSlots && !m_localShadowCullSlots.empty()) + return true; + + m_localShadowCullSlots.clear(); + m_localShadowCullSlots.resize(kLocalShadowCullMaxSlots); + m_localShadowCullSlotCount = 0; + + const u32 slotCount = kLocalShadowCullMaxSlots; + const u32 cap = kLocalShadowCullSlotCap; + static const char* kSetNames[3] = {"Static", "Dynamic", "Transparent"}; + for (u32 s = 0; s < slotCount; ++s) + { + for (u32 si = 0; si < 3; ++si) + { + auto& set = m_localShadowCullSlots[s].sets[si]; + char nameBuf[128]; + { + nvrhi::BufferDesc d; + snprintf(nameBuf, sizeof(nameBuf), "LocalShadowCull_s%u_%s_Idx", s, kSetNames[si]); + d.debugName = nameBuf; + d.byteSize = u64(cap) * sizeof(u32); + d.structStride = sizeof(u32); + d.canHaveUAVs = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + d.keepInitialState = true; + set.indices = nvDevice->createBuffer(d); + } + { + nvrhi::BufferDesc d; + snprintf(nameBuf, sizeof(nameBuf), "LocalShadowCull_s%u_%s_Args", s, kSetNames[si]); + d.debugName = nameBuf; + d.byteSize = u64(cap) * sizeof(IndirectDrawArgs); + d.structStride = sizeof(IndirectDrawArgs); + d.canHaveUAVs = true; + d.canHaveRawViews = true; + d.isDrawIndirectArgs = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + d.keepInitialState = true; + set.drawArgs = nvDevice->createBuffer(d); + } + { + nvrhi::BufferDesc d; + snprintf(nameBuf, sizeof(nameBuf), "LocalShadowCull_s%u_%s_Mats", s, kSetNames[si]); + d.debugName = nameBuf; + d.byteSize = u64(cap) * sizeof(u32); + d.structStride = sizeof(u32); + d.canHaveUAVs = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + d.keepInitialState = true; + set.mats = nvDevice->createBuffer(d); + } + { + nvrhi::BufferDesc d; + snprintf(nameBuf, sizeof(nameBuf), "LocalShadowCull_s%u_%s_Cnt", s, kSetNames[si]); + d.debugName = nameBuf; + d.byteSize = sizeof(u32); + d.canHaveUAVs = true; + d.canHaveRawViews = true; + d.isDrawIndirectArgs = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + d.keepInitialState = true; + set.count = nvDevice->createBuffer(d); + } + if (!set.indices || !set.drawArgs || !set.mats || !set.count) + { + m_localShadowCullSlots.clear(); + m_localShadowCullSlotCount = 0; + Msg("! [GPUCulling] LocalShadowCull slot alloc failed at %u", s); + return false; + } + } + } + m_localShadowCullSlotCount = slotCount; + Msg("* [GPUCulling] LocalShadowCull slots ready (%u x %u casters)", slotCount, cap); + return true; +} + +nvrhi::BindingSetHandle* GPUCullingManager::MutLocalShadowSlotDrawBinding(u32 slot, u32 setIdx) +{ + if (slot >= m_localShadowCullSlotCount || setIdx >= 3) + return nullptr; + return std::addressof(m_localShadowCullSlots[slot].sets[setIdx].drawBinding); +} + +nvrhi::IBuffer** GPUCullingManager::MutLocalShadowSlotDrawSrcInstance(u32 slot, u32 setIdx) +{ + if (slot >= m_localShadowCullSlotCount || setIdx >= 3) + return nullptr; + return std::addressof(m_localShadowCullSlots[slot].sets[setIdx].drawSrcInstance); +} + +nvrhi::IBuffer* GPUCullingManager::GetLocalShadowSlotIndices(u32 slot, u32 setIdx) const +{ + if (slot >= m_localShadowCullSlotCount || setIdx >= 3) + return nullptr; + return m_localShadowCullSlots[slot].sets[setIdx].indices.Get(); +} +nvrhi::IBuffer* GPUCullingManager::GetLocalShadowSlotDrawArgs(u32 slot, u32 setIdx) const +{ + if (slot >= m_localShadowCullSlotCount || setIdx >= 3) + return nullptr; + return m_localShadowCullSlots[slot].sets[setIdx].drawArgs.Get(); +} +nvrhi::IBuffer* GPUCullingManager::GetLocalShadowSlotMats(u32 slot, u32 setIdx) const +{ + if (slot >= m_localShadowCullSlotCount || setIdx >= 3) + return nullptr; + return m_localShadowCullSlots[slot].sets[setIdx].mats.Get(); +} +nvrhi::IBuffer* GPUCullingManager::GetLocalShadowSlotCount(u32 slot, u32 setIdx) const +{ + if (slot >= m_localShadowCullSlotCount || setIdx >= 3) + return nullptr; + return m_localShadowCullSlots[slot].sets[setIdx].count.Get(); +} + +bool GPUCullingManager::DispatchLightFrustumCasters(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const Fmatrix& lightVP, float radiusInflate) +{ + if (!cmdList || !nvDevice) + return false; + + ZoneScopedN("DispatchLightFrustumCasters"); + if (!EnsureLightShadowCullPipeline(nvDevice)) + return false; + + struct LightShadowCullParamsCB + { + Fvector4 frustumPlanes[6]; + u32 objectCount; + float radiusInflate; + u32 maxOut; + u32 pad; + }; + + Fmatrix vp = lightVP; + Fvector4 planes[6]; + ExtractFrustumPlanes(vp, planes); + + auto dispatchSet = [&](CullSetBuffers& set, const char* name) -> bool { + if (set.objectCount == 0) + return true; + if (!set.objectBuffer || !set.drawArgsBuffer || !set.materialIDBuffer) + return false; + + EnsureShadowCasterBuffers(cmdList, nvDevice, set, name); + if (!set.shadowIndicesBuffer || !set.shadowCompactDrawArgsBuffer || + !set.shadowCompactMaterialIDBuffer || !set.shadowCountBuffer) + return false; + + LightShadowCullParamsCB cb{}; + for (u32 i = 0; i < 6; ++i) + cb.frustumPlanes[i] = planes[i]; + cb.objectCount = set.objectCount; + cb.radiusInflate = radiusInflate; + cb.maxOut = set.objectCount; + cmdList->writeBuffer(m_lightShadowCullParamsCB, &cb, sizeof(cb)); + + const u32 zero = 0; + cmdList->writeBuffer(set.shadowCountBuffer, &zero, sizeof(u32)); + + cmdList->setBufferState(set.objectBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.drawArgsBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.materialIDBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.shadowIndicesBuffer, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(set.shadowCompactDrawArgsBuffer, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(set.shadowCompactMaterialIDBuffer, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(set.shadowCountBuffer, nvrhi::ResourceStates::UnorderedAccess); + + auto* csRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("light_shadow_cull", ".cs"); + framegraph::BindingSetBuilder bsb(*csRefl, nvDevice, "LightShadowCull"); + bsb.ConstantBuffer("LightShadowCullParams", m_lightShadowCullParamsCB); + bsb.BufferSRV("g_Objects", set.objectBuffer); + bsb.BufferSRV("g_InputDrawArgs", set.drawArgsBuffer); + bsb.BufferSRV("g_InputMaterialIDs", set.materialIDBuffer); + bsb.BufferUAV("g_OutIndices", set.shadowIndicesBuffer); + bsb.BufferUAV("g_OutDrawArgs", set.shadowCompactDrawArgsBuffer); + bsb.BufferUAV("g_OutMaterialIDs", set.shadowCompactMaterialIDBuffer); + bsb.BufferUAV("g_OutCount", set.shadowCountBuffer); + + auto bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet( + bsb.Build(), m_lightShadowCullLayout, nvDevice); + if (!bindingSet) + return false; + + nvrhi::ComputeState state; + state.pipeline = m_lightShadowCullPipeline; + state.bindings = {bindingSet}; + cmdList->setComputeState(state); + cmdList->dispatch((set.objectCount + 63) / 64, 1, 1); + + cmdList->setBufferState(set.shadowIndicesBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.shadowCompactDrawArgsBuffer, nvrhi::ResourceStates::IndirectArgument); + cmdList->setBufferState(set.shadowCompactMaterialIDBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.shadowCountBuffer, nvrhi::ResourceStates::IndirectArgument); + return true; + }; + + if (!dispatchSet(m_staticSet, "Static")) + return false; + if (!dispatchSet(m_dynamicSet, "Dynamic")) + return false; + if (!dispatchSet(m_transparentSet, "Transparent")) + return false; + return true; +} + +bool GPUCullingManager::DispatchLightFrustumCastersToSlot(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const Fmatrix& lightVP, float radiusInflate, u32 slot) +{ + if (!cmdList || !nvDevice || slot >= m_localShadowCullSlotCount) + return false; + if (!EnsureLightShadowCullPipeline(nvDevice)) + return false; + + auto* csRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("light_shadow_cull", ".cs"); + if (!csRefl) + return false; + + struct LightShadowCullParamsCB + { + Fvector4 frustumPlanes[6]; + u32 objectCount; + float radiusInflate; + u32 maxOut; + u32 pad; + }; + + Fmatrix vp = lightVP; + Fvector4 planes[6]; + ExtractFrustumPlanes(vp, planes); + + CullSetBuffers* srcSets[3] = {&m_staticSet, &m_dynamicSet, &m_transparentSet}; + auto& slotSets = m_localShadowCullSlots[slot].sets; + auto& cache = framegraph::GetPassResourceCache(); + + for (u32 si = 0; si < 3; ++si) + { + CullSetBuffers& set = *srcSets[si]; + auto& out = slotSets[si]; + if (set.objectCount == 0) + { + const u32 zero = 0; + if (out.count) + cmdList->writeBuffer(out.count, &zero, sizeof(u32)); + continue; + } + if (!set.objectBuffer || !set.drawArgsBuffer || !set.materialIDBuffer) + return false; + if (!out.indices || !out.drawArgs || !out.mats || !out.count) + return false; + + LightShadowCullParamsCB cb{}; + for (u32 i = 0; i < 6; ++i) + cb.frustumPlanes[i] = planes[i]; + cb.objectCount = set.objectCount; + cb.radiusInflate = radiusInflate; + cb.maxOut = kLocalShadowCullSlotCap; + cmdList->writeBuffer(m_lightShadowCullParamsCB, &cb, sizeof(cb)); + + const u32 zero = 0; + cmdList->writeBuffer(out.count, &zero, sizeof(u32)); + + cmdList->setBufferState(out.indices, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(out.drawArgs, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(out.mats, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(out.count, nvrhi::ResourceStates::UnorderedAccess); + + nvrhi::IBuffer* srcObj = set.objectBuffer.Get(); + if (!out.cullBinding || out.cullSrcObject != srcObj) + { + framegraph::BindingSetBuilder bsb(*csRefl, nvDevice, "LightShadowCullSlot"); + bsb.ConstantBuffer("LightShadowCullParams", m_lightShadowCullParamsCB); + bsb.BufferSRV("g_Objects", set.objectBuffer); + bsb.BufferSRV("g_InputDrawArgs", set.drawArgsBuffer); + bsb.BufferSRV("g_InputMaterialIDs", set.materialIDBuffer); + bsb.BufferUAV("g_OutIndices", out.indices); + bsb.BufferUAV("g_OutDrawArgs", out.drawArgs); + bsb.BufferUAV("g_OutMaterialIDs", out.mats); + bsb.BufferUAV("g_OutCount", out.count); + out.cullBinding = cache.GetOrCreateBindingSet(bsb.Build(), m_lightShadowCullLayout, nvDevice); + out.cullSrcObject = out.cullBinding ? srcObj : nullptr; + if (!out.cullBinding) + return false; + } + + nvrhi::ComputeState state; + state.pipeline = m_lightShadowCullPipeline; + state.bindings = {out.cullBinding}; + cmdList->setComputeState(state); + cmdList->dispatch((set.objectCount + 63) / 64, 1, 1); + + cmdList->setBufferState(out.indices, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(out.drawArgs, nvrhi::ResourceStates::IndirectArgument); + cmdList->setBufferState(out.mats, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(out.count, nvrhi::ResourceStates::IndirectArgument); + } + return true; +} + +bool GPUCullingManager::DispatchAllLocalShadowTileCulls( + nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const xr_vector& tiles, float radiusInflate) +{ + ZoneScopedN("DispatchAllLocalShadowTileCulls"); + if (!cmdList || !nvDevice || tiles.empty()) + return false; + if (!EnsureLightShadowCullPipeline(nvDevice)) + return false; + + auto* csRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("light_shadow_cull", ".cs"); + if (!csRefl) + return false; + + CullSetBuffers* srcSets[3] = {&m_staticSet, &m_dynamicSet, &m_transparentSet}; + bool anySrc = false; + for (u32 si = 0; si < 3; ++si) + { + CullSetBuffers& set = *srcSets[si]; + if (set.objectCount == 0 || !set.objectBuffer || !set.drawArgsBuffer || !set.materialIDBuffer) + continue; + cmdList->setBufferState(set.objectBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.drawArgsBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.materialIDBuffer, nvrhi::ResourceStates::ShaderResource); + anySrc = true; + } + if (!anySrc) + return false; + + struct LightShadowCullParamsCB + { + Fvector4 frustumPlanes[6]; + u32 objectCount; + float radiusInflate; + u32 maxOut; + u32 pad; + }; + + struct CullJob + { + u32 slot = 0; + u32 setIdx = 0; + u32 objectCount = 0; + Fvector4 planes[6]{}; + }; + static thread_local xr_vector s_jobs; + static thread_local xr_vector s_outs; + static thread_local xr_vector s_zeroCounts; + auto& jobs = s_jobs; + auto& outs = s_outs; + auto& zeroCounts = s_zeroCounts; + jobs.clear(); + outs.clear(); + zeroCounts.clear(); + + const u32 maxSlots = GetLocalShadowCullSlotCount(); + const u32 zero = 0; + bool anyTile = false; + for (u32 i = 0; i < tiles.size() && i < maxSlots; ++i) + { + const LocalShadowTile& tile = tiles[i]; + if (tile.size < 8 || !tile.needsRedraw) + continue; + anyTile = true; + + Fmatrix vp = tile.clipVP; + Fvector4 planes[6]; + ExtractFrustumPlanes(vp, planes); + + auto& slotSets = m_localShadowCullSlots[i].sets; + for (u32 si = 0; si < 3; ++si) + { + CullSetBuffers& set = *srcSets[si]; + auto& out = slotSets[si]; + if (!out.indices || !out.drawArgs || !out.mats || !out.count) + return false; + cmdList->writeBuffer(out.count, &zero, sizeof(u32)); + if (set.objectCount == 0) + { + zeroCounts.push_back(out.count.Get()); + continue; + } + CullJob job{}; + job.slot = i; + job.setIdx = si; + job.objectCount = set.objectCount; + for (u32 p = 0; p < 6; ++p) + job.planes[p] = planes[p]; + jobs.push_back(job); + outs.push_back(&out); + } + } + if (!anyTile) + return false; + if (jobs.empty()) + { + for (nvrhi::IBuffer* countBuf : zeroCounts) + { + if (countBuf) + cmdList->setBufferState(countBuf, nvrhi::ResourceStates::IndirectArgument); + } + return true; + } + + auto& cache = framegraph::GetPassResourceCache(); + for (u32 ji = 0; ji < jobs.size(); ++ji) + { + LocalShadowCullSlotSet& out = *outs[ji]; + cmdList->setBufferState(out.indices, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(out.drawArgs, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(out.mats, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(out.count, nvrhi::ResourceStates::UnorderedAccess); + } + + for (u32 ji = 0; ji < jobs.size(); ++ji) + { + const CullJob& job = jobs[ji]; + CullSetBuffers& set = *srcSets[job.setIdx]; + LocalShadowCullSlotSet& out = *outs[ji]; + + LightShadowCullParamsCB cb{}; + for (u32 p = 0; p < 6; ++p) + cb.frustumPlanes[p] = job.planes[p]; + cb.objectCount = job.objectCount; + cb.radiusInflate = radiusInflate; + cb.maxOut = kLocalShadowCullSlotCap; + cmdList->writeBuffer(m_lightShadowCullParamsCB, &cb, sizeof(cb)); + + nvrhi::IBuffer* srcObj = set.objectBuffer.Get(); + if (!out.cullBinding || out.cullSrcObject != srcObj) + { + framegraph::BindingSetBuilder bsb(*csRefl, nvDevice, "LightShadowCullSlot"); + bsb.ConstantBuffer("LightShadowCullParams", m_lightShadowCullParamsCB); + bsb.BufferSRV("g_Objects", set.objectBuffer); + bsb.BufferSRV("g_InputDrawArgs", set.drawArgsBuffer); + bsb.BufferSRV("g_InputMaterialIDs", set.materialIDBuffer); + bsb.BufferUAV("g_OutIndices", out.indices); + bsb.BufferUAV("g_OutDrawArgs", out.drawArgs); + bsb.BufferUAV("g_OutMaterialIDs", out.mats); + bsb.BufferUAV("g_OutCount", out.count); + out.cullBinding = cache.GetOrCreateBindingSet(bsb.Build(), m_lightShadowCullLayout, nvDevice); + out.cullSrcObject = out.cullBinding ? srcObj : nullptr; + if (!out.cullBinding) + return false; + } + + nvrhi::ComputeState state; + state.pipeline = m_lightShadowCullPipeline; + state.bindings = {out.cullBinding}; + cmdList->setComputeState(state); + cmdList->dispatch((job.objectCount + 63) / 64, 1, 1); + } + + for (u32 ji = 0; ji < jobs.size(); ++ji) + { + LocalShadowCullSlotSet& out = *outs[ji]; + cmdList->setBufferState(out.indices, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(out.drawArgs, nvrhi::ResourceStates::IndirectArgument); + cmdList->setBufferState(out.mats, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(out.count, nvrhi::ResourceStates::IndirectArgument); + } + for (nvrhi::IBuffer* countBuf : zeroCounts) + { + if (countBuf) + cmdList->setBufferState(countBuf, nvrhi::ResourceStates::IndirectArgument); + } + return true; +} + void GPUCullingManager::EnsureShadowCasterIdentityBuffers( nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice) { @@ -3077,7 +3737,8 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( const GeometryCollector* geometry, const Fmatrix& prevViewProj, bool forceDisableHiz, - bool skipUpload) + bool skipUpload, + framegraph::VirtualResourceHandle waitFor) { using namespace framegraph; @@ -3120,6 +3781,8 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( VirtualResourceHandle hizPyramid; VirtualResourceHandle staticDrawArgsBuffer; VirtualResourceHandle dynamicDrawArgsBuffer; + VirtualResourceHandle staticCompactDrawArgsBuffer; + VirtualResourceHandle dynamicCompactDrawArgsBuffer; GPUCullingManager* manager; const GeometryCollector* geometry; @@ -3148,6 +3811,25 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( skipUpload ? "gpu_cull_dynamic_drawargs_hiz" : "gpu_cull_dynamic_drawargs", m_dynamicSet.drawArgsBuffer, drawArgsDesc); + VirtualResourceHandle staticCompactDrawArgsHandle; + VirtualResourceHandle dynamicCompactDrawArgsHandle; + if (m_compactEnabled && m_staticSet.compactDrawArgsBuffer && m_dynamicSet.compactDrawArgsBuffer) + { + ResourceDesc compactArgsDesc; + compactArgsDesc.type = ResourceDesc::Type::Buffer; + compactArgsDesc.debugName = "GPUCull_CompactDrawArgs"; + compactArgsDesc.bufferSize = m_maxObjects * sizeof(IndirectDrawArgs); + compactArgsDesc.isUAV = true; + compactArgsDesc.isTransient = false; + + staticCompactDrawArgsHandle = fg.ImportBuffer( + skipUpload ? "gpu_cull_static_compact_drawargs_hiz" : "gpu_cull_static_compact_drawargs", + m_staticSet.compactDrawArgsBuffer, compactArgsDesc); + dynamicCompactDrawArgsHandle = fg.ImportBuffer( + skipUpload ? "gpu_cull_dynamic_compact_drawargs_hiz" : "gpu_cull_dynamic_compact_drawargs", + m_dynamicSet.compactDrawArgsBuffer, compactArgsDesc); + } + // Ensure Hi-Z handle is valid for binding (dummy 1x1 when pyramid missing) VirtualResourceHandle hizHandle = hizPyramid; u32 effHizW = hizWidth; @@ -3174,10 +3856,10 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( skipUpload ? "GPU Culling HiZ" : "GPU Culling", // Setup lambda - [&, effHizW, effHizH, effHizMips, staticDrawArgsHandle, dynamicDrawArgsHandle, geometry, prevViewProj, forceDisableHiz, skipUpload, hizHandle](FrameGraph& builder, PassHandle passHandle, GPUCullPassData& data) { + [&, effHizW, effHizH, effHizMips, staticDrawArgsHandle, dynamicDrawArgsHandle, + staticCompactDrawArgsHandle, dynamicCompactDrawArgsHandle, + geometry, prevViewProj, forceDisableHiz, skipUpload, hizHandle, waitFor](FrameGraph& builder, PassHandle passHandle, GPUCullPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); - // Graphics queue: asyncCompute left Forward/Skinning sampling compact - // buffers without FG barriers → terrain flicker / empty draws (MoltenVK). passBuilder.sideEffects(); data.manager = this; @@ -3189,9 +3871,15 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( data.forceDisableHiz = forceDisableHiz; data.skipUpload = skipUpload; + if (waitFor.is_valid()) + passBuilder.read(waitFor, ResourceState::IndirectArgument); data.hizPyramid = passBuilder.read(hizHandle, ResourceState::ShaderResource); data.staticDrawArgsBuffer = passBuilder.write(staticDrawArgsHandle, ResourceState::UnorderedAccess); data.dynamicDrawArgsBuffer = passBuilder.write(dynamicDrawArgsHandle, ResourceState::UnorderedAccess); + if (staticCompactDrawArgsHandle.is_valid()) + data.staticCompactDrawArgsBuffer = passBuilder.write(staticCompactDrawArgsHandle, ResourceState::UnorderedAccess); + if (dynamicCompactDrawArgsHandle.is_valid()) + data.dynamicCompactDrawArgsBuffer = passBuilder.write(dynamicCompactDrawArgsHandle, ResourceState::UnorderedAccess); }, // Execute lambda @@ -3244,9 +3932,9 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( R_ASSERT2(set.objectBuffer && set.visibleIndexBuffer && set.visibleCountBuffer && set.visibilityBuffer, "Cull set buffers not initialized"); - // Clear visible count to 0 u32 zero = 0; cmdList->writeBuffer(set.visibleCountBuffer, &zero, sizeof(u32)); + cmdList->clearBufferUInt(set.visibilityBuffer, 0); // Fill constant buffer CullParamsCB cb; @@ -3278,7 +3966,7 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( .BufferUAV("g_VisibleCount", set.visibleCountBuffer) .BufferUAV("g_Visibility", set.visibilityBuffer); - nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_cullLayout); + nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_cullLayout, nvDevice); R_ASSERT2(bindingSet, "Failed to create culling binding set"); // Set compute state and dispatch culling @@ -3441,9 +4129,10 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( terrainCB.hizHeight = data.hizHeight; terrainCB.hizMipLevels = data.hizMipLevels; terrainCB.frameId = frameId; - // Terrain: frustum/distance only. Temporal Hi-Z on the ground plane - // false-culls patches at mip boundaries → visible flicker. - terrainCB.hizEnable = 0u; + // Terrain: same-frame Hi-Z with conservative AABB (Cull2 after prepass). + // Keep off for temporal/dummy (width<=1) — mip flicker on ground plane. + terrainCB.hizEnable = (data.forceDisableHiz || ps_r_hiz_occlusion == 0 || + data.hizWidth <= 1) ? 0u : 1u; terrainCB.padding[0] = terrainCB.padding[1] = 0; mgr->ExtractFrustumPlanes(Device.mFullTransform, terrainCB.frustumPlanes); cmdList->writeBuffer(mgr->m_device->GetNativeBuffer(mgr->m_cullParamsCB), &terrainCB, sizeof(terrainCB)); @@ -3457,7 +4146,7 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( .BufferUAV("g_VisibleCount", mgr->m_terrainVisibleCountBuffer) .BufferUAV("g_Visibility", mgr->m_terrainVisibilityBuffer); - nvrhi::BindingSetHandle terrainBindingSet = nvDevice->createBindingSet(terrainBsb.Build(), mgr->m_cullLayout); + nvrhi::BindingSetHandle terrainBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(terrainBsb.Build(), mgr->m_cullLayout, nvDevice); R_ASSERT2(terrainBindingSet, "Terrain culling binding set creation failed"); nvrhi::ComputeState terrainState; @@ -3606,7 +4295,10 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( output.visibleIndices = VirtualResourceHandle(); // Not using framegraph for these output.visibleCount = VirtualResourceHandle(); - output.drawArgsBuffer = passData.staticDrawArgsBuffer; + // Forward/Depth sample compact draw-args; expose that producer edge (not the pre-compact buffer). + output.drawArgsBuffer = passData.staticCompactDrawArgsBuffer.is_valid() + ? passData.staticCompactDrawArgsBuffer + : passData.staticDrawArgsBuffer; output.staticDrawArgsBuffer = passData.staticDrawArgsBuffer; output.dynamicDrawArgsBuffer = passData.dynamicDrawArgsBuffer; output.staticObjectCount = m_staticSet.objectCount; @@ -3614,13 +4306,8 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( // Set compact output handles if compaction is enabled if (m_compactEnabled) { - // Import compact buffers into framegraph - ResourceDesc compactArgsDesc; - compactArgsDesc.type = ResourceDesc::Type::Buffer; - compactArgsDesc.debugName = "GPUCull_CompactDrawArgs"; - compactArgsDesc.bufferSize = m_maxObjects * sizeof(IndirectDrawArgs); - compactArgsDesc.isUAV = true; - compactArgsDesc.isTransient = false; + output.staticCompactDrawArgs = passData.staticCompactDrawArgsBuffer; + output.dynamicCompactDrawArgs = passData.dynamicCompactDrawArgsBuffer; ResourceDesc compactIndicesDesc; compactIndicesDesc.type = ResourceDesc::Type::Buffer; @@ -3630,11 +4317,12 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( compactIndicesDesc.isUAV = true; compactIndicesDesc.isTransient = false; - output.staticCompactDrawArgs = fg.ImportBuffer("gpu_cull_static_compact_drawargs", m_staticSet.compactDrawArgsBuffer, compactArgsDesc); - output.staticCompactBatchIndices = fg.ImportBuffer("gpu_cull_static_compact_batchindices", m_staticSet.compactBatchIndicesBuffer, compactIndicesDesc); - - output.dynamicCompactDrawArgs = fg.ImportBuffer("gpu_cull_dynamic_compact_drawargs", m_dynamicSet.compactDrawArgsBuffer, compactArgsDesc); - output.dynamicCompactBatchIndices = fg.ImportBuffer("gpu_cull_dynamic_compact_batchindices", m_dynamicSet.compactBatchIndicesBuffer, compactIndicesDesc); + output.staticCompactBatchIndices = fg.ImportBuffer( + skipUpload ? "gpu_cull_static_compact_batchindices_hiz" : "gpu_cull_static_compact_batchindices", + m_staticSet.compactBatchIndicesBuffer, compactIndicesDesc); + output.dynamicCompactBatchIndices = fg.ImportBuffer( + skipUpload ? "gpu_cull_dynamic_compact_batchindices_hiz" : "gpu_cull_dynamic_compact_batchindices", + m_dynamicSet.compactBatchIndicesBuffer, compactIndicesDesc); } else { output.staticCompactDrawArgs = VirtualResourceHandle(); output.staticCompactBatchIndices = VirtualResourceHandle(); @@ -3950,7 +4638,7 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( .BufferUAV("g_VisibleCount", mgr->m_skinnedVisibleCountBuffer) .BufferUAV("g_Visibility", visibility); - nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_cullLayout); + nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_cullLayout, nvDevice); R_ASSERT2(bindingSet, "Failed to create skinned culling binding set"); nvrhi::ComputeState state; @@ -3982,7 +4670,7 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( .BufferSRV("g_Visibility", mgr->m_skinnedVisibilityBuffer) .BufferUAV("g_DrawArgs", mgr->m_skinnedDrawArgsBuffer); - nvrhi::BindingSetHandle gateBindingSet = nvDevice->createBindingSet(gateBsb.Build(), mgr->m_skinnedArgsGateLayout); + nvrhi::BindingSetHandle gateBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(gateBsb.Build(), mgr->m_skinnedArgsGateLayout, nvDevice); R_ASSERT2(gateBindingSet, "Failed to create skinned args gate binding set"); nvrhi::ComputeState gateState; @@ -4268,7 +4956,7 @@ void GPUCullingManager::SetupDebugVisualizationPass( .Texture("g_HiZPyramid", hizTexture) .BufferUAV("g_DebugOutput", mgr->m_debugBuffer); - nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_debugComputeLayout); + nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_debugComputeLayout, nvDevice); R_ASSERT2(bindingSet, "Debug binding set creation failed"); nvrhi::ComputeState state; @@ -4326,7 +5014,7 @@ void GPUCullingManager::SetupDebugVisualizationPass( .Texture("g_HiZPyramid", hizTexture) .BufferUAV("g_DebugOutput", mgr->m_debugBuffer); - nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_debugComputeLayout); + nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_debugComputeLayout, nvDevice); nvrhi::ComputeState state; state.pipeline = mgr->m_particleDebugComputePipeline; @@ -4356,7 +5044,7 @@ void GPUCullingManager::SetupDebugVisualizationPass( bsb.ConstantBuffer("CullDebugVSParams", mgr->m_device->GetNativeBuffer(mgr->m_debugGraphicsParamsCB)) .BufferSRV("g_DebugData", mgr->m_debugBuffer); - nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_debugGraphicsLayout); + nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_debugGraphicsLayout, nvDevice); nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(colorTexture); @@ -4689,7 +5377,7 @@ GPUParticleCullOutput GPUCullingManager::SetupParticleCullingPass( .BufferUAV("g_VisibleIndices", mgr->m_particleVisibleCountBuffer) .BufferUAV("g_VisibleCount", mgr->m_particleDrawArgsBuffer); - nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_particleCullLayout); + nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_particleCullLayout, nvDevice); if (!bindingSet) { Msg("! [GPUCulling] Failed to create particle binding set"); return; diff --git a/src/Layers/xrRender/GPUCullingManager.h b/src/Layers/xrRender/GPUCullingManager.h index 961e568c584..143e64752c4 100644 --- a/src/Layers/xrRender/GPUCullingManager.h +++ b/src/Layers/xrRender/GPUCullingManager.h @@ -29,6 +29,7 @@ namespace xray::render::fg { class dxRender_Visual; // Forward declaration for visual pointer map class CKinematics; class RTAccelStructManager; + struct LocalShadowTile; namespace decals { class OverlayManager; } @@ -190,10 +191,12 @@ class GPUCullingManager { // Shutdown and release resources void Shutdown(); - // Upload scene objects to GPU (call once per frame before culling) - // Extracts bounding sphere data from geometry batches void UploadSceneObjects(fg::RenderContext* ctx, const GeometryCollector* geometry); + void SetStaticBatchSource(const xr_vector* batches) { m_staticBatchSource = batches; } + bool IsStaticDataCached() const { return m_staticDataCached; } + bool IsTerrainDataCached() const { return m_terrainDataCached; } + void InvalidateStaticCullingData(); void InvalidateShadersAndPipelines(); @@ -207,9 +210,10 @@ class GPUCullingManager { u32 hizHeight, u32 hizMipLevels, const GeometryCollector* geometry, - const Fmatrix& prevViewProj, // Previous frame's viewProj for temporal Hi-Z + const Fmatrix& prevViewProj, bool forceDisableHiz = false, - bool skipUpload = false + bool skipUpload = false, + framegraph::VirtualResourceHandle waitFor = {} ); nvrhi::ITexture* GetDummyHiZTexture() const { return m_dummyHizTexture.Get(); } @@ -346,10 +350,29 @@ class GPUCullingManager { // Light-frustum CSM caster compact (CPU filter + upload). Call once per cascade before draw. void BuildLightFrustumCasters(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const Fmatrix& lightVP, float radiusInflate = 1.25f, float skipNearCameraClearance = 0.f); + + bool DispatchLightFrustumCasters(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, const Fmatrix& lightVP, float radiusInflate = 1.25f); - /// Ensure identity shadow-caster index/count buffers exist (for cast-all fallback). - /// Does not frustum-filter — safe to call before local-shadow cast-all draws. + bool EnsureLightShadowCullPipeline(nvrhi::IDevice* nvDevice); + bool EnsureLocalShadowCullSlots(nvrhi::IDevice* nvDevice, u32 slotCount); + bool DispatchLightFrustumCastersToSlot(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const Fmatrix& lightVP, float radiusInflate, u32 slot); + bool DispatchAllLocalShadowTileCulls(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const xr_vector& tiles, float radiusInflate); + + static constexpr u32 kLocalShadowCullMaxSlots = 256; + static constexpr u32 kLocalShadowCullSlotCap = 4096; + + nvrhi::IBuffer* GetLocalShadowSlotIndices(u32 slot, u32 setIdx) const; + nvrhi::IBuffer* GetLocalShadowSlotDrawArgs(u32 slot, u32 setIdx) const; + nvrhi::IBuffer* GetLocalShadowSlotMats(u32 slot, u32 setIdx) const; + nvrhi::IBuffer* GetLocalShadowSlotCount(u32 slot, u32 setIdx) const; + nvrhi::BindingSetHandle* MutLocalShadowSlotDrawBinding(u32 slot, u32 setIdx); + nvrhi::IBuffer** MutLocalShadowSlotDrawSrcInstance(u32 slot, u32 setIdx); + u32 GetLocalShadowCullSlotCount() const { return m_localShadowCullSlotCount; } + void EnsureShadowCasterIdentityBuffers(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice); nvrhi::IBuffer* GetStaticShadowCompactDrawArgsBuffer() const { return m_staticSet.shadowCompactDrawArgsBuffer.Get(); } @@ -381,6 +404,7 @@ class GPUCullingManager { nvrhi::IBuffer* GetTessInstanceBuffer() const { return m_tessSet.instanceBuffer.Get(); } nvrhi::IBuffer* GetTessMaterialIDBuffer() const { return m_tessSet.materialIDBuffer.Get(); } nvrhi::IBuffer* GetTessBatchIndicesBuffer() const { return m_tessBatchIndicesBuffer.Get(); } + nvrhi::IBuffer* GetTessDrawArgsBuffer() const { return m_tessSet.drawArgsBuffer.Get(); } const xr_vector& GetTessDrawArgsData() const { return m_tessDrawArgsData; } const xr_vector& GetTessObjectData() const { return m_tessObjectData; } @@ -580,8 +604,31 @@ class GPUCullingManager { nvrhi::ComputePipelineHandle m_compactCountPipeline; nvrhi::ComputePipelineHandle m_compactScanPipeline; nvrhi::ComputePipelineHandle m_compactScatterPipeline; + nvrhi::ComputePipelineHandle m_lightShadowCullPipeline; nvrhi::BindingLayoutHandle m_cullLayout; nvrhi::BindingLayoutHandle m_clearArgsLayout; + nvrhi::BindingLayoutHandle m_lightShadowCullLayout; + nvrhi::BufferHandle m_lightShadowCullParamsCB; + bool m_lightShadowCullReady = false; + + struct LocalShadowCullSlotSet + { + nvrhi::BufferHandle indices; + nvrhi::BufferHandle drawArgs; + nvrhi::BufferHandle mats; + nvrhi::BufferHandle count; + nvrhi::BindingSetHandle cullBinding; + nvrhi::BindingSetHandle drawBinding; + nvrhi::IBuffer* cullSrcObject = nullptr; + nvrhi::IBuffer* drawSrcInstance = nullptr; + }; + struct LocalShadowCullSlot + { + LocalShadowCullSlotSet sets[3]; + }; + xr_vector m_localShadowCullSlots; + u32 m_localShadowCullSlotCount = 0; + nvrhi::BindingLayoutHandle m_compactCountLayout; nvrhi::BindingLayoutHandle m_compactScanLayout; nvrhi::BindingLayoutHandle m_compactScatterLayout; @@ -606,11 +653,17 @@ class GPUCullingManager { const CullSetBuffers& set, VariantPartitionBuffers& partition); - bool m_staticTerrainDrawArgsUploaded = false; // True after first upload (terrain) + bool m_staticTerrainDrawArgsUploaded = false; + bool m_terrainDataCached = false; + u32 m_lastMaterialCountForStatic = 0; + u64 m_transientUploadFingerprint = 0; + bool m_transientDataCached = false; + bool m_lastTessFlagOn = false; bool m_compactEnabled = false; bool m_staticDataCached = false; + const xr_vector* m_staticBatchSource = nullptr; // ─────────────────────────────────────────────────────── // TERRAIN-SPECIFIC BUFFERS diff --git a/src/Layers/xrRender/Geometry/GeometryBatch.cpp b/src/Layers/xrRender/Geometry/GeometryBatch.cpp index 376ad2da157..1fecfd89798 100644 --- a/src/Layers/xrRender/Geometry/GeometryBatch.cpp +++ b/src/Layers/xrRender/Geometry/GeometryBatch.cpp @@ -4,11 +4,10 @@ namespace xray::render { -// Global geometry collector instance (to be initialized by renderer) GeometryCollector* g_geometryCollector = nullptr; GeometryCollector::GeometryCollector() { - m_batches.reserve(4096); // Pre-allocate for typical scene + m_batches.reserve(16384); Msg("* [GeometryCollector] Created"); } @@ -17,87 +16,120 @@ GeometryCollector::~GeometryCollector() { } void GeometryCollector::BeginFrame() { - // Clear previous frame's batches m_batches.clear(); - - // Reset statistics + if (m_batches.capacity() < 16384) + m_batches.reserve(16384); m_stats = Stats{}; } void GeometryCollector::EndFrame() { - // Update statistics m_stats.numBatches = static_cast(m_batches.size()); } void GeometryCollector::Submit(const GeometryBatch& batch) { - VERIFY(batch.vertexBuffer != nullptr); // nvrhi::BufferHandle is a smart pointer + VERIFY(batch.vertexBuffer != nullptr); VERIFY(batch.indexBuffer != nullptr); VERIFY(batch.indexCount > 0); - // NOTE: pipeline can be nullptr during collection, will be set later from visual->shader m_batches.push_back(batch); } +void GeometryCollector::SubmitStatic(const GeometryBatch& s) { + VERIFY(s.vertexBuffer != nullptr); + VERIFY(s.indexBuffer != nullptr); + VERIFY(s.indexCount > 0); + + GeometryBatch& d = m_batches.emplace_back(); + d.vertexBuffer = s.vertexBuffer; + d.indexBuffer = s.indexBuffer; + d.indexCount = s.indexCount; + d.startIndex = s.startIndex; + d.baseVertex = s.baseVertex; + d.vertexStride = s.vertexStride; + d.materialID = s.materialID; + d.bindlessMaterialID = s.bindlessMaterialID; + d.albedoTexture = s.albedoTexture; + d.normalTexture = s.normalTexture; + d.materialTexture = s.materialTexture; + d.worldMatrix = s.worldMatrix; + d.worldBoundsCenter = s.worldBoundsCenter; + d.worldBoundsRadius = s.worldBoundsRadius; + d.pipeline = s.pipeline; + d.bindingSet = s.bindingSet; + d.materialPSO = s.materialPSO; + d.renderPhase = s.renderPhase; + d.visual = s.visual; + d.renderable = s.renderable; + d.isVisible = s.isVisible; + d.isStatic = s.isStatic; + d.isSkinned = s.isSkinned; + d.skinningRenderMode = s.skinningRenderMode; + d.skinnedPoolFormat = s.skinnedPoolFormat; + d.skinnedPoolBaseVertex = s.skinnedPoolBaseVertex; + d.skinnedPoolFirstIndex = s.skinnedPoolFirstIndex; + d.isTerrain = s.isTerrain; + d.terrainMaterialID = s.terrainMaterialID; + d.cachedAlphaTest = s.cachedAlphaTest; + d.cachedTransparent = s.cachedTransparent; + d.sortFlagsValid = s.sortFlagsValid; + d.ssa = s.ssa; + d.megaBufferAlloc = s.megaBufferAlloc; +} + void GeometryCollector::Sort() { - // ═══════════════════════════════════════════════════════ - // RENDER ORDER SORTING (using SSA + shader flags) - // ═══════════════════════════════════════════════════════ - // Proper render order for forward rendering: - // 1. Opaque (iPriority == 0) - SSA descending (front-to-back for early-Z) - // 2. Alpha-tested (iPriority == 1) - SSA descending (after opaque fills depth) - // 3. Transparent (bStrictB2F) - SSA ascending (back-to-front for blending) - // - // SSA = R / distSQ (larger = closer/bigger = more visually important) - // SSA descending = front-to-back, SSA ascending = back-to-front - - std::sort(m_batches.begin(), m_batches.end(), - [](const GeometryBatch& a, const GeometryBatch& b) { - // Get sorting properties using member functions - bool aB2F = a.IsStrictB2F(); - bool bB2F = b.IsStrictB2F(); - bool aAref = a.IsAlphaTested(); - bool bAref = b.IsAlphaTested(); - - // 1. Transparent (bStrictB2F) batches render LAST - if (aB2F != bB2F) { - return !aB2F; // Non-B2F comes before B2F - } - - // 2. For transparent batches: sort by SSA ascending (back-to-front) - if (aB2F && bB2F) { - return a.ssa < b.ssa; // Ascending SSA = back-to-front - } - - // 3. Alpha-tested renders AFTER opaque - // This ensures opaque geometry fills depth buffer first, - // so clip() in alpha-tested shaders shows correct background. - if (aAref != bAref) { - return !aAref; // Non-aref (opaque) comes before aref - } - - // 4. Within same category: sort by SSA descending (front-to-back) - // This matches vanilla's cmp_ssa: return lhs.ssa > rhs.ssa - return a.ssa > b.ssa; // Descending SSA = front-to-back - }); + if (m_batches.empty()) + return; + + for (auto& batch : m_batches) + { + if (!batch.sortFlagsValid) + batch.CacheSortFlags(); + } + + const u32 n = static_cast(m_batches.size()); + static thread_local xr_vector order; + order.resize(n); + for (u32 i = 0; i < n; ++i) + order[i] = i; + + auto opaqueEnd = std::partition(order.begin(), order.end(), + [&](u32 i) { return !m_batches[i].cachedTransparent && !m_batches[i].cachedAlphaTest; }); + auto arefEnd = std::partition(opaqueEnd, order.end(), + [&](u32 i) { return !m_batches[i].cachedTransparent; }); + + auto bySsaAsc = [&](u32 a, u32 b) { return m_batches[a].ssa < m_batches[b].ssa; }; + std::sort(arefEnd, order.end(), bySsaAsc); + + bool identity = true; + for (u32 i = 0; i < n; ++i) + { + if (order[i] != i) + { + identity = false; + break; + } + } + if (identity) + return; + + static thread_local xr_vector sorted; + sorted.clear(); + sorted.reserve(n); + for (u32 i : order) + sorted.push_back(std::move(m_batches[i])); + m_batches.swap(sorted); } u64 GeometryCollector::ComputeSortKey(const GeometryBatch& batch) { - // Compute sort key (higher bits = more important) u64 key = 0; - // Bits 48-63: Pipeline (most important - avoid PSO changes) if (batch.pipeline) { u64 pipelineHash = reinterpret_cast(batch.pipeline) >> 4; key |= (pipelineHash & 0xFFFF) << 48; } - // Bits 32-47: Material ID key |= (static_cast(batch.materialID) & 0xFFFF) << 32; - - // Bits 16-31: Albedo texture key |= (batch.albedoTexture.index & 0xFFFF) << 16; - - // Bits 0-15: Normal texture key |= (batch.normalTexture.index & 0xFFFF); return key; diff --git a/src/Layers/xrRender/Geometry/GeometryBatch.h b/src/Layers/xrRender/Geometry/GeometryBatch.h index 4a6821455d7..ef9ce602c5c 100644 --- a/src/Layers/xrRender/Geometry/GeometryBatch.h +++ b/src/Layers/xrRender/Geometry/GeometryBatch.h @@ -23,9 +23,8 @@ struct MaterialPSO; // Forward declaration // ══════════════════════════════════════════════════════════ struct GeometryBatch { - // Vertex/index buffers (NVRHI handles for wrapped legacy buffers) - nvrhi::BufferHandle vertexBuffer; - nvrhi::BufferHandle indexBuffer; + nvrhi::IBuffer* vertexBuffer = nullptr; + nvrhi::IBuffer* indexBuffer = nullptr; // Draw parameters u32 indexCount = 0; @@ -97,6 +96,10 @@ struct GeometryBatch { bool isTerrain = false; u32 terrainMaterialID = UINT32_MAX; // Index into g_TerrainMaterials for terrain rendering + bool cachedAlphaTest = false; + bool cachedTransparent = false; + bool sortFlagsValid = false; + // SSA (Screen Space Area) for sorting - matches vanilla CalcSSA() // SSA = R / distSQ where R = bounding sphere radius, distSQ = distance squared to camera // Larger SSA = closer/bigger = should render first (front-to-back for opaque) @@ -117,26 +120,40 @@ struct GeometryBatch { // ═══════════════════════════════════════════════════ // Uses MaterialSystem for material flags - // Check if batch is alpha-tested (uses clip/discard in shader) bool IsAlphaTested() const { + if (sortFlagsValid) + return cachedAlphaTest; if (!visual || !visual->shaderName.size()) return false; - return MaterialSystem::Instance() .GetMaterialInfo(visual->shaderName) .alphaTest; } - // Check if batch requires back-to-front sorting (transparent/alpha-blended) bool IsStrictB2F() const { + if (sortFlagsValid) + return cachedTransparent; if (!visual || !visual->shaderName.size()) return false; - return MaterialSystem::Instance() .GetMaterialInfo(visual->shaderName) .transparent; } + void CacheSortFlags() { + if (!visual || !visual->shaderName.size()) + { + cachedAlphaTest = false; + cachedTransparent = false; + sortFlagsValid = true; + return; + } + const auto& info = MaterialSystem::Instance().GetMaterialInfo(visual->shaderName); + cachedAlphaTest = info.alphaTest; + cachedTransparent = info.transparent; + sortFlagsValid = true; + } + // Check if batch is opaque (no alpha-test and no strict B2F) bool IsOpaque() const { return !IsAlphaTested() && !IsStrictB2F(); @@ -156,10 +173,9 @@ class GeometryCollector { void BeginFrame(); void EndFrame(); - // Submit geometry for rendering void Submit(const GeometryBatch& batch); + void SubmitStatic(const GeometryBatch& batch); - // Get batches for rendering (const) const xr_vector& GetBatches() const { return m_batches; } // Get batches for routing (non-const, for Week 16 dynamic routing) diff --git a/src/Layers/xrRender/Geometry/MaterialCache.cpp b/src/Layers/xrRender/Geometry/MaterialCache.cpp index d9a42d888cc..1da87af9f8b 100644 --- a/src/Layers/xrRender/Geometry/MaterialCache.cpp +++ b/src/Layers/xrRender/Geometry/MaterialCache.cpp @@ -34,6 +34,8 @@ #include "Layers/xrRender/Materials/ShaderInfo.h" #include "Layers/xrRender/ShaderVariant/ShaderVariantRegistry.h" #include "Layers/xrRender/Bindless/VariantTextureBuffer.h" +#include "Layers/xrRender/GPUCullingManager.h" +#include "Layers/xrRender/r_FrameGraphRenderer.h" #include "xrEngine/xr_object.h" @@ -1063,6 +1065,10 @@ u32 MaterialCache::RegisterBindlessMaterial(MaterialPSO* matPSO) matData.detailBumpIndex = INVALID_TEXTURE_INDEX; matData.detailBumpXIndex = INVALID_TEXTURE_INDEX; matData.tessMethod = 0; + matData.sssMapIndex = INVALID_TEXTURE_INDEX; + matData.emissiveIntensity = 0.f; + matData._pad1 = 0; + matData._pad2 = 0; if (matPSO->pass) { fg::STextureList* texList = matPSO->pass->T._get(); @@ -1153,7 +1159,8 @@ u32 MaterialCache::PreRegisterTerrainMaterial(dxRender_Visual* visual) if (baseForScale.size() > 0) { matData.detailScale = GetDetailScale(baseForScale); } else { - matData.detailScale = 4.0f; + // Classic TextureDescr default / dt_params when unset + matData.detailScale = 1.0f; } u32 terrainMaterialID = terrainBuffer.RegisterMaterial(matData); @@ -1448,10 +1455,10 @@ u32 MaterialCache::PreRegisterBindlessMaterial(dxRender_Visual* visual) matData.detailBumpIndex = INVALID_TEXTURE_INDEX; matData.detailBumpXIndex = INVALID_TEXTURE_INDEX; matData.tessMethod = 0; - matData.normalXIndex = INVALID_TEXTURE_INDEX; - matData.detailBumpIndex = INVALID_TEXTURE_INDEX; - matData.detailBumpXIndex = INVALID_TEXTURE_INDEX; - matData.tessMethod = 0; + matData.sssMapIndex = INVALID_TEXTURE_INDEX; + matData.emissiveIntensity = 0.f; + matData._pad1 = 0; + matData._pad2 = 0; if (visual->textureName.size() > 0) { matData.detailScale = GetDetailScale(visual->textureName); @@ -1466,27 +1473,71 @@ u32 MaterialCache::PreRegisterBindlessMaterial(dxRender_Visual* visual) if (matInfo.transparent) { matData.flags |= MAT_FLAG_ALPHA_BLEND; } + if (matInfo.multiply) { + matData.flags |= MAT_FLAG_MULTIPLY; + } if (strstr(visual->shaderName.c_str(), "water") != nullptr) { matData.flags |= MAT_FLAG_WATER; - // Ensure water goes through transparent partition even if blender props are stale matData.flags |= MAT_FLAG_ALPHA_BLEND; } - // Trees / bushes / leaves — enable foliage SSS path + { + const char* sh = visual->shaderName.c_str(); + const char* tex = visual->textureName.size() ? visual->textureName.c_str() : ""; + const bool emissiveName = + strstr(sh, "selflight") || strstr(sh, "emissive") || strstr(sh, "glow") || + strstr(sh, "lightplane") || strstr(sh, "light_plane") || + strstr(tex, "glow") || strstr(tex, "selflight") || strstr(tex, "emissive") || + strstr(tex, "neon"); + if (emissiveName) + { + matData.flags |= MAT_FLAG_EMISSIVE; + float intens = 2.5f; + if (strstr(sh, "selflight")) + intens = 3.5f; + if (strstr(sh, "glow") || strstr(tex, "glow")) + intens = 4.5f; + matData.emissiveIntensity = intens; + } + } + // Foliage SSS: leaves/bushes only — not bark, LODs, cars, opaque trunks. + // Old matcher tagged every MT_TREE + lod\* + trees\* textures → green SSS on wood/vehicles. { const char* sh = visual->shaderName.c_str(); const char* tex = visual->textureName.size() ? visual->textureName.c_str() : ""; const u16 vtype = visual->getType(); const bool treeType = (vtype == MT_TREE_ST || vtype == MT_TREE_PM); - if (treeType || - strstr(sh, "trees") || strstr(sh, "tree\\") || strstr(sh, "tree/") || + const bool cutout = + (matData.flags & (MAT_FLAG_ALPHA_TEST | MAT_FLAG_ALPHA_BLEND)) != 0; + + const bool barkLike = + strstr(tex, "bark") || strstr(tex, "stvol") || strstr(tex, "trunk") || + strstr(tex, "kora") || strstr(tex, "brevno") || strstr(tex, "wood_") || + strstr(tex, "stem") || strstr(tex, "pen_") || strstr(sh, "trunk"); + + const bool leafTex = + strstr(tex, "leaves") || strstr(tex, "leaf") || strstr(tex, "vetka") || + strstr(tex, "foliage") || strstr(tex, "flora") || strstr(tex, "xvoy") || + strstr(tex, "needles"); + + const bool plantShader = strstr(sh, "bush") || strstr(sh, "flora") || strstr(sh, "leaf") || - strstr(sh, "vegetation") || strstr(sh, "lod\\") || strstr(sh, "lod/") || - strstr(tex, "trees\\") || strstr(tex, "trees/") || - strstr(tex, "bush") || strstr(tex, "flora") || strstr(tex, "leaves")) + strstr(sh, "vegetation") || + strstr(sh, "trees\\") || strstr(sh, "trees/") || + strstr(sh, "tree\\") || strstr(sh, "tree/"); + + // Leaves are alpha-cutout on tree/bush shaders; never match lod\ alone. + if (!barkLike && + ((cutout && (treeType || plantShader || leafTex)) || leafTex)) { matData.flags |= MAT_FLAG_FOLIAGE; } + + if (strstr(tex, "glass") || strstr(tex, "steklo") || strstr(tex, "window") || + strstr(sh, "glass") || strstr(sh, "steklo")) + { + matData.flags |= MAT_FLAG_GLASS; + } } matData.shaderVariant = matInfo.shaderVariant; if (matInfo.transparent) @@ -1516,16 +1567,37 @@ u32 MaterialCache::PreRegisterBindlessMaterial(dxRender_Visual* visual) return materialID; } -u32 MaterialCache::PreRegisterParticleMaterial(const shared_str& textureName, u8 blendMode) +u32 MaterialCache::PreRegisterParticleMaterial( + const shared_str& textureName, + u8 blendMode, + const char* shaderName, + const char* sixWayPosXYZ, + const char* sixWayNegXYZ) { using namespace fg::bindless; if (!textureName.size() || !textureName[0]) return UINT32_MAX; - // Key by texture + blend: SET needs aref 200, BLEND/ADD keep near-zero for soft fade. + u32 colorMode = 0; + bool hard = false; + if (shaderName && shaderName[0]) + { + if (strstr(shaderName, "particle_s-aadd") || strstr(shaderName, "s-aadd")) + colorMode = 3; + else if (strstr(shaderName, "particle_s-add") || strstr(shaderName, "s-add")) + colorMode = 2; + else if (strstr(shaderName, "particle_s-blend") || strstr(shaderName, "s-blend")) + colorMode = 1; + if (strstr(shaderName, "particle_hard") || strstr(shaderName, "_hard")) + hard = true; + } + string256 cacheKey; - xr_sprintf(cacheKey, "%s#%u", textureName.c_str(), (u32)blendMode); + xr_sprintf(cacheKey, "%s#%u#%u#%u#%s#%s", + textureName.c_str(), (u32)blendMode, colorMode, hard ? 1u : 0u, + sixWayPosXYZ ? sixWayPosXYZ : "", + sixWayNegXYZ ? sixWayNegXYZ : ""); shared_str key(cacheKey); auto it = m_particleTextureToMaterialID.find(key); @@ -1536,7 +1608,6 @@ u32 MaterialCache::PreRegisterParticleMaterial(const shared_str& textureName, u8 if (!materialBuffer.IsInitialized()) return UINT32_MAX; - // PARTICLE_BLEND_SET = 0 (classic deferred blender aref 200 + depth write) constexpr u8 kParticleBlendSet = 0; MaterialData matData = {}; matData.diffuseIndex = INVALID_TEXTURE_INDEX; @@ -1544,6 +1615,7 @@ u32 MaterialCache::PreRegisterParticleMaterial(const shared_str& textureName, u8 matData.detailIndex = INVALID_TEXTURE_INDEX; matData.pbrIndex = INVALID_TEXTURE_INDEX; matData.detailScale = 1.0f; + matData.sssMapIndex = INVALID_TEXTURE_INDEX; if (blendMode == kParticleBlendSet) { matData.alphaRef = 200.0f / 255.0f; @@ -1554,6 +1626,19 @@ u32 MaterialCache::PreRegisterParticleMaterial(const shared_str& textureName, u8 matData.alphaRef = 0.01f / 255.0f; matData.flags = 0; } + if (hard) + matData.flags |= MAT_FLAG_PARTICLE_HARD; + if (colorMode >= 2) + { + matData.flags |= MAT_FLAG_EMISSIVE; + matData.emissiveIntensity = (colorMode >= 3) ? 6.0f : 3.5f; + } + else if (shaderName && (strstr(shaderName, "glow") || strstr(shaderName, "emissive"))) + { + matData.flags |= MAT_FLAG_EMISSIVE; + matData.emissiveIntensity = 4.0f; + } + matData.tessMethod = colorMode; matData.shaderVariant = 0; u32 materialID = materialBuffer.RegisterMaterial(matData); @@ -1564,6 +1649,10 @@ u32 MaterialCache::PreRegisterParticleMaterial(const shared_str& textureName, u8 pending.materialID = materialID; pending.visual = nullptr; pending.textureName = textureName; + if (sixWayPosXYZ && sixWayPosXYZ[0]) + pending.sixWayPosXYZ = sixWayPosXYZ; + if (sixWayNegXYZ && sixWayNegXYZ[0]) + pending.sixWayNegXYZ = sixWayNegXYZ; m_pendingMaterials.push_back(pending); Msg("* [MaterialCache] PreRegisterParticle: matID=%u tex='%s' blend=%u aref=%.3f pending=%u", @@ -1578,14 +1667,15 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) { using namespace fg::bindless; - if (m_pendingMaterials.empty()) { - return; - } - auto& materialBuffer = MaterialBuffer::Instance(); if (!materialBuffer.IsInitialized()) return; + u32 processedCount = 0; + bool tessRoutingDirty = false; + + if (!m_pendingMaterials.empty()) + { resources::TextureManager* texManager = m_resourceManager ? m_resourceManager->GetTextureManager() : nullptr; if (!texManager) return; @@ -1596,8 +1686,6 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) return; } - u32 processedCount = 0; - for (const auto& pending : m_pendingMaterials) { dxRender_Visual* visual = pending.visual; u32 materialID = pending.materialID; @@ -1661,10 +1749,45 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) } } + if (pending.sixWayPosXYZ.size() && pending.sixWayPosXYZ[0]) { + resources::TextureHandle handle = texManager->LoadTexture(pending.sixWayPosXYZ.c_str()); + if (handle.IsValid()) { + nvrhi::ITexture* nvrhiTex = texManager->GetNVRHITexture(handle); + if (nvrhiTex) { + u32 descriptorIndex = backend->RegisterBindlessTexture(nvrhiTex); + if (descriptorIndex != INVALID_TEXTURE_INDEX) { + matData.normalIndex = descriptorIndex; + matData.flags |= MAT_FLAG_HAS_NORMAL; + updated = true; + } + } + } + } + if (pending.sixWayNegXYZ.size() && pending.sixWayNegXYZ[0]) { + resources::TextureHandle handle = texManager->LoadTexture(pending.sixWayNegXYZ.c_str()); + if (handle.IsValid()) { + nvrhi::ITexture* nvrhiTex = texManager->GetNVRHITexture(handle); + if (nvrhiTex) { + u32 descriptorIndex = backend->RegisterBindlessTexture(nvrhiTex); + if (descriptorIndex != INVALID_TEXTURE_INDEX) { + matData.detailIndex = descriptorIndex; + matData.flags |= MAT_FLAG_HAS_DETAIL; + updated = true; + } + } + } + } + auto& texDescMgr = TextureDescr; - shared_str bumpName = isWater ? shared_str("water\\water_normal") : texDescMgr.GetBumpName(diffuseName); + const bool hasSixWay = pending.sixWayPosXYZ.size() && pending.sixWayPosXYZ[0]; + shared_str bumpName = isWater ? shared_str("water\\water_normal") + : (hasSixWay ? shared_str() : texDescMgr.GetBumpName(diffuseName)); if (bumpName.size() && bumpName[0]) { resources::TextureHandle handle = texManager->LoadTexture(bumpName.c_str()); + if (isWater && !handle.IsValid()) + handle = texManager->LoadTexture("fx\\water_normal"); + if (isWater && !handle.IsValid()) + handle = texManager->LoadTexture("water\\water_water_bump"); if (handle.IsValid()) { nvrhi::ITexture* nvrhiTex = texManager->GetNVRHITexture(handle); if (nvrhiTex) { @@ -1677,6 +1800,21 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) } } + if (isWater) { + resources::TextureHandle dudv = texManager->LoadTexture("water\\water_dudv"); + if (dudv.IsValid()) { + nvrhi::ITexture* nvrhiTex = texManager->GetNVRHITexture(dudv); + if (nvrhiTex) { + u32 descriptorIndex = backend->RegisterBindlessTexture(nvrhiTex); + if (descriptorIndex != INVALID_TEXTURE_INDEX) { + matData.detailIndex = descriptorIndex; + matData.flags |= MAT_FLAG_HAS_DETAIL; + updated = true; + } + } + } + } + // bump# (s_bumpX) — dual-bump error + height for HM tess / POM if (!isWater) { string256 bumpXName; @@ -1698,7 +1836,7 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) LPCSTR detailTexName = nullptr; bool detailHasBump = false; - if (texDescMgr.GetDetailTexture(diffuseName, detailTexName)) { + if (!isWater && texDescMgr.GetDetailTexture(diffuseName, detailTexName)) { if (detailTexName && detailTexName[0]) { resources::TextureHandle handle = texManager->LoadTexture(detailTexName); if (handle.IsValid()) { @@ -1753,34 +1891,38 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) } } - // Tessellation method assignment (used only when r4_enable_tessellation is - // on — and that flag is forcibly cleared on Apple/MoltenVK). - // Classic: deffer_model / deffer_flat blender token. We approximate with - // models\ + bump# only (PN+HM). Level/trees use steep parallax instead. bool denyTess = isWater || - (matData.flags & (MAT_FLAG_FOLIAGE | MAT_FLAG_WATER | MAT_FLAG_TERRAIN | - MAT_FLAG_ALPHA_TEST | MAT_FLAG_ALPHA_BLEND)) != 0; - bool isModelShader = false; + (matData.flags & (MAT_FLAG_FOLIAGE | MAT_FLAG_WATER | MAT_FLAG_TERRAIN)) != 0; if (visual && visual->shaderName.size()) { const char* sh = visual->shaderName.c_str(); - if (strstr(sh, "models\\") || strstr(sh, "models/") || - strstr(sh, "model\\") || strstr(sh, "model/")) - isModelShader = true; if (strstr(sh, "flora") || strstr(sh, "tree") || strstr(sh, "grass") || - strstr(sh, "particle") || strstr(sh, "effects\\")) + strstr(sh, "particle") || strstr(sh, "effects\\") || strstr(sh, "effects/")) denyTess = true; } u32 wantMethod = 0; - if (!denyTess && isModelShader && (matData.flags & MAT_FLAG_HAS_NORMAL_X)) - wantMethod = 3; // TESS_PN_HM + if (!denyTess) + { + u32 blenderTess = 0; + if (visual && visual->shaderName.size() && + shader_info::GetShaderTessellationMethod(visual->shaderName.c_str(), blenderTess)) + wantMethod = blenderTess; + if (wantMethod >= 2 && (matData.flags & MAT_FLAG_HAS_NORMAL_X) == 0) + wantMethod = (wantMethod == 2) ? 0 : 1; + if (wantMethod == 3 && (matData.flags & MAT_FLAG_HAS_DETAIL) == 0) + wantMethod = 1; + if (wantMethod > 3) + wantMethod = 0; + } if (matData.tessMethod != wantMethod) { + if (wantMethod != 0 || matData.tessMethod != 0) + tessRoutingDirty = true; matData.tessMethod = wantMethod; updated = true; } - if (diffuseName.c_str() && diffuseName[0]) { + if (diffuseName.c_str() && diffuseName[0] && !isWater) { shared_str pbrName = texDescMgr.GetPBRName(diffuseName); if (!pbrName.empty()) { resources::TextureHandle handle = texManager->LoadTexture(pbrName.c_str()); @@ -1796,21 +1938,44 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) } } } + + shared_str sssName = texDescMgr.GetSSSName(diffuseName); + if (!sssName.empty()) { + resources::TextureHandle handle = texManager->LoadTexture(sssName.c_str()); + if (handle.IsValid()) { + nvrhi::ITexture* nvrhiTex = texManager->GetNVRHITexture(handle); + if (nvrhiTex) { + u32 descriptorIndex = backend->RegisterBindlessTexture(nvrhiTex); + if (descriptorIndex != INVALID_TEXTURE_INDEX) { + matData.sssMapIndex = descriptorIndex; + matData.flags |= MAT_FLAG_HAS_SSS_MAP; + updated = true; + static u32 s_sssBoundLog = 0; + if (s_sssBoundLog < 24) + { + Msg("* [SSS] bound map '%s' for '%s' idx=%u", + sssName.c_str(), diffuseName.c_str(), descriptorIndex); + ++s_sssBoundLog; + } + } + } + } + } } - // One-shot bump + tess diagnostic { static u32 s_bumpDbgCount = 0; static u32 s_tessMatCount = 0; if (matData.tessMethod != 0) s_tessMatCount++; if (s_bumpDbgCount < 40) { - Msg("* [BumpDbg] mat=%u diff='%s' bump='%s' nIdx=%u nXIdx=%u flags=0x%X tess=%u", - materialID, - diffuseName.size() ? diffuseName.c_str() : "", + u32 blenderTess = 0; + const char* sh = (visual && visual->shaderName.size()) ? visual->shaderName.c_str() : ""; + shader_info::GetShaderTessellationMethod(sh, blenderTess); + Msg("* [BumpDbg] mat=%u sh='%s' bump='%s' flags=0x%X tess=%u blenderTess=%u", + materialID, sh, bumpName.size() ? bumpName.c_str() : "(none)", - matData.normalIndex, matData.normalXIndex, matData.flags, - matData.tessMethod); + matData.flags, matData.tessMethod, blenderTess); s_bumpDbgCount++; if (s_bumpDbgCount == 40) Msg("* [TessDbg] materials with tessMethod!=0 so far: %u", s_tessMatCount); @@ -1853,11 +2018,82 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) } m_pendingMaterials.clear(); + } // !m_pendingMaterials.empty() + +#if !defined(XR_PLATFORM_APPLE) + { + static int s_prevTessFlag = -1; + const int tessOn = ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION) ? 1 : 0; + if (tessOn != s_prevTessFlag) + { + s_prevTessFlag = tessOn; + u32 fixed = 0; + for (const auto& kv : m_materialIDByNames) + { + const u32 id = kv.second; + const MaterialData* existing = materialBuffer.GetMaterial(id); + if (!existing) + continue; + MaterialData matData = *existing; + u32 wantMethod = 0; + if (tessOn) + { + const char* sh = kv.first.first.c_str(); + const bool denyTess = + (matData.flags & (MAT_FLAG_FOLIAGE | MAT_FLAG_WATER | MAT_FLAG_TERRAIN | + MAT_FLAG_PARTICLE_HARD)) != 0 || + (sh && (strstr(sh, "flora") || strstr(sh, "tree") || strstr(sh, "grass") || + strstr(sh, "particle") || strstr(sh, "effects\\") || + strstr(sh, "effects/"))); + u32 blenderTess = 0; + if (!denyTess && sh && sh[0] && + shader_info::GetShaderTessellationMethod(sh, blenderTess)) + wantMethod = blenderTess; + if (wantMethod >= 2 && (matData.flags & MAT_FLAG_HAS_NORMAL_X) == 0) + wantMethod = (wantMethod == 2) ? 0 : 1; + if (wantMethod == 3 && (matData.flags & MAT_FLAG_HAS_DETAIL) == 0) + wantMethod = 1; + if (wantMethod > 3) + wantMethod = 0; + } + if (matData.tessMethod != wantMethod) + { + matData.tessMethod = wantMethod; + materialBuffer.UpdateMaterial(id, matData); + ++fixed; + tessRoutingDirty = true; + } + } + u32 withTess = 0; + for (const auto& kv : m_materialIDByNames) + { + const MaterialData* m = materialBuffer.GetMaterial(kv.second); + if (m && m->tessMethod != 0) + ++withTess; + } + Msg("* [Tess] flag=%d updated=%u materials with tessMethod!=0: %u (blender only)", + tessOn, fixed, withTess); + tessRoutingDirty = true; + } + } +#endif if (processedCount > 0) { materialBuffer.Upload(ctx); } + if (tessRoutingDirty) + { + if (auto* fgr = dynamic_cast(GEnv.Render)) + { + if (auto* cull = fgr->GetGPUCullingManager()) + { + cull->InvalidateStaticCullingData(); + Msg("* [Tess] static cull invalidated after tessMethod update"); + } + } + } + auto& vtb = bindless::VariantTextureBuffer::Instance(); if (vtb.IsInitialized()) vtb.Upload(ctx); diff --git a/src/Layers/xrRender/Geometry/MaterialCache.h b/src/Layers/xrRender/Geometry/MaterialCache.h index 74f924791f2..ddde30268d9 100644 --- a/src/Layers/xrRender/Geometry/MaterialCache.h +++ b/src/Layers/xrRender/Geometry/MaterialCache.h @@ -299,6 +299,8 @@ class MaterialCache { u32 materialID; dxRender_Visual* visual; shared_str textureName; + shared_str sixWayPosXYZ; + shared_str sixWayNegXYZ; }; xr_vector m_pendingMaterials; @@ -343,7 +345,12 @@ class MaterialCache { u32 PreRegisterBindlessMaterial(dxRender_Visual* visual); - u32 PreRegisterParticleMaterial(const shared_str& textureName, u8 blendMode = 1); + u32 PreRegisterParticleMaterial( + const shared_str& textureName, + u8 blendMode = 1, + const char* shaderName = nullptr, + const char* sixWayPosXYZ = nullptr, + const char* sixWayNegXYZ = nullptr); bool IsTerrainMaterial(dxRender_Visual* visual); diff --git a/src/Layers/xrRender/Materials/MaterialSystem.cpp b/src/Layers/xrRender/Materials/MaterialSystem.cpp index e3930028f14..591aa7bec65 100644 --- a/src/Layers/xrRender/Materials/MaterialSystem.cpp +++ b/src/Layers/xrRender/Materials/MaterialSystem.cpp @@ -108,12 +108,22 @@ const MaterialSystem::MaterialInfo& MaterialSystem::GetMaterialInfo(const shared break; case BlendMode::AlphaBlend: case BlendMode::Additive: + info.transparent = true; + if (blendInfo.alphaRef > 0) + { + info.alphaTest = true; + info.alphaRef = blendInfo.alphaRef; + } + else + { + info.alphaTest = false; + info.alphaRef = 0; + } + break; case BlendMode::Multiply: case BlendMode::Multiply2X: - // Classic blenders often keep AREF together with blend (model/detail/ - // screen_set with oAREF). Dropping aref made bindless_forward skip - // clip() → fringe / no cutoff on alpha effects. info.transparent = true; + info.multiply = true; if (blendInfo.alphaRef > 0) { info.alphaTest = true; @@ -275,7 +285,7 @@ xr_string MaterialSystem::GetNormalPath(const char* textureName) const xr_string MaterialSystem::GetPBRPath(const char* textureName) const { - // Convention: texture_pbr (packed R=AO, G=Roughness, B=Metallic) + // Convention: texture_pbr (packed R=Metallic, G=Roughness, B=AO, A=Parallax) xr_string path = textureName; path += "_pbr"; return path; diff --git a/src/Layers/xrRender/Materials/MaterialSystem.h b/src/Layers/xrRender/Materials/MaterialSystem.h index 044d81ba22a..0652b63e6af 100644 --- a/src/Layers/xrRender/Materials/MaterialSystem.h +++ b/src/Layers/xrRender/Materials/MaterialSystem.h @@ -37,6 +37,7 @@ class MaterialSystem bool alphaTest = false; // Uses clip()/discard - needs alpha test in depth prepass u32 alphaRef = 0; // Alpha reference threshold (0-255), normalized to 0.0-1.0 for GPU bool transparent = false; // Requires back-to-front sorting (bStrictB2F) + bool multiply = false; // DestColor*SrcColor (wall stains / burns) u8 priority = 1; // Render priority (0-3) for batching // Future PBR properties (stored here for unified material system) @@ -58,7 +59,7 @@ class MaterialSystem { fg::TextureHandle albedo; // Base color / diffuse fg::TextureHandle normal; // Normal map - fg::TextureHandle pbr; // Packed PBR (R=AO, G=Roughness, B=Metallic) + fg::TextureHandle pbr; // Packed PBR (R=Metallic, G=Roughness, B=AO, A=Parallax) fg::TextureHandle detail; // Detail texture (optional) bool IsValid() const; diff --git a/src/Layers/xrRender/Materials/ShaderInfo.h b/src/Layers/xrRender/Materials/ShaderInfo.h index f6d7404d760..a032dc87374 100644 --- a/src/Layers/xrRender/Materials/ShaderInfo.h +++ b/src/Layers/xrRender/Materials/ShaderInfo.h @@ -33,6 +33,8 @@ struct ShaderBlendInfo }; bool GetShaderBlendInfo(const char* shaderName, ShaderBlendInfo& out); +bool GetShaderTessellationMethod(const char* shaderName, u32& outMethod); + bool GetParticleBlendIndex(const char* shaderName, u32& outIndex); } diff --git a/src/Layers/xrRender/MaxRectsAllocator.h b/src/Layers/xrRender/MaxRectsAllocator.h new file mode 100644 index 00000000000..8676dcf38d6 --- /dev/null +++ b/src/Layers/xrRender/MaxRectsAllocator.h @@ -0,0 +1,144 @@ +#pragma once + +#include "xrCore/xrCore.h" +#include +#include + +namespace xray::render::fg +{ + +struct MaxRectsRect +{ + u32 x = 0; + u32 y = 0; + u32 w = 0; + u32 h = 0; +}; + +class MaxRectsAllocator +{ + u32 m_size = 0; + xr_vector m_free; + xr_vector m_used; + + static bool Intersects(const MaxRectsRect& a, const MaxRectsRect& b) + { + return !(a.x + a.w <= b.x || b.x + b.w <= a.x || a.y + a.h <= b.y || b.y + b.h <= a.y); + } + + void SplitFree(u32 freeIdx, const MaxRectsRect& used) + { + const MaxRectsRect f = m_free[freeIdx]; + m_free.erase(m_free.begin() + freeIdx); + + if (used.x > f.x) + m_free.push_back({f.x, f.y, used.x - f.x, f.h}); + if (used.x + used.w < f.x + f.w) + m_free.push_back({used.x + used.w, f.y, (f.x + f.w) - (used.x + used.w), f.h}); + if (used.y > f.y) + m_free.push_back({used.x, f.y, used.w, used.y - f.y}); + if (used.y + used.h < f.y + f.h) + m_free.push_back({used.x, used.y + used.h, used.w, (f.y + f.h) - (used.y + used.h)}); + } + + void PruneFree() + { + for (u32 i = 0; i < m_free.size();) + { + bool contained = false; + for (u32 j = 0; j < m_free.size(); ++j) + { + if (i == j) + continue; + const MaxRectsRect& a = m_free[i]; + const MaxRectsRect& b = m_free[j]; + if (a.x >= b.x && a.y >= b.y && a.x + a.w <= b.x + b.w && a.y + a.h <= b.y + b.h) + { + contained = true; + break; + } + } + if (contained) + m_free.erase(m_free.begin() + i); + else + ++i; + } + } + +public: + void initialize(u32 size) + { + m_size = size; + m_free.clear(); + m_used.clear(); + m_free.push_back({0, 0, size, size}); + } + + bool push(MaxRectsRect& out, u32 size) + { + if (size < 4 || size > m_size) + return false; + + s32 bestScore = INT_MAX; + s32 bestShort = INT_MAX; + s32 bestIdx = -1; + MaxRectsRect best{}; + + for (u32 i = 0; i < m_free.size(); ++i) + { + const MaxRectsRect& f = m_free[i]; + if (f.w < size || f.h < size) + continue; + const s32 leftoverX = s32(f.w) - s32(size); + const s32 leftoverY = s32(f.h) - s32(size); + const s32 shortSide = std::min(leftoverX, leftoverY); + const s32 longSide = std::max(leftoverX, leftoverY); + if (shortSide < bestShort || (shortSide == bestShort && longSide < bestScore)) + { + bestShort = shortSide; + bestScore = longSide; + bestIdx = s32(i); + best = {f.x, f.y, size, size}; + } + } + + if (bestIdx < 0) + return false; + + SplitFree(u32(bestIdx), best); + PruneFree(); + m_used.push_back(best); + out = best; + return true; + } + + bool reserve(u32 posX, u32 posY, u32 size) + { + if (size < 4 || size > m_size) + return false; + if (posX + size > m_size || posY + size > m_size) + return false; + + MaxRectsRect used{posX, posY, size, size}; + for (const MaxRectsRect& u : m_used) + { + if (Intersects(u, used)) + return false; + } + + for (u32 i = 0; i < m_free.size();) + { + if (Intersects(m_free[i], used)) + { + SplitFree(i, used); + continue; + } + ++i; + } + PruneFree(); + m_used.push_back(used); + return true; + } +}; + +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/ParticleEffectDef.cpp b/src/Layers/xrRender/ParticleEffectDef.cpp index 074fbe0d9b1..ab18b8d1413 100644 --- a/src/Layers/xrRender/ParticleEffectDef.cpp +++ b/src/Layers/xrRender/ParticleEffectDef.cpp @@ -276,6 +276,13 @@ BOOL CPEDef::Load(IReader& F) } #endif + if (F.find_chunk(PED_CHUNK_LIGHTING)) + { + m_LightingMode = (ParticleLightingMode)F.r_u32(); + F.r_stringZ(m_SixWayPosXYZ); + F.r_stringZ(m_SixWayNegXYZ); + } + return TRUE; } @@ -321,6 +328,14 @@ BOOL CPEDef::Load2(CInifile& ini) { m_APDefaultRotation = ini.r_fvector3("align_to_path", "default_rotation"); } + + if (ini.line_exist("_effect", "lighting_mode")) + m_LightingMode = (ParticleLightingMode)ini.r_u32("_effect", "lighting_mode"); + if (ini.line_exist("_effect", "sixway_pos_xyz")) + m_SixWayPosXYZ = ini.r_string("_effect", "sixway_pos_xyz"); + if (ini.line_exist("_effect", "sixway_neg_xyz")) + m_SixWayNegXYZ = ini.r_string("_effect", "sixway_neg_xyz"); + #ifdef _EDITOR if (pCreateEAction) { @@ -349,6 +364,11 @@ void CPEDef::Save2(CInifile& ini) ini.w_u32("_effect", "max_particles", m_MaxParticles); //.!! F.w (m_Actions.pointer(),m_Actions.size()); ini.w_u32("_effect", "flags", m_Flags.get()); + ini.w_u32("_effect", "lighting_mode", (u32)m_LightingMode); + if (m_SixWayPosXYZ.c_str()) + ini.w_string("_effect", "sixway_pos_xyz", m_SixWayPosXYZ.c_str()); + if (m_SixWayNegXYZ.c_str()) + ini.w_string("_effect", "sixway_neg_xyz", m_SixWayNegXYZ.c_str()); if (m_Flags.is(dfSprite)) { @@ -473,6 +493,12 @@ void CPEDef::Save(IWriter& F) } F.close_chunk(); #endif + + F.open_chunk(PED_CHUNK_LIGHTING); + F.w_u32((u32)m_LightingMode); + F.w_stringZ(m_SixWayPosXYZ); + F.w_stringZ(m_SixWayNegXYZ); + F.close_chunk(); } #ifdef _EDITOR diff --git a/src/Layers/xrRender/ParticleEffectDef.h b/src/Layers/xrRender/ParticleEffectDef.h index 4cd73c16586..f0b93b8f928 100644 --- a/src/Layers/xrRender/ParticleEffectDef.h +++ b/src/Layers/xrRender/ParticleEffectDef.h @@ -22,6 +22,14 @@ typedef BOOL (*CollisionCallback)( CParticleEffect* E, PAPI::Particle& P, const Fvector& pt, const Fvector& norm); // TRUE-continue collision exec typedef void (*DestroyCallback)(CParticleEffect* E, PAPI::Particle& P); +enum class ParticleLightingMode : u8 +{ + Emissive = 0, + Lit = 1, + Volumetric = 2, + SixWay = 3, +}; + class PFunction; struct SFrame { @@ -75,8 +83,11 @@ class ECORE_API CPEDef // texture shared_str m_ShaderName; shared_str m_TextureName; + shared_str m_SixWayPosXYZ; + shared_str m_SixWayNegXYZ; ref_shader m_CachedShader; SFrame m_Frame; + ParticleLightingMode m_LightingMode = ParticleLightingMode::Emissive; // compiled actions CMemoryWriter m_Actions; // def @@ -157,5 +168,6 @@ class ECORE_API CPEDef #define PED_CHUNK_VEL_SCALE 0x0022 #define PED_CHUNK_EDATA 0x0024 #define PED_CHUNK_ALIGN_TO_PATH 0x0025 +#define PED_CHUNK_LIGHTING 0x0026 //--------------------------------------------------------------------------- } // namespace xray::render::fg diff --git a/src/Layers/xrRender/Profiler/StatsOverlay.cpp b/src/Layers/xrRender/Profiler/StatsOverlay.cpp index bccf925b4c9..44468d47a85 100644 --- a/src/Layers/xrRender/Profiler/StatsOverlay.cpp +++ b/src/Layers/xrRender/Profiler/StatsOverlay.cpp @@ -516,6 +516,33 @@ void StatsOverlay::RenderGeometrySection() ImGui::Unindent(); } + // ═══════════════════════════════════════════════════ + // MDI / CLUSTER + // ═══════════════════════════════════════════════════ + ImGui::Text("MDI draws: %u (maxCountSum %u)", s.mdiDrawCalls, s.mdiMaxDrawCountSum); + if (s.lightsClustered > 0) + { + ImGui::Text("Lights: %u clustered (HiZ vis %u)", s.lightsClustered, s.lightsHiZVisible); + ImGui::Indent(); + if (s.lightsPoint > 0) + ImGui::BulletText("Point: %u", s.lightsPoint); + if (s.lightsSpot > 0) + ImGui::BulletText("Spot: %u", s.lightsSpot); + if (s.lightsOmni > 0) + ImGui::BulletText("Omni: %u", s.lightsOmni); + ImGui::Unindent(); + } + if (s.localShadowTiles > 0 || s.localShadowCandidates > 0) + { + ImGui::Text("Local shadows: %u tiles (cand %u, drop %u, redraw %u)", + s.localShadowTiles, s.localShadowCandidates, s.localShadowDropped, s.localShadowRedraw); + } + if (s.fgSetupPassesMs > 0.f || s.fgCompileMs > 0.f) + { + ImGui::Text("FG SetupPasses: %s Compile: %s", + FormatTime(s.fgSetupPassesMs, 0), FormatTime(s.fgCompileMs, 1)); + } + // ═══════════════════════════════════════════════════ // SKINNING STATS // ═══════════════════════════════════════════════════ diff --git a/src/Layers/xrRender/Profiler/StatsOverlay.h b/src/Layers/xrRender/Profiler/StatsOverlay.h index b37b21f9306..0d5120fc21a 100644 --- a/src/Layers/xrRender/Profiler/StatsOverlay.h +++ b/src/Layers/xrRender/Profiler/StatsOverlay.h @@ -53,6 +53,14 @@ struct RenderStats u32 lightsPoint = 0; u32 lightsSpot = 0; u32 lightsOmni = 0; + u32 localShadowTiles = 0; + u32 localShadowCandidates = 0; + u32 localShadowDropped = 0; + u32 localShadowRedraw = 0; + + // MDI / draw instrumentation (from DrawIndexedIndirectCountOrFallback) + u32 mdiDrawCalls = 0; + u32 mdiMaxDrawCountSum = 0; u32 detailInstances = 0; u32 detailSlots = 0; // Detail slots @@ -74,6 +82,8 @@ struct RenderStats u32 fgArenaPeak = 0; u32 fgArenaCapacity = 0; u32 fgArenaFallbacks = 0; + float fgSetupPassesMs = 0.f; + float fgCompileMs = 0.f; void Reset() { @@ -84,12 +94,15 @@ struct RenderStats skinnedMeshes = totalBones = maxBonesPerMesh = 0; skinnedSubmitted = skinnedVisible = skinnedCulled = 0; lightsClustered = lightsHiZVisible = lightsPoint = lightsSpot = lightsOmni = 0; + localShadowTiles = localShadowCandidates = localShadowDropped = localShadowRedraw = 0; + mdiDrawCalls = mdiMaxDrawCountSum = 0; detailInstances = detailSlots = 0; detailTrisPerBlade[0] = detailTrisPerBlade[1] = detailTrisPerBlade[2] = 0; detailVisibleSlots = detailVisibleLOD0 = detailVisibleLOD1 = detailVisibleLOD2 = 0; detailVisibleDecals = 0; detailGeneratedInstances = detailVisibleCapacity = detailDecalCapacity = 0; fgArenaUsed = fgArenaPeak = fgArenaCapacity = fgArenaFallbacks = 0; + fgSetupPassesMs = fgCompileMs = 0.f; } }; diff --git a/src/Layers/xrRender/RayTracing/RTAccelStructManager.cpp b/src/Layers/xrRender/RayTracing/RTAccelStructManager.cpp index 55623c3165a..7ed016785a5 100644 --- a/src/Layers/xrRender/RayTracing/RTAccelStructManager.cpp +++ b/src/Layers/xrRender/RayTracing/RTAccelStructManager.cpp @@ -105,11 +105,16 @@ void RTAccelStructManager::Shutdown() m_staticBlas = nullptr; m_uniqueGeometries.clear(); m_tlas = nullptr; + m_tlasMaxInstances = 0; + m_tlasBuiltInstances = 0; m_batchInfoBuffer = nullptr; m_skinnedOutputVB = nullptr; m_skinnedIB = nullptr; m_skinnedBlas = nullptr; m_skinnedBatchData.clear(); + m_skinnedTotalVerts = 0; + m_skinnedTotalIndices = 0; + m_skinnedTopoHash = 0; m_skinnedReady = false; m_grassOutputVB = nullptr; m_grassIB = nullptr; @@ -366,16 +371,25 @@ void RTAccelStructManager::BuildTLAS(nvrhi::ICommandList* cmdList) return; } - nvrhi::rt::AccelStructDesc tlasDesc; - tlasDesc.debugName = "SceneTLAS"; - tlasDesc.isTopLevel = true; - tlasDesc.topLevelMaxInstances = instanceCount; - tlasDesc.buildFlags = nvrhi::rt::AccelStructBuildFlags::PreferFastTrace; - - m_tlas = nvDevice->createAccelStruct(tlasDesc); - if (!m_tlas) { - Msg("! [RT] Failed to create TLAS"); - return; + const bool needCreate = !m_tlas || instanceCount > m_tlasMaxInstances; + if (needCreate) { + m_tlasMaxInstances = std::max(instanceCount + 64u, m_tlasMaxInstances * 2u); + m_tlasMaxInstances = std::max(m_tlasMaxInstances, 64u); + + nvrhi::rt::AccelStructDesc tlasDesc; + tlasDesc.debugName = "SceneTLAS"; + tlasDesc.isTopLevel = true; + tlasDesc.topLevelMaxInstances = m_tlasMaxInstances; + tlasDesc.buildFlags = nvrhi::rt::AccelStructBuildFlags::PreferFastTrace | + nvrhi::rt::AccelStructBuildFlags::AllowUpdate; + + m_tlas = nvDevice->createAccelStruct(tlasDesc); + if (!m_tlas) { + Msg("! [RT] Failed to create TLAS (instances=%u capacity=%u)", instanceCount, m_tlasMaxInstances); + m_tlasMaxInstances = 0; + return; + } + Msg("* [RT] TLAS created (capacity=%u)", m_tlasMaxInstances); } xr_vector instances; @@ -426,20 +440,26 @@ void RTAccelStructManager::BuildTLAS(nvrhi::ICommandList* cmdList) u32 grassBatchOffset = skinnedBatchOffset + skinnedCount; if (m_grassBlas && grassCount > 0) { - auto grassInstFlags = m_grassBillboardMode - ? nvrhi::rt::InstanceFlags::TriangleCullDisable - : nvrhi::rt::InstanceFlags::ForceOpaque; + auto grassFlags = nvrhi::rt::InstanceFlags::TriangleCullDisable; + if (!m_grassBillboardMode) + grassFlags = grassFlags | nvrhi::rt::InstanceFlags::ForceOpaque; nvrhi::rt::InstanceDesc inst; inst.setTransform(nvrhi::rt::c_IdentityTransform) .setInstanceID(grassBatchOffset) - .setInstanceMask(0x01) - .setFlags(grassInstFlags) + .setInstanceMask(0x02) + .setFlags(grassFlags) .setBLAS(m_grassBlas); instances.push_back(inst); } m_batchCount = grassBatchOffset + grassCount; - cmdList->buildTopLevelAccelStruct(m_tlas, instances.data(), (u32)instances.size()); + const u32 builtCount = (u32)instances.size(); + auto buildFlags = nvrhi::rt::AccelStructBuildFlags::PreferFastTrace | + nvrhi::rt::AccelStructBuildFlags::AllowUpdate; + if (!needCreate && m_tlasBuiltInstances == builtCount) + buildFlags = buildFlags | nvrhi::rt::AccelStructBuildFlags::PerformUpdate; + cmdList->buildTopLevelAccelStruct(m_tlas, instances.data(), builtCount, buildFlags); + m_tlasBuiltInstances = builtCount; } static void AppendBatchInfos( @@ -513,15 +533,21 @@ void RTAccelStructManager::CreateBatchInfoBuffer(nvrhi::ICommandList* cmdList, G if (batchInfos.empty()) return; - nvrhi::BufferDesc desc; - desc.debugName = "RTBatchInfoBuffer"; - desc.byteSize = batchInfos.size() * sizeof(RTBatchInfo); - desc.structStride = sizeof(RTBatchInfo); - desc.initialState = nvrhi::ResourceStates::ShaderResource; - desc.keepInitialState = true; - - m_batchInfoBuffer = nvDevice->createBuffer(desc); - cmdList->writeBuffer(m_batchInfoBuffer, batchInfos.data(), batchInfos.size() * sizeof(RTBatchInfo)); + const u64 neededBytes = batchInfos.size() * sizeof(RTBatchInfo); + if (!m_batchInfoBuffer || m_batchInfoBuffer->getDesc().byteSize < neededBytes) { + nvrhi::BufferDesc desc; + desc.debugName = "RTBatchInfoBuffer"; + desc.byteSize = neededBytes; + desc.structStride = sizeof(RTBatchInfo); + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + m_batchInfoBuffer = nvDevice->createBuffer(desc); + if (!m_batchInfoBuffer) { + Msg("! [RT] Failed to create batch info buffer (%u entries)", (u32)batchInfos.size()); + return; + } + } + cmdList->writeBuffer(m_batchInfoBuffer, batchInfos.data(), neededBytes); } u32 RTAccelStructManager::GetSkinningFormatID(u16 renderMode, u32 stride) @@ -556,7 +582,7 @@ void RTAccelStructManager::InitSkinningPipeline() cbDesc.byteSize = sizeof(RTSkinningCB); cbDesc.isConstantBuffer = true; cbDesc.isVolatile = true; - cbDesc.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; + cbDesc.maxVersions = 8192; s_skinCB = m_device->CreateBuffer(cbDesc); s_skinLayout = cache.GetOrCreateBindingLayoutFromReflection("RTSkinning", *skinResult.reflection, nvDevice); @@ -647,6 +673,13 @@ void RTAccelStructManager::BuildSkinnedBLAS( for (const auto& b : worldBatches) processBatch(b); for (const auto& b : hudBatches) processBatch(b); + constexpr u32 kMaxSkinnedVerts = 500000u; + if (totalVerts > kMaxSkinnedVerts) { + Msg("! [RT] Skinned verts capped %u -> %u", totalVerts, kMaxSkinnedVerts); + InvalidateSkinned(); + return; + } + u64 vbSize = static_cast(totalVerts) * SKINNED_VERTEX_STRIDE; u64 ibSize = static_cast(totalIndices) * sizeof(u32); @@ -660,6 +693,11 @@ void RTAccelStructManager::BuildSkinnedBLAS( desc.keepInitialState = true; desc.canHaveUAVs = true; m_skinnedOutputVB = nvDevice->createBuffer(desc); + if (!m_skinnedOutputVB) { + Msg("! [RT] SkinnedOutputVB create failed"); + InvalidateSkinned(); + return; + } } if (!m_skinnedIB || m_skinnedIB->getDesc().byteSize < ibSize) { @@ -671,6 +709,11 @@ void RTAccelStructManager::BuildSkinnedBLAS( desc.initialState = nvrhi::ResourceStates::ShaderResource; desc.keepInitialState = true; m_skinnedIB = nvDevice->createBuffer(desc); + if (!m_skinnedIB) { + Msg("! [RT] SkinnedIB create failed"); + InvalidateSkinned(); + return; + } } cmdList->writeBuffer(m_skinnedIB, consolidatedIndices.data(), consolidatedIndices.size() * sizeof(u32)); @@ -710,9 +753,18 @@ void RTAccelStructManager::BuildSkinnedBLAS( cmdList->dispatch((sb.vertexCount + 255) / 256, 1, 1); } + u64 topoHash = m_skinnedBatchData.size(); + for (const auto& sb : m_skinnedBatchData) { + topoHash = topoHash * 131ull + sb.vertexCount; + topoHash = topoHash * 131ull + sb.indexCount; + topoHash = topoHash * 131ull + sb.indexOffset; + topoHash = topoHash * 131ull + sb.vertexOffset; + } + nvrhi::rt::AccelStructDesc blasDesc; blasDesc.debugName = "SkinnedBLAS"; - blasDesc.buildFlags = nvrhi::rt::AccelStructBuildFlags::PreferFastBuild; + blasDesc.buildFlags = nvrhi::rt::AccelStructBuildFlags::PreferFastBuild | + nvrhi::rt::AccelStructBuildFlags::AllowUpdate; for (const auto& sb : m_skinnedBatchData) { nvrhi::rt::GeometryTriangles tri; @@ -731,21 +783,39 @@ void RTAccelStructManager::BuildSkinnedBLAS( blasDesc.addBottomLevelGeometry(geom); } - m_skinnedBlas = nvDevice->createAccelStruct(blasDesc); - if (m_skinnedBlas) + const bool needCreate = !m_skinnedBlas || m_skinnedTopoHash != topoHash || + m_skinnedTotalVerts != totalVerts || m_skinnedTotalIndices != totalIndices; + if (needCreate) { + m_skinnedBlas = nvDevice->createAccelStruct(blasDesc); + if (!m_skinnedBlas) { + Msg("! [RT] Skinned BLAS create failed"); + InvalidateSkinned(); + return; + } + nvrhi::utils::BuildBottomLevelAccelStruct(cmdList, m_skinnedBlas, blasDesc); + Msg("* [RT] Built %u skinned BLAS (%u verts, %u indices)", + (u32)m_skinnedBatchData.size(), totalVerts, totalIndices); + } else if (m_skinnedBlas) { + blasDesc.buildFlags = nvrhi::rt::AccelStructBuildFlags::PreferFastBuild | + nvrhi::rt::AccelStructBuildFlags::AllowUpdate | + nvrhi::rt::AccelStructBuildFlags::PerformUpdate; nvrhi::utils::BuildBottomLevelAccelStruct(cmdList, m_skinnedBlas, blasDesc); + } + m_skinnedTotalVerts = totalVerts; + m_skinnedTotalIndices = totalIndices; + m_skinnedTopoHash = topoHash; m_batchCounts.skinned = (u32)m_skinnedBatchData.size(); m_skinnedReady = true; - - Msg("* [RT] Built %u skinned BLAS (%u verts, %u indices)", - (u32)m_skinnedBatchData.size(), totalVerts, totalIndices); } void RTAccelStructManager::InvalidateSkinned() { m_skinnedBlas = nullptr; m_skinnedBatchData.clear(); + m_skinnedTotalVerts = 0; + m_skinnedTotalIndices = 0; + m_skinnedTopoHash = 0; m_batchCounts.skinned = 0; m_skinnedReady = false; } @@ -854,13 +924,31 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail if (billboardMode) { InitBillboardPipeline(); - if (!s_billboardInitialized) return; + if (!s_billboardInitialized) { + Msg("! [RT] Billboard grass pipeline missing"); + return; + } u32 maxVPB = detailMgr->maxPulledIndexCount; maxVPB = (maxVPB / 3) * 3; - if (maxVPB == 0 || !detailMgr->billboardDrawArgsBuffer) { InvalidateGrass(); return; } + constexpr u32 kMaxVertsPerBillboardRT = 48u; + if (maxVPB > kMaxVertsPerBillboardRT) + maxVPB = kMaxVertsPerBillboardRT; + if (maxVPB == 0 || !detailMgr->billboardDrawArgsBuffer) { + InvalidateGrass(); + return; + } + constexpr u32 kMaxRtBillboardVerts = 1500000u; u32 capacity = detailMgr->visibleBufferCapacity; + const u32 maxCapacity = kMaxRtBillboardVerts / maxVPB; + if (capacity > maxCapacity) + capacity = maxCapacity; + if (capacity == 0) { + InvalidateGrass(); + return; + } + totalVerts = capacity * maxVPB; totalIndices = totalVerts; u64 vbSize = static_cast(totalVerts) * GRASS_VERTEX_STRIDE; @@ -876,6 +964,11 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail desc.keepInitialState = true; desc.canHaveUAVs = true; m_grassOutputVB = nvDevice->createBuffer(desc); + if (!m_grassOutputVB) { + Msg("! [RT] Grass VB alloc failed (%.1f MB)", float(vbSize) / (1024.f * 1024.f)); + InvalidateGrass(); + return; + } } if (!m_grassIB || m_grassIB->getDesc().byteSize < ibSize) { @@ -888,6 +981,11 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail desc.initialState = nvrhi::ResourceStates::ShaderResource; desc.keepInitialState = true; m_grassIB = nvDevice->createBuffer(desc); + if (!m_grassIB) { + Msg("! [RT] Grass IB alloc failed (%.1f MB)", float(ibSize) / (1024.f * 1024.f)); + InvalidateGrass(); + return; + } } BillboardRTCB cb; @@ -895,6 +993,7 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail cb.pad[0] = cb.pad[1] = cb.pad[2] = 0; auto* billboardRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("rt_grass_billboard", ".cs"); + if (!billboardRefl) { InvalidateGrass(); return; } framegraph::BindingSetBuilder bsb(*billboardRefl, nvDevice, "RT.Billboard"); bsb.BufferSRV("g_AllInstances", detailMgr->generatedInstancesBuffer) .BufferSRV("g_VisibleIndices", detailMgr->visibleBillboardInstancesBuffer) @@ -905,6 +1004,7 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail .BufferUAV("g_OutputIB", m_grassIB) .ConstantBuffer("BillboardRTCB", m_device->GetNativeBuffer(s_billboardCB)); auto bindingSet = nvDevice->createBindingSet(bsb.Build(), s_billboardLayout); + if (!bindingSet) { InvalidateGrass(); return; } cmdList->writeBuffer(m_device->GetNativeBuffer(s_billboardCB), &cb, sizeof(BillboardRTCB)); nvrhi::ComputeState state; @@ -922,6 +1022,14 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail u32 totalBlades = lodCounts[0] + lodCounts[1] + lodCounts[2]; if (totalBlades == 0) { InvalidateGrass(); return; } + constexpr u32 kMaxRtGrassBlades = 500000u; + if (totalBlades > kMaxRtGrassBlades) { + const float scale = (float)kMaxRtGrassBlades / (float)totalBlades; + for (u32 lod = 0; lod < FGDetailManager::LOD_COUNT; lod++) + lodCounts[lod] = (u32)((float)lodCounts[lod] * scale); + totalBlades = lodCounts[0] + lodCounts[1] + lodCounts[2]; + } + u32 lodVertsPerBlade[FGDetailManager::LOD_COUNT]; u32 lodIndicesPerBlade[FGDetailManager::LOD_COUNT]; for (u32 lod = 0; lod < FGDetailManager::LOD_COUNT; lod++) { @@ -945,6 +1053,7 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail desc.keepInitialState = true; desc.canHaveUAVs = true; m_grassOutputVB = nvDevice->createBuffer(desc); + if (!m_grassOutputVB) { InvalidateGrass(); return; } } if (!m_grassIB || m_grassIB->getDesc().byteSize < ibSize) { @@ -957,6 +1066,7 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail desc.initialState = nvrhi::ResourceStates::ShaderResource; desc.keepInitialState = true; m_grassIB = nvDevice->createBuffer(desc); + if (!m_grassIB) { InvalidateGrass(); return; } } float windAngleDeg = 0.0f; @@ -984,6 +1094,7 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail if (lodCounts[lod] == 0) continue; auto* grassRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("rt_grass_vertices", ".cs"); + if (!grassRefl) { InvalidateGrass(); return; } framegraph::BindingSetBuilder bsb(*grassRefl, nvDevice, "RT.Grass"); bsb.BufferSRV("g_AllInstances", detailMgr->generatedInstancesBuffer) .BufferSRV("g_SlotData", detailMgr->slotDataBuffer) @@ -993,6 +1104,7 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail .BufferUAV("g_OutputIB", m_grassIB) .ConstantBuffer("GrassRTCB", m_device->GetNativeBuffer(s_grassCB)); auto bindingSet = nvDevice->createBindingSet(bsb.Build(), s_grassLayout); + if (!bindingSet) { InvalidateGrass(); return; } GrassRTCB cb = cbTemplate; cb.segments = FGDetailManager::LOD_SEGMENTS[lod]; @@ -1012,11 +1124,10 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail vertexOffset += totalVertsThisLod; indexOffset += lodCounts[lod] * lodIndicesPerBlade[lod]; } - - Msg("* [RT] Built blade grass BLAS (%u blades, %u verts, %u indices, LODs: %u/%u/%u)", - totalBlades, totalVerts, totalIndices, lodCounts[0], lodCounts[1], lodCounts[2]); } + if (totalVerts == 0 || totalIndices == 0) { InvalidateGrass(); return; } + nvrhi::rt::AccelStructDesc blasDesc; blasDesc.debugName = "GrassBLAS"; blasDesc.buildFlags = nvrhi::rt::AccelStructBuildFlags::PreferFastBuild; @@ -1040,8 +1151,12 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail if (!m_grassBlas || m_grassTotalVerts != totalVerts || m_grassTotalIndices != totalIndices) m_grassBlas = nvDevice->createAccelStruct(blasDesc); - if (m_grassBlas) - nvrhi::utils::BuildBottomLevelAccelStruct(cmdList, m_grassBlas, blasDesc); + if (!m_grassBlas) { + Msg("! [RT] Grass BLAS create failed (verts=%u)", totalVerts); + InvalidateGrass(); + return; + } + nvrhi::utils::BuildBottomLevelAccelStruct(cmdList, m_grassBlas, blasDesc); m_grassTotalVerts = totalVerts; m_grassTotalIndices = totalIndices; @@ -1049,6 +1164,18 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail m_grassReady = true; m_grassBillboardMode = billboardMode; m_detailAtlasIndex = billboardMode ? detailMgr->buildDetailsBindlessIndex : 0; + + { + static u32 s_lastVerts = 0; + static bool s_lastBillboard = false; + if (s_lastVerts != totalVerts || s_lastBillboard != billboardMode) + { + s_lastVerts = totalVerts; + s_lastBillboard = billboardMode; + Msg("* [RT] Grass BLAS ready (%s, verts=%u, atlas=%u)", + billboardMode ? "billboard" : "blades", totalVerts, m_detailAtlasIndex); + } + } } } diff --git a/src/Layers/xrRender/RayTracing/RTAccelStructManager.h b/src/Layers/xrRender/RayTracing/RTAccelStructManager.h index 1fa0f51d057..ff56d1f8eb4 100644 --- a/src/Layers/xrRender/RayTracing/RTAccelStructManager.h +++ b/src/Layers/xrRender/RayTracing/RTAccelStructManager.h @@ -130,6 +130,8 @@ class RTAccelStructManager { nvrhi::rt::AccelStructHandle m_staticBlas; xr_vector m_uniqueGeometries; nvrhi::rt::AccelStructHandle m_tlas; + u32 m_tlasMaxInstances = 0; + u32 m_tlasBuiltInstances = 0; nvrhi::BufferHandle m_batchInfoBuffer; nvrhi::IBuffer* m_megaVB = nullptr; @@ -141,6 +143,9 @@ class RTAccelStructManager { nvrhi::BufferHandle m_skinnedIB; nvrhi::rt::AccelStructHandle m_skinnedBlas; xr_vector m_skinnedBatchData; + u32 m_skinnedTotalVerts = 0; + u32 m_skinnedTotalIndices = 0; + u64 m_skinnedTopoHash = 0; bool m_skinnedReady = false; nvrhi::BufferHandle m_grassOutputVB; diff --git a/src/Layers/xrRender/RenderContext/RenderDevice.cpp b/src/Layers/xrRender/RenderContext/RenderDevice.cpp index 4b1da36f258..b1ad79d4450 100644 --- a/src/Layers/xrRender/RenderContext/RenderDevice.cpp +++ b/src/Layers/xrRender/RenderContext/RenderDevice.cpp @@ -203,13 +203,13 @@ TextureHandle RenderDevice::CreateTexture( VERIFY(m_initialized); VERIFY(desc.width > 0 && desc.height > 0); - // Convert to NVRHI descriptor nvrhi::TextureDesc nvrhiDesc = ConvertTextureDesc(desc); - // Create NVRHI texture nvrhi::TextureHandle nvrhiTexture = GetNativeDevice()->createTexture(nvrhiDesc); if (!nvrhiTexture) { - Msg("! [RenderDevice] Failed to create texture: %s", desc.debugName.c_str()); + Msg("! [RenderDevice] Failed to create texture: %s (%ux%u depth=%u arr=%u mips=%u fmt=%d)", + desc.debugName.c_str(), desc.width, desc.height, desc.depth, desc.arraySize, + desc.mipLevels, (int)desc.format); return TextureHandle{}; } @@ -943,17 +943,20 @@ bool RenderDevice::ValidateShaderHandle(ShaderHandle handle) const { nvrhi::TextureDesc RenderDevice::ConvertTextureDesc(const TextureDesc& desc) { nvrhi::TextureDesc nvrhiDesc; - nvrhiDesc.width = desc.width; - nvrhiDesc.height = desc.height; - nvrhiDesc.depth = desc.depth; - nvrhiDesc.arraySize = desc.arraySize; - nvrhiDesc.mipLevels = desc.mipLevels; + nvrhiDesc.width = desc.width ? desc.width : 1; + nvrhiDesc.height = desc.height ? desc.height : 1; + nvrhiDesc.depth = desc.depth ? desc.depth : 1; + nvrhiDesc.arraySize = desc.arraySize ? desc.arraySize : 1; + nvrhiDesc.mipLevels = desc.mipLevels ? desc.mipLevels : 1; nvrhiDesc.sampleCount = 1; nvrhiDesc.sampleQuality = 0; nvrhiDesc.format = desc.format; nvrhiDesc.debugName = desc.debugName.c_str(); - nvrhiDesc.initialState = desc.initialState; + nvrhiDesc.initialState = desc.initialState == nvrhi::ResourceStates::Unknown + ? nvrhi::ResourceStates::ShaderResource + : desc.initialState; nvrhiDesc.keepInitialState = true; + nvrhiDesc.isShaderResource = true; // Dimension switch (desc.dimension) { diff --git a/src/Layers/xrRender/RenderContext/RenderDevice.h b/src/Layers/xrRender/RenderContext/RenderDevice.h index ade4fc42b46..6a45ac9b17d 100644 --- a/src/Layers/xrRender/RenderContext/RenderDevice.h +++ b/src/Layers/xrRender/RenderContext/RenderDevice.h @@ -163,7 +163,7 @@ class RenderDevice { bool isVolatile = false; u32 maxVersions = 0; - static constexpr u32 VOLATILE_CB_MAX_VERSIONS = 64; + static constexpr u32 VOLATILE_CB_MAX_VERSIONS = 256; shared_str debugName; }; diff --git a/src/Layers/xrRender/ResourceManager.cpp b/src/Layers/xrRender/ResourceManager.cpp index 3c93cf47436..324b90c0084 100644 --- a/src/Layers/xrRender/ResourceManager.cpp +++ b/src/Layers/xrRender/ResourceManager.cpp @@ -56,7 +56,9 @@ IBlender* CResourceManager::_FindBlender(LPCSTR Name) } // Helper: Convert oBlend token ID to BlendMode -// Token IDs from Blender_Screen_SET/Blender_Particle: 0=SET, 1=BLEND, 2=ADD, 3=MUL, 4=MUL_2X, 5=ALPHA-ADD +// Screen_SET: 0=SET, 1=BLEND, 2=ADD, 3=MUL, 4=MUL_2X, 5=ALPHA-ADD, +// 6=MUL_2X (B^D) wallmarks, 7=SET(2r), 8=BLEND(2r), 9=BLEND(4r) +// Particle: 0..5 only (same as Screen_SET 0..5) static CResourceManager::BlendMode TokenToBlendMode(u32 tokenID) { using BlendMode = CResourceManager::BlendMode; @@ -68,7 +70,11 @@ static CResourceManager::BlendMode TokenToBlendMode(u32 tokenID) case 3: return BlendMode::Multiply; // MUL case 4: return BlendMode::Multiply2X; // MUL_2X case 5: return BlendMode::Additive; // ALPHA-ADD - default: return BlendMode::AlphaBlend; // Unknown - assume blend + case 6: return BlendMode::Multiply2X; // MUL_2X (B^D) — effects\wallmarkmult + case 7: return BlendMode::Opaque; // SET (2r) + case 8: return BlendMode::AlphaBlend; // BLEND (2r) + case 9: return BlendMode::AlphaBlend; // BLEND (4r) + default: return BlendMode::AlphaBlend; } } diff --git a/src/Layers/xrRender/ResourceManager/TextureManager.cpp b/src/Layers/xrRender/ResourceManager/TextureManager.cpp index b5ea7ab2a54..3d50b748bc4 100644 --- a/src/Layers/xrRender/ResourceManager/TextureManager.cpp +++ b/src/Layers/xrRender/ResourceManager/TextureManager.cpp @@ -3,6 +3,8 @@ #include "DDSLoader.h" #include "TextureStreaming.h" #include "../RenderContext/RenderDevice.h" +#include "xrEngine/IRenderBackend.h" +#include // Modern Texture Manager Implementation // Week 1 - Day 1-2: Tasks 1.4, 2.2 @@ -241,11 +243,16 @@ TextureHandle TextureManager::LoadTexture( { shared_str pathStr = path; - // Check if already loaded (deduplication) auto it = m_pathToHandle.find(pathStr); if (it != m_pathToHandle.end()) { TextureHandle existing = it->second; if (ValidateHandle(existing)) { + TextureMetadata& existingMeta = m_textures[existing.index]; + if (existingMeta.state == TextureState::Failed) { + existingMeta.state = TextureState::Unloaded; + existingMeta.nvrhiTexture = nullptr; + LoadTextureSync(existing); + } AddRef(existing); return existing; } @@ -342,7 +349,6 @@ TextureHandle TextureManager::CreateTexture( nvrhiDesc.clearValue = nvrhi::Color(1.0f); } - // Create texture meta.nvrhiTexture = m_device->GetNVRHIDevice()->createTexture(nvrhiDesc); if (meta.nvrhiTexture) { @@ -350,17 +356,14 @@ TextureHandle TextureManager::CreateTexture( meta.residentMips = desc.mipLevels; meta.requestedMips = desc.mipLevels; - // Calculate memory meta.memoryUsed = desc.CalculateMemorySize(); m_stats.texturesResident++; m_stats.totalMemoryUsed += meta.memoryUsed; - - // Msg("~ [TextureManager] Created runtime texture '%s': %ux%ux%u, %.2f MB", - // desc.debugName.c_str(), - // desc.width, desc.height, desc.depth, - // meta.memoryUsed / (1024.0f * 1024.0f)); } else { - Msg("! [TextureManager] Failed to create NVRHI texture '%s'", desc.debugName.c_str()); + Msg("! [TextureManager] Failed to create NVRHI texture '%s' (%ux%u fmt=%d rt=%d uav=%d)", + desc.debugName.c_str(), desc.width, desc.height, (int)desc.format, + (int)desc.isRenderTarget, (int)desc.isUAV); + meta.state = TextureState::Failed; } m_stats.texturesTotal++; @@ -422,9 +425,10 @@ nvrhi::ITexture* TextureManager::GetNVRHITexture(TextureHandle handle) { // AUTO-RELOAD IF EVICTED // ═══════════════════════════════════════════════════ + if (meta.state == TextureState::Failed) + return nullptr; + if (meta.state == TextureState::Unloaded || meta.state == TextureState::Evicted) { - // Msg("! [TextureManager] ⚠️ Accessing evicted texture: %s - reloading...", - // meta.filePath.c_str()); LoadTextureSync(handle); } @@ -708,49 +712,25 @@ void TextureManager::LoadTextureSync(TextureHandle handle) { TextureMetadata& meta = m_textures[handle.index]; - // Already loaded? - if (meta.state == TextureState::Resident) { + if (meta.state == TextureState::Resident || meta.state == TextureState::Failed) return; - } - // Mark as loading meta.state = TextureState::Loading; - // ═══════════════════════════════════════════════════ - // LOAD TEXTURE FILE (AUTO-DETECTS .DDS OR .OGM) - // ═══════════════════════════════════════════════════ - // DDSLoader now automatically detects file type: - // - .dds → Static DDS texture - // - .ogm → Theora video texture (dynamic, per-frame decode) - DDSData ddsData; if (!DDSLoader::LoadFromFile(meta.filePath.c_str(), ddsData)) { Msg("! [TextureManager] Failed to load texture: %s", meta.filePath.c_str()); - meta.state = TextureState::Unloaded; + meta.state = TextureState::Failed; return; } - // Check texture type bool isVideoTexture = (ddsData.type == DDSData::TextureType::Video); bool isSequenceTexture = (ddsData.type == DDSData::TextureType::Sequence); - // if (isVideoTexture) { - // Msg("* [TextureManager] Video texture detected: %s", meta.filePath.c_str()); - // } - // if (isSequenceTexture) { - // Msg("* [TextureManager] Sequence texture detected: %s (%u frames)", - // meta.filePath.c_str(), - // (u32)ddsData.sequenceState->frameData.size()); - // } - - // ═══════════════════════════════════════════════════ - // CHECK MEMORY BUDGET BEFORE ALLOCATING TEXTURE - // ═══════════════════════════════════════════════════ - u64 requiredMemory = ddsData.totalDataSize; if (!EnforceMemoryBudget(requiredMemory)) { - Msg("! [TextureManager] ❌ Cannot load %s - out of memory! (need %llu MB, have %llu / %llu MB used)", + Msg("! [TextureManager] Cannot load %s - out of memory! (need %llu MB, have %llu / %llu MB used)", meta.filePath.c_str(), requiredMemory / (1024 * 1024), m_memoryUsed / (1024 * 1024), @@ -759,99 +739,144 @@ void TextureManager::LoadTextureSync(TextureHandle handle) { return; } - // Create NVRHI texture (without initial data) - fg::RenderDevice::TextureDesc deviceDesc; - deviceDesc.width = ddsData.desc.width; - deviceDesc.height = ddsData.desc.height; - deviceDesc.depth = ddsData.desc.depth; - deviceDesc.arraySize = ddsData.desc.arraySize; - deviceDesc.mipLevels = ddsData.desc.mipLevels; - deviceDesc.format = ddsData.desc.format; - deviceDesc.debugName = ddsData.filePath.c_str(); - - // Determine dimension + nvrhi::TextureDesc nvrhiDesc; + nvrhiDesc.width = std::max(1u, ddsData.desc.width); + nvrhiDesc.height = std::max(1u, ddsData.desc.height); + nvrhiDesc.depth = std::max(1u, ddsData.desc.depth); + nvrhiDesc.arraySize = std::max(1u, ddsData.desc.arraySize); + nvrhiDesc.mipLevels = std::max(1u, ddsData.desc.mipLevels); + nvrhiDesc.format = ddsData.desc.format; + nvrhiDesc.debugName = ddsData.filePath.c_str() ? ddsData.filePath.c_str() : meta.filePath.c_str(); + nvrhiDesc.initialState = nvrhi::ResourceStates::ShaderResource; + nvrhiDesc.keepInitialState = true; + nvrhiDesc.isShaderResource = true; + nvrhiDesc.sampleCount = 1; + switch (ddsData.desc.type) { case TextureDesc::Texture1D: - deviceDesc.dimension = fg::RenderDevice::TextureDesc::Texture1D; + nvrhiDesc.dimension = nvrhi::TextureDimension::Texture1D; break; - case TextureDesc::Texture2D: - deviceDesc.dimension = fg::RenderDevice::TextureDesc::Texture2D; + case TextureDesc::Texture2DArray: + nvrhiDesc.dimension = nvrhi::TextureDimension::Texture2DArray; break; case TextureDesc::Texture3D: - deviceDesc.dimension = fg::RenderDevice::TextureDesc::Texture3D; + nvrhiDesc.dimension = nvrhi::TextureDimension::Texture3D; break; case TextureDesc::TextureCube: - deviceDesc.dimension = fg::RenderDevice::TextureDesc::TextureCube; + nvrhiDesc.dimension = nvrhi::TextureDimension::TextureCube; + break; + default: + nvrhiDesc.dimension = nvrhi::TextureDimension::Texture2D; break; } - // Create texture (no initial data - we'll upload separately) - fg::TextureHandle deviceHandle = m_device->CreateTexture(deviceDesc, nullptr); - if (!deviceHandle.IsValid()) { - Msg("! [TextureManager] Failed to create NVRHI texture: %s", meta.filePath.c_str()); - meta.state = TextureState::Unloaded; - return; + auto* nvDevice = m_device->GetNVRHIDevice(); + meta.nvrhiTexture = nvDevice->createTexture(nvrhiDesc); + if (!meta.nvrhiTexture && nvrhiDesc.format == nvrhi::Format::BGRA8_UNORM) + { + nvrhiDesc.format = nvrhi::Format::RGBA8_UNORM; + meta.nvrhiTexture = nvDevice->createTexture(nvrhiDesc); + if (meta.nvrhiTexture) + Msg("~ [TextureManager] Created %s as RGBA8 (BGRA8 create failed)", meta.filePath.c_str()); } - // Upload all mip levels using RenderDevice's upload API - xr_vector slices; - slices.reserve(ddsData.mipLevels.size()); - - for (const DDSMipLevel& mip : ddsData.mipLevels) { - fg::RenderDevice::TextureSliceData slice; - - // Calculate which array slice and mip this belongs to - // DDS stores: [slice0_mip0, slice0_mip1, ..., slice1_mip0, slice1_mip1, ...] - u32 totalMips = ddsData.desc.mipLevels; - u32 mipIndex = (u32)(&mip - &ddsData.mipLevels[0]); - - slice.arraySlice = mipIndex / totalMips; - slice.mipLevel = mipIndex % totalMips; - slice.data = mip.data; - slice.dataSize = mip.size; - slice.rowPitch = mip.rowPitch; // Use pitch calculated by DDSLoader - slice.slicePitch = mip.slicePitch; // Use pitch calculated by DDSLoader - - slices.push_back(slice); + if (!meta.nvrhiTexture) { + EvictTextures(requiredMemory * 2); + meta.nvrhiTexture = nvDevice->createTexture(nvrhiDesc); } - // Upload all slices in one command list - m_device->UploadTextureData(deviceHandle, slices.data(), (u32)slices.size()); + DDSData faceData[6]; + bool usedFaceFallback = false; + if (!meta.nvrhiTexture && ddsData.desc.type == TextureDesc::TextureCube) { + bool facesOk = true; + for (int face = 0; face < 6 && facesOk; ++face) { + string_path facePath; + xr_sprintf(facePath, "%s-%d", meta.filePath.c_str(), face); + if (!DDSLoader::LoadFromFile(facePath, faceData[face]) || + faceData[face].desc.type != TextureDesc::Texture2D || + faceData[face].desc.width != faceData[0].desc.width || + faceData[face].desc.height != faceData[0].desc.height || + faceData[face].desc.format != faceData[0].desc.format || + faceData[face].desc.mipLevels != faceData[0].desc.mipLevels) { + facesOk = false; + } + } + if (facesOk) { + nvrhiDesc.width = faceData[0].desc.width; + nvrhiDesc.height = faceData[0].desc.height; + nvrhiDesc.depth = 1; + nvrhiDesc.arraySize = 6; + nvrhiDesc.mipLevels = std::max(1u, faceData[0].desc.mipLevels); + nvrhiDesc.format = faceData[0].desc.format; + nvrhiDesc.dimension = nvrhi::TextureDimension::TextureCube; + EvictTextures(faceData[0].totalDataSize * 6ull); + meta.nvrhiTexture = nvDevice->createTexture(nvrhiDesc); + if (meta.nvrhiTexture) { + usedFaceFallback = true; + requiredMemory = faceData[0].totalDataSize * 6ull; + Msg("~ [TextureManager] %s: BC cube create failed, using face DDS (%ux%u fmt=%d)", + meta.filePath.c_str(), nvrhiDesc.width, nvrhiDesc.height, (int)nvrhiDesc.format); + } + } + } - // Store NVRHI handle - meta.nvrhiTexture = m_device->GetNativeTexture(deviceHandle); + if (!meta.nvrhiTexture) { + Msg("! [TextureManager] Failed to create NVRHI texture: %s (%ux%u depth=%u arr=%u mips=%u fmt=%d)", + meta.filePath.c_str(), + nvrhiDesc.width, nvrhiDesc.height, nvrhiDesc.depth, nvrhiDesc.arraySize, + nvrhiDesc.mipLevels, (int)ddsData.desc.format); + meta.state = TextureState::Failed; + return; + } - // ═══════════════════════════════════════════════════ - // STORE VIDEO TEXTURE STATE (IF APPLICABLE) - // ═══════════════════════════════════════════════════ + if (usedFaceFallback) { + for (int face = 0; face < 6; ++face) { + const u32 totalMips = std::max(1u, faceData[face].desc.mipLevels); + for (u32 mipLevel = 0; mipLevel < totalMips && mipLevel < faceData[face].mipLevels.size(); ++mipLevel) { + const DDSMipLevel& mip = faceData[face].mipLevels[mipLevel]; + m_device->UploadTextureDataToNVRHI( + meta.nvrhiTexture.Get(), + (u32)face, + mipLevel, + mip.data, + mip.size, + mip.rowPitch, + mip.slicePitch); + } + } + } else { + for (const DDSMipLevel& mip : ddsData.mipLevels) { + u32 totalMips = std::max(1u, ddsData.desc.mipLevels); + u32 mipIndex = (u32)(&mip - &ddsData.mipLevels[0]); + u32 arraySlice = mipIndex / totalMips; + u32 mipLevel = mipIndex % totalMips; + m_device->UploadTextureDataToNVRHI( + meta.nvrhiTexture.Get(), + arraySlice, + mipLevel, + mip.data, + mip.size, + mip.rowPitch, + mip.slicePitch); + } + } if (isVideoTexture) { - // Move DDSData into metadata so we can update it each frame meta.videoTextureData = xr_make_unique(); - *meta.videoTextureData = std::move(ddsData); // Transfer ownership - - // Msg("* [TextureManager] Video texture state stored for: %s", meta.filePath.c_str()); + *meta.videoTextureData = std::move(ddsData); } if (isSequenceTexture) { - // Move DDSData into metadata so we can animate frames meta.sequenceTextureData = xr_make_unique(); - *meta.sequenceTextureData = std::move(ddsData); // Transfer ownership - - // Initialize animation state + *meta.sequenceTextureData = std::move(ddsData); meta.sequenceTextureData->sequenceState->currentFrame = 0; - - // Msg("* [TextureManager] Sequence texture state stored for: %s", meta.filePath.c_str()); } - // Update metadata meta.state = TextureState::Resident; - meta.residentMips = (isVideoTexture || isSequenceTexture) ? 1 : ddsData.desc.mipLevels; // Video/sequence textures have 1 mip - meta.totalMips = (isVideoTexture || isSequenceTexture) ? 1 : ddsData.desc.mipLevels; - meta.requestedMips = (isVideoTexture || isSequenceTexture) ? 1 : ddsData.desc.mipLevels; - meta.memoryUsed = ddsData.totalDataSize; - - // Update memory tracking + meta.residentMips = (isVideoTexture || isSequenceTexture) ? 1 : nvrhiDesc.mipLevels; + meta.totalMips = (isVideoTexture || isSequenceTexture) ? 1 : nvrhiDesc.mipLevels; + meta.requestedMips = meta.totalMips; + meta.memoryUsed = requiredMemory; m_memoryUsed += meta.memoryUsed; } @@ -987,6 +1012,7 @@ const char* TextureStateToString(TextureState state) { case TextureState::Resident: return "Resident"; case TextureState::Evicting: return "Evicting"; case TextureState::Evicted: return "Evicted"; + case TextureState::Failed: return "Failed"; default: return "Unknown"; } } diff --git a/src/Layers/xrRender/ResourceManager/TextureManager.h b/src/Layers/xrRender/ResourceManager/TextureManager.h index 7c98414ec3e..69865012800 100644 --- a/src/Layers/xrRender/ResourceManager/TextureManager.h +++ b/src/Layers/xrRender/ResourceManager/TextureManager.h @@ -22,11 +22,12 @@ struct DDSData; // Forward declaration for video texture support // ═══════════════════════════════════════════════════ enum class TextureState : u8 { - Unloaded, // Not in memory (on disk only) - Loading, // Async load in progress - Resident, // Fully loaded in VRAM - Evicting, // Marked for eviction - Evicted, // Was resident, now evicted (keep metadata) + Unloaded, + Loading, + Resident, + Evicting, + Evicted, + Failed, }; const char* TextureStateToString(TextureState state); @@ -283,7 +284,7 @@ class TextureManager { // MEMORY MANAGEMENT // ═══════════════════════════════════════════════════ - u64 m_memoryBudget = 2ULL * 1024 * 1024 * 1024; // 2GB default + u64 m_memoryBudget = 16ULL * 1024 * 1024 * 1024; u64 m_memoryUsed = 0; // ═══════════════════════════════════════════════════ diff --git a/src/Layers/xrRender/SMAP_Allocator.h b/src/Layers/xrRender/SMAP_Allocator.h index 1277c2549c0..88b60c3d356 100644 --- a/src/Layers/xrRender/SMAP_Allocator.h +++ b/src/Layers/xrRender/SMAP_Allocator.h @@ -66,7 +66,6 @@ class SMAP_Allocator { VERIFY(_size <= psize && _size > 4); - // setup first in the soup, if empty state if (stack.empty()) { Ivector2 p; @@ -76,7 +75,6 @@ class SMAP_Allocator return true; } - // perform search (first-fit) for (u32 it = 0; it < cpoint.size(); it++) { R.setup(cpoint[it], _size); @@ -94,14 +92,29 @@ class SMAP_Allocator if (bIntersect) continue; - // OK, place cpoint.erase(cpoint.begin() + it); _add(R); return true; } - // fail return false; } + + BOOL reserve(u32 posX, u32 posY, u32 size) + { + if (size < 4 || size > psize) + return FALSE; + if (posX + size > psize || posY + size > psize) + return FALSE; + Ivector2 p; + p.set(int(posX), int(posY)); + SMAP_Rect R{}; + R.setup(p, size); + for (u32 t = 0; t < stack.size(); t++) + if (stack[t].intersect(R)) + return FALSE; + _add(R); + return TRUE; + } }; } // namespace xray::render::fg diff --git a/src/Layers/xrRender/ShaderInfo.cpp b/src/Layers/xrRender/ShaderInfo.cpp index 25a169cb554..d001022f665 100644 --- a/src/Layers/xrRender/ShaderInfo.cpp +++ b/src/Layers/xrRender/ShaderInfo.cpp @@ -5,6 +5,8 @@ #include "Layers/xrRender/Blender_CLSID.h" #include "Layers/xrRender/blenders/Blender_BmmD.h" #include "Layers/xrRender/blenders/Blender_Particle.h" +#include "Layers/xrRender/blenders/blender_deffer_flat.h" +#include "Layers/xrRender/blenders/blender_deffer_model.h" #include "Layers/xrRender/ResourceManager.h" #include "Layers/xrRender/r_FrameGraphRenderer.h" #include "Layers/xrRender/r__scene.h" @@ -69,6 +71,31 @@ bool GetShaderBlendInfo(const char* shaderName, ShaderBlendInfo& out) return true; } +bool GetShaderTessellationMethod(const char* shaderName, u32& outMethod) +{ + outMethod = 0; + if (!shaderName || !shaderName[0]) + return false; + auto* res = GetResources(); + if (!res) + return false; + fg::IBlender* B = res->_FindBlender(shaderName); + if (!B) + return false; + const CLASS_ID cls = B->getDescription().CLS; + if (cls == fg::B_DEFAULT || cls == fg::B_VERT) + { + outMethod = static_cast(B)->GetTessellation(); + return true; + } + if (cls == fg::B_MODEL) + { + outMethod = static_cast(B)->GetTessellation(); + return true; + } + return false; +} + bool GetParticleBlendIndex(const char* shaderName, u32& outIndex) { if (!shaderName || !shaderName[0]) diff --git a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp index 23ef33bbeac..f4eaba7c7f3 100644 --- a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp +++ b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp @@ -5,6 +5,7 @@ #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" #include "Layers/xrRender/GPUCullingManager.h" #include "Layers/xrRender/FrameGraphPasses/PassCommon.h" +#include "Layers/xrRender/FrameGraphPasses/IBLPrefilterPassSetup.h" namespace xray::render { @@ -163,7 +164,7 @@ nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( pipeDesc.renderState.rasterState = pass.rasterState; pipeDesc.renderState.rasterState.frontCounterClockwise = false; - if (pass.blendEnabled) + if (pass.blendEnabled && !(remappedToSkinned && !variant.transparent)) { pipeDesc.renderState.blendState.targets[0] = pass.blendRT; pipeDesc.renderState.blendState.alphaToCoverageEnable = pass.alphaToCoverage; @@ -249,20 +250,20 @@ void DrawVariantPartition( framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "VariantPartition"); bsb.ConstantBuffer("static_globals", cfg.staticGlobalsCB); - bsb.BufferSRV("g_Materials", cfg.materialBuffer); - bsb.BufferSRV("g_VariantTextures", cfg.variantTexBuffer); + fg::passes::BindBindlessMaterialTables(bsb); bsb.BufferSRV("g_InstanceData", cfg.instanceBuffer); bsb.BufferSRV("g_CompactBatchIndices", p.batchIndicesBuffer); bsb.BufferSRV("g_CompactMaterialIDs", p.materialIDsBuffer); if (cfg.lightDataBuffer) bsb.BufferSRV("g_LightData", cfg.lightDataBuffer); + if (cfg.shadowDataBuffer) + bsb.BufferSRV("g_ShadowData", cfg.shadowDataBuffer); if (cfg.clusterGridBuffer) bsb.BufferSRV("g_ClusterGrid", cfg.clusterGridBuffer); if (cfg.lightIndexListBuffer) bsb.BufferSRV("g_LightIndexList", cfg.lightIndexListBuffer); - // CSM is fixed-register in bindless_forward; water has no shadow map. if (xr_strcmp(psName, "water") != 0) { static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; @@ -273,16 +274,40 @@ void DrawVariantPartition( if (t) bsb.Texture(kNames[i], t); } + nvrhi::ITexture* contactHist = cfg.contactHistory + ? cfg.contactHistory + : smpCache.GetDummyContactHistory(nvDevice); + if (contactHist) + bsb.Texture("g_ContactHistory", contactHist); nvrhi::ITexture* localAtlas = cfg.localShadowAtlas ? cfg.localShadowAtlas - : framegraph::GetPassResourceCache().GetDummyShadowMap(nvDevice); + : smpCache.GetDummyShadowMap(nvDevice); if (localAtlas) bsb.Texture("g_LocalShadowAtlas", localAtlas); + nvrhi::ITexture* localEsm = cfg.localShadowESM + ? cfg.localShadowESM + : smpCache.GetDummyLocalShadowESM(nvDevice); + if (localEsm) + bsb.Texture("g_LocalShadowESM", localEsm); + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + nvrhi::ITexture* dummyHzb = smpCache.GetDummyContactDepth(nvDevice); + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* hz = cfg.shadowHZB[i] ? cfg.shadowHZB[i] : dummyHzb; + if (hz) + bsb.Texture(kHzb[i], hz); + } + nvrhi::ITexture* shadowMask = cfg.shadowMask + ? cfg.shadowMask + : smpCache.GetDummyContactHistory(nvDevice); + if (shadowMask) + bsb.Texture("g_ShadowMask", shadowMask); } if (sky0) bsb.Texture("s_env0", sky0); if (sky1) bsb.Texture("s_env1", sky1); + xray::render::fg::passes::BindIBLResources(bsb, xray::render::fg::passes::GetCurrentIBLBindResources(), nvDevice); auto bindingSet = smpCache.GetOrCreateBindingSet(bsb.Build(), layout, nvDevice); if (!bindingSet) diff --git a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h index 7b9e09593c7..5be3aac4c95 100644 --- a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h +++ b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h @@ -76,10 +76,15 @@ struct VariantPartitionDrawConfig nvrhi::IBuffer* megaVertexBuffer = nullptr; nvrhi::ITexture* shadowMapArray = nullptr; nvrhi::ITexture* shadowCascades[3] = {}; + nvrhi::ITexture* shadowHZB[3] = {}; nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* localShadowESM = nullptr; + nvrhi::ITexture* contactHistory = nullptr; + nvrhi::ITexture* shadowMask = nullptr; nvrhi::ITexture* envSky0 = nullptr; nvrhi::ITexture* envSky1 = nullptr; nvrhi::IBuffer* lightDataBuffer = nullptr; + nvrhi::IBuffer* shadowDataBuffer = nullptr; nvrhi::IBuffer* clusterGridBuffer = nullptr; nvrhi::IBuffer* lightIndexListBuffer = nullptr; VariantPartitionConfig partition; diff --git a/src/Layers/xrRender/TextureDescrManager.cpp b/src/Layers/xrRender/TextureDescrManager.cpp index 9ff83ca0c35..7b563867a8b 100644 --- a/src/Layers/xrRender/TextureDescrManager.cpp +++ b/src/Layers/xrRender/TextureDescrManager.cpp @@ -344,18 +344,16 @@ shared_str CTextureDescrMngr::GetParallaxName(const shared_str& tex_name) const shared_str CTextureDescrMngr::GetPBRName(const shared_str& tex_name) const { - // First check .thm metadata for explicit pbr_name map_TD::const_iterator I = m_texture_details.find(tex_name); if (I != m_texture_details.end()) { if (I->second.m_spec && I->second.m_spec->m_pbr_name.size()) { - return I->second.m_spec->m_pbr_name; + if (TextureFileExists(I->second.m_spec->m_pbr_name.c_str())) + return I->second.m_spec->m_pbr_name; } } - // Fallback: Check if _pbr file exists with naming convention - // tex_name might be "wood" or "wood_d", try to derive base name xr_string base = tex_name.c_str(); if (base.size() > 2 && base.substr(base.size() - 2) == "_d") base = base.substr(0, base.size() - 2); @@ -368,6 +366,20 @@ shared_str CTextureDescrMngr::GetPBRName(const shared_str& tex_name) const return ""; } +shared_str CTextureDescrMngr::GetSSSName(const shared_str& tex_name) const +{ + xr_string base = tex_name.c_str(); + if (base.size() > 2 && base.substr(base.size() - 2) == "_d") + base = base.substr(0, base.size() - 2); + + string256 nm; + strconcat(sizeof(nm), nm, base.c_str(), "_sss"); + if (TextureFileExists(nm)) + return nm; + + return ""; +} + BOOL CTextureDescrMngr::UseSteepParallax(const shared_str& tex_name) const { map_TD::const_iterator I = m_texture_details.find(tex_name); diff --git a/src/Layers/xrRender/TextureDescrManager.h b/src/Layers/xrRender/TextureDescrManager.h index 954e9fce19b..86943e514f1 100644 --- a/src/Layers/xrRender/TextureDescrManager.h +++ b/src/Layers/xrRender/TextureDescrManager.h @@ -74,6 +74,8 @@ class CTextureDescrMngr shared_str GetParallaxName(const shared_str& tex_name) const; // Consolidated packed PBR texture (R=metallic, G=roughness, B=ao, A=parallax): shared_str GetPBRName(const shared_str& tex_name) const; + // SSS map (R=thickness, G=strength, B=profile): _sss + shared_str GetSSSName(const shared_str& tex_name) const; }; extern ECORE_API CTextureDescrMngr TextureDescr; diff --git a/src/Layers/xrRender/Upscaling/DlssFgPassSetup.cpp b/src/Layers/xrRender/Upscaling/DlssFgPassSetup.cpp new file mode 100644 index 00000000000..fdba00e4ce0 --- /dev/null +++ b/src/Layers/xrRender/Upscaling/DlssFgPassSetup.cpp @@ -0,0 +1,143 @@ +#include "stdafx.h" +#include "DlssFgPassSetup.h" +#include "StreamlineDLSS.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/FrameGraphPasses/TAAPassSetup.h" + +extern ENGINE_API int ps_r_dlss_fg; +extern ENGINE_API int ps_r_upscale; + +namespace xray::render::fg::passes { +using namespace framegraph; + +namespace { + +void CopyMatrix(float dst[16], const Fmatrix& m) +{ + dst[0] = m._11; dst[1] = m._12; dst[2] = m._13; dst[3] = m._14; + dst[4] = m._21; dst[5] = m._22; dst[6] = m._23; dst[7] = m._24; + dst[8] = m._31; dst[9] = m._32; dst[10] = m._33; dst[11] = m._34; + dst[12] = m._41; dst[13] = m._42; dst[14] = m._43; dst[15] = m._44; +} + +} + +void setupDlssFgPass( + FrameGraph& fg, + VirtualResourceHandle colorWithUI, + VirtualResourceHandle hudlessColor, + VirtualResourceHandle depth, + VirtualResourceHandle motionVectors, + const Fmatrix& viewToClip, + const Fmatrix& prevViewProj, + const Fmatrix& currViewProj, + u32 renderW, + u32 renderH, + u32 displayW, + u32 displayH, + bool reset) +{ + if (ps_r_upscale != 2 || !ps_r_dlss_fg || !Streamline_IsFGAvailable()) + return; + if (!colorWithUI.is_valid() || !depth.is_valid() || !motionVectors.is_valid()) + return; + + struct PassData + { + VirtualResourceHandle color; + VirtualResourceHandle hudless; + VirtualResourceHandle depth; + VirtualResourceHandle motion; + Fmatrix viewToClip{}; + Fmatrix clipToView{}; + Fmatrix clipToPrev{}; + Fmatrix prevToClip{}; + u32 renderW = 0, renderH = 0; + u32 displayW = 0, displayH = 0; + bool reset = false; + float jitterX = 0.f, jitterY = 0.f; + }; + + Fmatrix invCurr; + invCurr.invert(currViewProj); + Fmatrix clipToPrev; + clipToPrev.mul(prevViewProj, invCurr); + Fmatrix prevToClip; + prevToClip.invert(clipToPrev); + Fmatrix clipToView; + clipToView.invert(viewToClip); + + fg.addCallbackPass( + "DLSS-FG", + [&](FrameGraph& builder, PassHandle passHandle, PassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.color = pb.read(colorWithUI, ResourceState::ShaderResource); + if (hudlessColor.is_valid()) + data.hudless = pb.read(hudlessColor, ResourceState::ShaderResource); + data.depth = pb.read(depth, ResourceState::ShaderResource); + data.motion = pb.read(motionVectors, ResourceState::ShaderResource); + pb.sideEffects(); + data.viewToClip = viewToClip; + data.clipToView = clipToView; + data.clipToPrev = clipToPrev; + data.prevToClip = prevToClip; + data.renderW = renderW; + data.renderH = renderH; + data.displayW = displayW; + data.displayH = displayH; + data.reset = reset; + data.jitterX = g_taa_jitter_px; + data.jitterY = g_taa_jitter_py; + }, + [](const PassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + if (!ctx) + return; + auto* color = fgGraph.GetPhysicalTexture(data.color); + auto* depthTex = fgGraph.GetPhysicalTexture(data.depth); + auto* mv = fgGraph.GetPhysicalTexture(data.motion); + if (!color || !depthTex || !mv) + return; + + DlssFgInputs in{}; + in.backbuffer = color; + in.hudless = data.hudless.is_valid() ? fgGraph.GetPhysicalTexture(data.hudless) : nullptr; + in.depth = depthTex; + in.motionVectors = mv; + in.displayWidth = data.displayW; + in.displayHeight = data.displayH; + in.renderWidth = data.renderW; + in.renderHeight = data.renderH; + in.jitterX = data.jitterX; + in.jitterY = data.jitterY; + in.reset = data.reset; + CopyMatrix(in.viewToClip, data.viewToClip); + CopyMatrix(in.clipToView, data.clipToView); + CopyMatrix(in.clipToPrevClip, data.clipToPrev); + CopyMatrix(in.prevClipToClip, data.prevToClip); + in.cameraPos[0] = Device.vCameraPosition.x; + in.cameraPos[1] = Device.vCameraPosition.y; + in.cameraPos[2] = Device.vCameraPosition.z; + in.cameraUp[0] = Device.vCameraTop.x; + in.cameraUp[1] = Device.vCameraTop.y; + in.cameraUp[2] = Device.vCameraTop.z; + in.cameraFwd[0] = Device.vCameraDirection.x; + in.cameraFwd[1] = Device.vCameraDirection.y; + in.cameraFwd[2] = Device.vCameraDirection.z; + Fvector right; + right.crossproduct(Device.vCameraTop, Device.vCameraDirection); + right.normalize_safe(); + in.cameraRight[0] = right.x; + in.cameraRight[1] = right.y; + in.cameraRight[2] = right.z; + in.cameraNear = VIEWPORT_NEAR; + in.cameraFar = g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.far_plane : 600.f; + in.cameraFOV = deg2rad(Device.fFOV); + in.cameraAspect = (data.displayH > 0) ? ((float)data.displayW / (float)data.displayH) : 1.f; + + Streamline_EvaluateDLSSG(ctx->GetCommandList(), in); + }); +} + +} diff --git a/src/Layers/xrRender/Upscaling/DlssFgPassSetup.h b/src/Layers/xrRender/Upscaling/DlssFgPassSetup.h new file mode 100644 index 00000000000..f2396d59e0f --- /dev/null +++ b/src/Layers/xrRender/Upscaling/DlssFgPassSetup.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes { + +void setupDlssFgPass( + framegraph::FrameGraph& fg, + framegraph::VirtualResourceHandle colorWithUI, + framegraph::VirtualResourceHandle hudlessColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle motionVectors, + const Fmatrix& viewToClip, + const Fmatrix& prevViewProj, + const Fmatrix& currViewProj, + u32 renderW, + u32 renderH, + u32 displayW, + u32 displayH, + bool reset); + +} diff --git a/src/Layers/xrRender/Upscaling/IUpscaleBackend.h b/src/Layers/xrRender/Upscaling/IUpscaleBackend.h new file mode 100644 index 00000000000..e76c71cf3b9 --- /dev/null +++ b/src/Layers/xrRender/Upscaling/IUpscaleBackend.h @@ -0,0 +1,74 @@ +#pragma once + +#include + +namespace xray::render::fg { + +enum class UpscaleBackendType : u32 +{ + None = 0, + FSR, + DLSS +}; + +enum class UpscaleQuality : u32 +{ + UltraPerformance = 0, + Performance, + Balanced, + Quality, + Native, + DLAA +}; + +struct UpscaleInputs +{ + nvrhi::ITexture* color = nullptr; + nvrhi::ITexture* depth = nullptr; + nvrhi::ITexture* motionVectors = nullptr; + nvrhi::ITexture* exposure = nullptr; + nvrhi::ITexture* reactiveMask = nullptr; + nvrhi::ITexture* output = nullptr; + nvrhi::ITexture* normals = nullptr; + nvrhi::ITexture* diffuseAlbedo = nullptr; + nvrhi::ITexture* specularAlbedo = nullptr; + nvrhi::ITexture* roughness = nullptr; + nvrhi::ITexture* diffuseHitDistance = nullptr; + nvrhi::ITexture* specularHitDistance = nullptr; + nvrhi::ITexture* noisyDiffuse = nullptr; + nvrhi::ITexture* noisySpecular = nullptr; + float worldToView[16]{}; + float viewToClip[16]{}; + u32 renderWidth = 0; + u32 renderHeight = 0; + u32 displayWidth = 0; + u32 displayHeight = 0; + float jitterX = 0.f; + float jitterY = 0.f; + float sharpness = 0.f; + float frameTimeMs = 16.f; + u32 frameIndex = 0; + bool reset = false; + bool enableFG = false; + bool enableRR = false; +}; + +class IUpscaleBackend +{ +public: + virtual ~IUpscaleBackend() = default; + virtual bool Init(nvrhi::IDevice* device) = 0; + virtual void Shutdown() = 0; + virtual bool IsAvailable() const = 0; + virtual UpscaleBackendType GetType() const = 0; + virtual bool SupportsFG() const { return false; } + virtual bool SupportsRR() const { return false; } + virtual bool Evaluate(nvrhi::ICommandList* cmd, const UpscaleInputs& inputs) = 0; +}; + +IUpscaleBackend* CreateNullUpscaleBackend(); +IUpscaleBackend* CreateFSRUpscaleBackend(); +IUpscaleBackend* CreateDLSSUpscaleBackend(); +IUpscaleBackend* CreateUpscaleBackendAuto(); + +} diff --git a/src/Layers/xrRender/Upscaling/NgxDLSS.cpp b/src/Layers/xrRender/Upscaling/NgxDLSS.cpp new file mode 100644 index 00000000000..ba9bca0f834 --- /dev/null +++ b/src/Layers/xrRender/Upscaling/NgxDLSS.cpp @@ -0,0 +1,810 @@ +#include "stdafx.h" +#include "StreamlineDLSS.h" +#include "Layers/xrRender/Backend/VulkanBackend.h" + +#if defined(XRAY_USE_DLSS) + +#include +#include +#include +#include +#include +#include +#if defined(XR_PLATFORM_LINUX) +#include +#include +#include +#endif + +#include "nvsdk_ngx.h" +#include "nvsdk_ngx_defs_dlssg.h" +#include "nvsdk_ngx_helpers.h" +#include "nvsdk_ngx_helpers_vk.h" +#include "nvsdk_ngx_helpers_dlssd_vk.h" +#include "nvsdk_ngx_helpers_dlssg_vk.h" + +extern ENGINE_API int ps_r_dlss_quality; +extern ENGINE_API int ps_r_dlss_fg; +extern ENGINE_API int ps_r_dlss_rr; +extern ENGINE_API int ps_r_dlss_auto_exposure; + +namespace xray::render::fg { +namespace { + +constexpr const char* kNgxProjectId = "a0f57b54-1daf-4934-90ae-c4035c19df04"; +constexpr unsigned long long kNgxApplicationId = 231313132ull; + +bool g_slPreInit = false; +bool g_ngxReady = false; +bool g_dlssAvailable = false; +bool g_rrAvailable = false; +bool g_fgAvailable = false; +NVSDK_NGX_Parameter* g_params = nullptr; +NVSDK_NGX_Handle* g_dlssHandle = nullptr; +NVSDK_NGX_Handle* g_rrHandle = nullptr; +NVSDK_NGX_Handle* g_fgHandle = nullptr; +u32 g_featW = 0, g_featH = 0, g_outW = 0, g_outH = 0; +int g_featQuality = -1; +int g_featCreateFlags = -1; +bool g_featRR = false; +u32 g_fgW = 0, g_fgH = 0, g_fgRenderW = 0, g_fgRenderH = 0; +u32 g_fgFormat = 0; +nvrhi::TextureHandle g_fgInterp; +nvrhi::TextureHandle g_fgReal; +bool g_fgPresentPending = false; +std::wstring g_appDataPathW; +std::vector g_searchPathsW; +std::vector g_searchPathPtrs; +NVSDK_NGX_FeatureCommonInfo g_featureInfo{}; + +std::wstring Utf8ToWide(const xr_string& path) +{ +#if defined(XR_PLATFORM_WINDOWS) + if (path.empty()) + return L"."; + int n = MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, nullptr, 0); + std::wstring out(n > 0 ? n - 1 : 0, L'\0'); + if (n > 1) + MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, out.data(), n); + return out.empty() ? L"." : out; +#else + std::wstring out; + out.reserve(path.size()); + for (unsigned char c : path) + out.push_back((wchar_t)c); + return out.empty() ? L"." : out; +#endif +} + +xr_string NormalizeFsPath(xr_string path) +{ + for (char& c : path) + { + if (c == '\\') + c = '/'; + } + while (!path.empty() && (path.back() == '/' || path.back() == '\\')) + path.pop_back(); + return path; +} + +xr_string ResolveNgxAppDataPath() +{ + xr_string path; + if (auto* p = FS.get_path("$app_data_root$")) + path = NormalizeFsPath(p->m_Path); + if (path.empty()) + { +#if defined(XR_PLATFORM_LINUX) + char cwd[PATH_MAX] = {}; + if (getcwd(cwd, sizeof(cwd))) + path = cwd; +#endif + } + if (path.empty() && Core.ApplicationPath[0]) + path = NormalizeFsPath(Core.ApplicationPath); + if (path.empty()) + path = "."; + path += "/ngx"; +#if defined(XR_PLATFORM_LINUX) + mkdir(path.c_str(), 0755); +#endif + return path; +} + +void CollectDllSearchPaths() +{ + g_searchPathsW.clear(); + g_searchPathPtrs.clear(); + auto addPath = [&](const xr_string& p) { + xr_string n = NormalizeFsPath(p); + if (n.empty()) + return; + std::wstring w = Utf8ToWide(n); + for (const auto& existing : g_searchPathsW) + if (existing == w) + return; + g_searchPathsW.push_back(std::move(w)); + }; + +#if defined(XR_PLATFORM_LINUX) + char exePath[PATH_MAX] = {}; + const ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); + if (n > 0) + { + exePath[n] = 0; + char* slash = strrchr(exePath, '/'); + if (slash) + { + *slash = 0; + addPath(xr_string(exePath)); + } + } + char cwd[PATH_MAX] = {}; + if (getcwd(cwd, sizeof(cwd))) + addPath(xr_string(cwd)); +#endif + if (Core.ApplicationPath[0]) + addPath(xr_string(Core.ApplicationPath)); + + g_searchPathPtrs.reserve(g_searchPathsW.size()); + for (const auto& w : g_searchPathsW) + g_searchPathPtrs.push_back(w.c_str()); + + memset(&g_featureInfo, 0, sizeof(g_featureInfo)); + g_featureInfo.PathListInfo.Path = g_searchPathPtrs.data(); + g_featureInfo.PathListInfo.Length = (unsigned int)g_searchPathPtrs.size(); +} + +NVSDK_NGX_PerfQuality_Value MapQuality(int q) +{ + switch (q) + { + case 0: return NVSDK_NGX_PerfQuality_Value_UltraPerformance; + case 1: return NVSDK_NGX_PerfQuality_Value_MaxPerf; + case 2: return NVSDK_NGX_PerfQuality_Value_Balanced; + case 3: return NVSDK_NGX_PerfQuality_Value_MaxQuality; + case 5: return NVSDK_NGX_PerfQuality_Value_DLAA; + default: return NVSDK_NGX_PerfQuality_Value_MaxQuality; + } +} + +bool MakeResourceVK(nvrhi::ITexture* tex, bool readWrite, NVSDK_NGX_Resource_VK& out) +{ + if (!tex) + return false; + const auto img = tex->getNativeObject(nvrhi::ObjectTypes::VK_Image); + const auto view = tex->getNativeView(nvrhi::ObjectTypes::VK_ImageView); + if (!img.integer || !view.integer) + return false; + const nvrhi::Format fmt = tex->getDesc().format; + VkImageSubresourceRange range{}; + range.aspectMask = (fmt == nvrhi::Format::D32 || + fmt == nvrhi::Format::D16 || + fmt == nvrhi::Format::D24S8 || + fmt == nvrhi::Format::D32S8) + ? VK_IMAGE_ASPECT_DEPTH_BIT + : VK_IMAGE_ASPECT_COLOR_BIT; + range.levelCount = 1; + range.layerCount = 1; + out = NVSDK_NGX_Create_ImageView_Resource_VK( + (VkImageView)view.integer, + (VkImage)img.integer, + range, + (VkFormat)nvrhi::vulkan::convertFormat(tex->getDesc().format), + tex->getDesc().width, + tex->getDesc().height, + readWrite); + return true; +} + +void DestroyFgFeature() +{ + if (g_fgHandle && g_params) + { + NVSDK_NGX_VULKAN_ReleaseFeature(g_fgHandle); + g_fgHandle = nullptr; + } + g_fgInterp = nullptr; + g_fgReal = nullptr; + g_fgPresentPending = false; + g_fgW = g_fgH = g_fgRenderW = g_fgRenderH = 0; + g_fgFormat = 0; +} + +void DestroyFeatures() +{ + if (g_dlssHandle && g_params) + { + NVSDK_NGX_VULKAN_ReleaseFeature(g_dlssHandle); + g_dlssHandle = nullptr; + } + if (g_rrHandle && g_params) + { + NVSDK_NGX_VULKAN_ReleaseFeature(g_rrHandle); + g_rrHandle = nullptr; + } + g_featW = g_featH = g_outW = g_outH = 0; + g_featQuality = -1; + g_featCreateFlags = -1; + g_featRR = false; +} + +void CopyTo44(float dst[4][4], const float src[16]) +{ + for (int r = 0; r < 4; ++r) + for (int c = 0; c < 4; ++c) + dst[r][c] = src[r * 4 + c]; +} + +bool EnsureFeature(nvrhi::ICommandList* cmd, const UpscaleInputs& inputs, bool wantRR) +{ + if (!g_ngxReady || !g_params || !cmd) + return false; + int createFlags = + NVSDK_NGX_DLSS_Feature_Flags_IsHDR | + NVSDK_NGX_DLSS_Feature_Flags_MVLowRes; + if (ps_r_dlss_auto_exposure) + createFlags |= NVSDK_NGX_DLSS_Feature_Flags_AutoExposure; + + if (g_featW == inputs.renderWidth && g_featH == inputs.renderHeight && + g_outW == inputs.displayWidth && g_outH == inputs.displayHeight && + g_featQuality == ps_r_dlss_quality && g_featRR == wantRR && + g_featCreateFlags == createFlags && + (wantRR ? g_rrHandle : g_dlssHandle)) + return true; + + DestroyFeatures(); + + auto* vkCmd = (VkCommandBuffer)cmd->getNativeObject(nvrhi::ObjectTypes::VK_CommandBuffer).integer; + auto* backend = dynamic_cast(GEnv.Backend); + if (!vkCmd || !backend) + return false; + + NVSDK_NGX_DLSS_Create_Params create{}; + create.Feature.InWidth = inputs.renderWidth; + create.Feature.InHeight = inputs.renderHeight; + create.Feature.InTargetWidth = inputs.displayWidth; + create.Feature.InTargetHeight = inputs.displayHeight; + create.Feature.InPerfQualityValue = MapQuality(ps_r_dlss_quality); + create.InFeatureCreateFlags = createFlags; + + NVSDK_NGX_Result res; + if (wantRR && g_rrAvailable) + { + NVSDK_NGX_DLSSD_Create_Params rrCreate{}; + rrCreate.InDenoiseMode = NVSDK_NGX_DLSS_Denoise_Mode_DLUnified; + rrCreate.InRoughnessMode = NVSDK_NGX_DLSS_Roughness_Mode_Packed; + rrCreate.InUseHWDepth = NVSDK_NGX_DLSS_Depth_Type_HW; + rrCreate.InWidth = inputs.renderWidth; + rrCreate.InHeight = inputs.renderHeight; + rrCreate.InTargetWidth = inputs.displayWidth; + rrCreate.InTargetHeight = inputs.displayHeight; + rrCreate.InPerfQualityValue = MapQuality(ps_r_dlss_quality); + rrCreate.InFeatureCreateFlags = create.InFeatureCreateFlags; + res = NGX_VULKAN_CREATE_DLSSD_EXT1( + backend->GetVkDevice(), vkCmd, 1, 1, &g_rrHandle, g_params, &rrCreate); + if (NVSDK_NGX_FAILED(res)) + { + Msg("! [Upscale] DLSS-RR CreateFeature failed 0x%08x — falling back to DLSS SR", (u32)res); + wantRR = false; + } + } + + if (!wantRR) + { + res = NGX_VULKAN_CREATE_DLSS_EXT1( + backend->GetVkDevice(), vkCmd, 1, 1, &g_dlssHandle, g_params, &create); + if (NVSDK_NGX_FAILED(res)) + { + Msg("! [Upscale] DLSS CreateFeature failed 0x%08x", (u32)res); + return false; + } + } + + g_featW = inputs.renderWidth; + g_featH = inputs.renderHeight; + g_outW = inputs.displayWidth; + g_outH = inputs.displayHeight; + g_featQuality = ps_r_dlss_quality; + g_featCreateFlags = createFlags; + g_featRR = wantRR && g_rrHandle != nullptr; + return true; +} + +} + +bool Streamline_PreInstanceInit() +{ + g_slPreInit = true; + return true; +} + +void Streamline_GetRequiredInstanceExtensions(xr_vector& outExts) +{ + unsigned int instCount = 0, devCount = 0; + const char** instExts = nullptr; + const char** devExts = nullptr; + if (NVSDK_NGX_SUCCEED(NVSDK_NGX_VULKAN_RequiredExtensions(&instCount, &instExts, &devCount, &devExts))) + { + for (unsigned int i = 0; i < instCount; ++i) + outExts.push_back(instExts[i]); + (void)devExts; + (void)devCount; + } +} + +void Streamline_GetRequiredDeviceExtensions(void* /*physicalDevice*/, xr_vector& outExts) +{ + unsigned int instCount = 0, devCount = 0; + const char** instExts = nullptr; + const char** devExts = nullptr; + if (NVSDK_NGX_SUCCEED(NVSDK_NGX_VULKAN_RequiredExtensions(&instCount, &instExts, &devCount, &devExts))) + { + for (unsigned int i = 0; i < devCount; ++i) + outExts.push_back(devExts[i]); + (void)instExts; + (void)instCount; + } +} + +bool Streamline_SetVulkanInfo(const StreamlineVulkanInfo& info) +{ + if (!info.instance || !info.physicalDevice || !info.device) + return false; + if (g_ngxReady) + return g_dlssAvailable; + + const xr_string appData = ResolveNgxAppDataPath(); + g_appDataPathW = Utf8ToWide(appData); + CollectDllSearchPaths(); + Msg("* [Upscale] NGX init appData=%s dllPaths=%u", appData.c_str(), (u32)g_searchPathPtrs.size()); + FlushLog(); + + Msg("* [Upscale] NGX calling VULKAN_Init (ApplicationId)..."); + FlushLog(); + NVSDK_NGX_Result res = NVSDK_NGX_VULKAN_Init( + kNgxApplicationId, + g_appDataPathW.c_str(), + (VkInstance)info.instance, + (VkPhysicalDevice)info.physicalDevice, + (VkDevice)info.device, + vkGetInstanceProcAddr, + vkGetDeviceProcAddr, + g_searchPathPtrs.empty() ? nullptr : &g_featureInfo, + NVSDK_NGX_Version_API); + + if (NVSDK_NGX_FAILED(res)) + { + Msg("! [Upscale] NGX ApplicationId Init failed 0x%08x — trying ProjectID", (u32)res); + FlushLog(); + res = NVSDK_NGX_VULKAN_Init_with_ProjectID( + kNgxProjectId, + NVSDK_NGX_ENGINE_TYPE_CUSTOM, + "1.6.0", + g_appDataPathW.c_str(), + (VkInstance)info.instance, + (VkPhysicalDevice)info.physicalDevice, + (VkDevice)info.device, + vkGetInstanceProcAddr, + vkGetDeviceProcAddr, + g_searchPathPtrs.empty() ? nullptr : &g_featureInfo, + NVSDK_NGX_Version_API); + } + + if (NVSDK_NGX_FAILED(res)) + { + Msg("! [Upscale] NGX Vulkan Init failed 0x%08x", (u32)res); + FlushLog(); + return false; + } + Msg("* [Upscale] NGX Init OK (0x%08x)", (u32)res); + FlushLog(); + + res = NVSDK_NGX_VULKAN_GetCapabilityParameters(&g_params); + if (NVSDK_NGX_FAILED(res) || !g_params) + { + Msg("! [Upscale] NGX GetCapabilityParameters failed"); + NVSDK_NGX_VULKAN_Shutdown1((VkDevice)info.device); + return false; + } + + int needsUpdatedDriver = 0; + unsigned int minDriver = 0; + NVSDK_NGX_Parameter_GetI(g_params, NVSDK_NGX_Parameter_SuperSampling_NeedsUpdatedDriver, &needsUpdatedDriver); + NVSDK_NGX_Parameter_GetUI(g_params, NVSDK_NGX_Parameter_SuperSampling_MinDriverVersionMajor, &minDriver); + int dlssSupported = 0; + NVSDK_NGX_Parameter_GetI(g_params, NVSDK_NGX_Parameter_SuperSampling_Available, &dlssSupported); + g_dlssAvailable = dlssSupported != 0; + + int rrSupported = 0; + NVSDK_NGX_Parameter_GetI(g_params, NVSDK_NGX_Parameter_SuperSamplingDenoising_Available, &rrSupported); + g_rrAvailable = rrSupported != 0; + + int fgSupported = 0; + NVSDK_NGX_Parameter_GetI(g_params, NVSDK_NGX_Parameter_FrameGeneration_Available, &fgSupported); + if (!fgSupported) + NVSDK_NGX_Parameter_GetI(g_params, NVSDK_NGX_Parameter_FrameInterpolation_Available, &fgSupported); + g_fgAvailable = fgSupported != 0; + if (!g_fgAvailable) + { + int fgInit = 0; + unsigned int fgMinMaj = 0, fgMinMin = 0; + int fgNeedsDriver = 0; + NVSDK_NGX_Parameter_GetI(g_params, NVSDK_NGX_Parameter_FrameGeneration_FeatureInitResult, &fgInit); + NVSDK_NGX_Parameter_GetUI(g_params, NVSDK_NGX_Parameter_FrameGeneration_MinDriverVersionMajor, &fgMinMaj); + NVSDK_NGX_Parameter_GetUI(g_params, NVSDK_NGX_Parameter_FrameGeneration_MinDriverVersionMinor, &fgMinMin); + NVSDK_NGX_Parameter_GetI(g_params, NVSDK_NGX_Parameter_FrameGeneration_NeedsUpdatedDriver, &fgNeedsDriver); + Msg("! [Upscale] DLSS-FG unavailable initResult=0x%08x needsDriver=%d minDriver=%u.%u (RTX 40+ required)", + (u32)fgInit, fgNeedsDriver, fgMinMaj, fgMinMin); + } + + g_ngxReady = true; + Msg("* [Upscale] NGX ready dlss=%d rr=%d fg=%d needsDriver=%d minMajor=%u", + g_dlssAvailable ? 1 : 0, g_rrAvailable ? 1 : 0, g_fgAvailable ? 1 : 0, + needsUpdatedDriver, minDriver); + if (!g_dlssAvailable) + Msg("! [Upscale] DLSS plugin unavailable — check libnvidia-ngx-dlss.so next to xr_3da / LD_LIBRARY_PATH"); + return g_dlssAvailable; +} + +void Streamline_Shutdown() +{ + DestroyFgFeature(); + DestroyFeatures(); + if (g_params) + { + NVSDK_NGX_VULKAN_DestroyParameters(g_params); + g_params = nullptr; + } + if (g_ngxReady) + { + auto* backend = dynamic_cast(GEnv.Backend); + if (backend) + NVSDK_NGX_VULKAN_Shutdown1(backend->GetVkDevice()); + } + g_ngxReady = false; + g_dlssAvailable = g_rrAvailable = g_fgAvailable = false; +} + +bool Streamline_IsDLSSAvailable() { return g_dlssAvailable; } +bool Streamline_IsFGAvailable() { return g_fgAvailable; } +bool Streamline_IsRRAvailable() { return g_rrAvailable; } + +bool EvaluateDLSSSR(VkCommandBuffer vkCmd, const UpscaleInputs& inputs, + NVSDK_NGX_Resource_VK& color, NVSDK_NGX_Resource_VK& depth, NVSDK_NGX_Resource_VK& mv, + NVSDK_NGX_Resource_VK& out, NVSDK_NGX_Resource_VK* exposure) +{ + if (!g_dlssHandle) + return false; + NVSDK_NGX_VK_DLSS_Eval_Params eval{}; + eval.Feature.pInColor = &color; + eval.Feature.pInOutput = &out; + eval.Feature.InSharpness = inputs.sharpness; + eval.pInDepth = &depth; + eval.pInMotionVectors = &mv; + eval.InJitterOffsetX = inputs.jitterX; + eval.InJitterOffsetY = inputs.jitterY; + eval.InRenderSubrectDimensions = { inputs.renderWidth, inputs.renderHeight }; + eval.InReset = inputs.reset ? 1 : 0; + eval.InMVScaleX = (float)inputs.renderWidth; + eval.InMVScaleY = (float)inputs.renderHeight; + if (exposure) + eval.pInExposureTexture = exposure; + const NVSDK_NGX_Result res = NGX_VULKAN_EVALUATE_DLSS_EXT(vkCmd, g_dlssHandle, g_params, &eval); + if (NVSDK_NGX_FAILED(res)) + { + Msg("! [Upscale] DLSS SR Evaluate failed 0x%08x", (u32)res); + return false; + } + return true; +} + +bool Streamline_EvaluateDLSS(nvrhi::ICommandList* cmd, const UpscaleInputs& inputs) +{ + if (!g_ngxReady || !cmd || !inputs.color || !inputs.output || !inputs.depth || !inputs.motionVectors) + return false; + + const bool wantRR = inputs.enableRR && g_rrAvailable && + inputs.normals && inputs.diffuseAlbedo && inputs.specularAlbedo; + if (!EnsureFeature(cmd, inputs, wantRR)) + return false; + + NVSDK_NGX_Resource_VK color{}, depth{}, mv{}, out{}, exposure{}; + if (!MakeResourceVK(inputs.color, false, color) || + !MakeResourceVK(inputs.depth, false, depth) || + !MakeResourceVK(inputs.motionVectors, false, mv) || + !MakeResourceVK(inputs.output, true, out)) + return false; + const bool hasExposure = inputs.exposure && MakeResourceVK(inputs.exposure, false, exposure); + + auto* vkCmd = (VkCommandBuffer)cmd->getNativeObject(nvrhi::ObjectTypes::VK_CommandBuffer).integer; + if (!vkCmd) + return false; + + bool ok = false; + if (g_featRR && g_rrHandle && wantRR) + { + NVSDK_NGX_Resource_VK normals{}, diffAlb{}, specAlb{}, diffHit{}, specHit{}; + if (!MakeResourceVK(inputs.normals, false, normals) || + !MakeResourceVK(inputs.diffuseAlbedo, false, diffAlb) || + !MakeResourceVK(inputs.specularAlbedo, false, specAlb)) + { + Msg("! [Upscale] DLSS-RR missing G-buffer — falling back to SR"); + } + else + { + const bool hasDiffHit = inputs.diffuseHitDistance && MakeResourceVK(inputs.diffuseHitDistance, false, diffHit); + const bool hasSpecHit = inputs.specularHitDistance && MakeResourceVK(inputs.specularHitDistance, false, specHit); + + NVSDK_NGX_VK_DLSSD_Eval_Params eval{}; + eval.pInColor = &color; + eval.pInOutput = &out; + eval.pInDepth = &depth; + eval.pInMotionVectors = &mv; + eval.pInDiffuseAlbedo = &diffAlb; + eval.pInSpecularAlbedo = &specAlb; + eval.pInNormals = &normals; + eval.pInRoughness = &normals; + if (hasDiffHit) + eval.pInDiffuseHitDistance = &diffHit; + if (hasSpecHit) + eval.pInSpecularHitDistance = &specHit; + eval.pInWorldToViewMatrix = const_cast(inputs.worldToView); + eval.pInViewToClipMatrix = const_cast(inputs.viewToClip); + eval.InJitterOffsetX = inputs.jitterX; + eval.InJitterOffsetY = inputs.jitterY; + eval.InRenderSubrectDimensions = { inputs.renderWidth, inputs.renderHeight }; + eval.InReset = inputs.reset ? 1 : 0; + eval.InMVScaleX = (float)inputs.renderWidth; + eval.InMVScaleY = (float)inputs.renderHeight; + eval.InFrameTimeDeltaInMsec = inputs.frameTimeMs; + eval.InPreExposure = 1.f; + eval.InExposureScale = 1.f; + if (hasExposure) + eval.pInExposureTexture = &exposure; + + const NVSDK_NGX_Result res = NGX_VULKAN_EVALUATE_DLSSD_EXT(vkCmd, g_rrHandle, g_params, &eval); + if (NVSDK_NGX_SUCCEED(res)) + { + ok = true; + static bool s_rrOk = false; + if (!s_rrOk) + { + s_rrOk = true; + Msg("* [Upscale] DLSS-RR Evaluate OK"); + } + } + else + { + Msg("! [Upscale] DLSS-RR Evaluate failed 0x%08x — falling back to SR", (u32)res); + DestroyFeatures(); + if (!EnsureFeature(cmd, inputs, false)) + return false; + } + } + } + + if (!ok) + ok = EvaluateDLSSSR(vkCmd, inputs, color, depth, mv, out, hasExposure ? &exposure : nullptr); + + return ok; +} + +bool Streamline_EvaluateDLSSRR(nvrhi::ICommandList* cmd, const UpscaleInputs& inputs) +{ + UpscaleInputs rrInputs = inputs; + rrInputs.enableRR = true; + return Streamline_EvaluateDLSS(cmd, rrInputs); +} + +bool EnsureFgFeature(nvrhi::ICommandList* cmd, const DlssFgInputs& inputs, u32 vkFormat) +{ + if (!g_ngxReady || !g_params || !g_fgAvailable || !cmd) + return false; + if (g_fgHandle && g_fgW == inputs.displayWidth && g_fgH == inputs.displayHeight && + g_fgRenderW == inputs.renderWidth && g_fgRenderH == inputs.renderHeight && + g_fgFormat == vkFormat && g_fgInterp && g_fgReal) + return true; + + DestroyFgFeature(); + + auto* vkCmd = (VkCommandBuffer)cmd->getNativeObject(nvrhi::ObjectTypes::VK_CommandBuffer).integer; + if (!vkCmd) + return false; + + NVSDK_NGX_DLSSG_Create_Params create{}; + create.Width = inputs.displayWidth; + create.Height = inputs.displayHeight; + create.NativeBackbufferFormat = vkFormat; + create.RenderWidth = inputs.renderWidth; + create.RenderHeight = inputs.renderHeight; + create.DynamicResolutionScaling = false; + + NVSDK_NGX_Parameter_SetUI(g_params, "Enable.OFA", 1); + NVSDK_NGX_Parameter_SetUI(g_params, "DLSSG.EnableInterp", 1); + + const NVSDK_NGX_Result res = NGX_VK_CREATE_DLSSG(vkCmd, 1, 1, &g_fgHandle, g_params, &create); + if (NVSDK_NGX_FAILED(res) || !g_fgHandle) + { + Msg("! [Upscale] DLSS-FG CreateFeature failed 0x%08x", (u32)res); + g_fgHandle = nullptr; + return false; + } + + nvrhi::IDevice* nv = cmd->getDevice(); + nvrhi::TextureDesc td; + td.width = inputs.displayWidth; + td.height = inputs.displayHeight; + td.format = inputs.backbuffer->getDesc().format; + td.isUAV = true; + td.isShaderResource = true; + td.isRenderTarget = true; + td.initialState = nvrhi::ResourceStates::UnorderedAccess; + td.keepInitialState = true; + td.debugName = "rt_DlssFg_Interp"; + g_fgInterp = nv->createTexture(td); + td.debugName = "rt_DlssFg_Real"; + g_fgReal = nv->createTexture(td); + if (!g_fgInterp || !g_fgReal) + { + DestroyFgFeature(); + return false; + } + + g_fgW = inputs.displayWidth; + g_fgH = inputs.displayHeight; + g_fgRenderW = inputs.renderWidth; + g_fgRenderH = inputs.renderHeight; + g_fgFormat = vkFormat; + Msg("* [Upscale] DLSS-FG CreateFeature OK %ux%u (render %ux%u)", + g_fgW, g_fgH, g_fgRenderW, g_fgRenderH); + return true; +} + +bool Streamline_EvaluateDLSSG(nvrhi::ICommandList* cmd, const DlssFgInputs& inputs) +{ + g_fgPresentPending = false; + if (!g_ngxReady || !cmd || !inputs.backbuffer || !inputs.depth || !inputs.motionVectors) + return false; + if (!g_fgAvailable || !ps_r_dlss_fg) + return false; + + const u32 vkFormat = (u32)nvrhi::vulkan::convertFormat(inputs.backbuffer->getDesc().format); + if (!EnsureFgFeature(cmd, inputs, vkFormat)) + return false; + + NVSDK_NGX_Resource_VK color{}, depth{}, mv{}, hudless{}, interp{}, real{}; + if (!MakeResourceVK(inputs.backbuffer, false, color) || + !MakeResourceVK(inputs.depth, false, depth) || + !MakeResourceVK(inputs.motionVectors, false, mv) || + !MakeResourceVK(g_fgInterp.Get(), true, interp) || + !MakeResourceVK(g_fgReal.Get(), true, real)) + return false; + + const bool hasHudless = inputs.hudless && MakeResourceVK(inputs.hudless, false, hudless); + + auto* vkCmd = (VkCommandBuffer)cmd->getNativeObject(nvrhi::ObjectTypes::VK_CommandBuffer).integer; + if (!vkCmd || !g_fgHandle) + return false; + + NVSDK_NGX_VK_DLSSG_Eval_Params eval{}; + eval.pBackbuffer = &color; + eval.pDepth = &depth; + eval.pMVecs = &mv; + if (hasHudless) + eval.pHudless = &hudless; + eval.pOutputInterpFrame = &interp; + eval.pOutputRealFrame = ℜ + + NVSDK_NGX_DLSSG_Opt_Eval_Params opt{}; + opt.multiFrameCount = 1; + opt.multiFrameIndex = 1; + CopyTo44(opt.cameraViewToClip, inputs.viewToClip); + CopyTo44(opt.clipToCameraView, inputs.clipToView); + for (int i = 0; i < 4; ++i) + for (int j = 0; j < 4; ++j) + opt.clipToLensClip[i][j] = (i == j) ? 1.f : 0.f; + CopyTo44(opt.clipToPrevClip, inputs.clipToPrevClip); + CopyTo44(opt.prevClipToClip, inputs.prevClipToClip); + opt.jitterOffset[0] = inputs.jitterX; + opt.jitterOffset[1] = inputs.jitterY; + opt.mvecScale[0] = (float)inputs.renderWidth; + opt.mvecScale[1] = (float)inputs.renderHeight; + opt.cameraPos[0] = inputs.cameraPos[0]; + opt.cameraPos[1] = inputs.cameraPos[1]; + opt.cameraPos[2] = inputs.cameraPos[2]; + opt.cameraUp[0] = inputs.cameraUp[0]; + opt.cameraUp[1] = inputs.cameraUp[1]; + opt.cameraUp[2] = inputs.cameraUp[2]; + opt.cameraRight[0] = inputs.cameraRight[0]; + opt.cameraRight[1] = inputs.cameraRight[1]; + opt.cameraRight[2] = inputs.cameraRight[2]; + opt.cameraFwd[0] = inputs.cameraFwd[0]; + opt.cameraFwd[1] = inputs.cameraFwd[1]; + opt.cameraFwd[2] = inputs.cameraFwd[2]; + opt.cameraNear = inputs.cameraNear; + opt.cameraFar = inputs.cameraFar; + opt.cameraFOV = inputs.cameraFOV; + opt.cameraAspectRatio = inputs.cameraAspect; + opt.colorBuffersHDR = false; + opt.depthInverted = false; + opt.cameraMotionIncluded = true; + opt.reset = inputs.reset; + opt.motionVectorsDilated = false; + opt.mvecsSubrectSize = { inputs.renderWidth, inputs.renderHeight }; + opt.depthSubrectSize = { inputs.renderWidth, inputs.renderHeight }; + opt.backbufferSubrectSize = { inputs.displayWidth, inputs.displayHeight }; + if (hasHudless) + opt.hudLessSubrectSize = { inputs.displayWidth, inputs.displayHeight }; + + const NVSDK_NGX_Result res = NGX_VK_EVALUATE_DLSSG(vkCmd, g_fgHandle, g_params, &eval, &opt); + if (NVSDK_NGX_FAILED(res)) + { + Msg("! [Upscale] DLSS-FG Evaluate failed 0x%08x", (u32)res); + return false; + } + + if (auto* bb = GEnv.Backend ? GEnv.Backend->GetBackBuffer() : nullptr) + { + if (bb->getDesc().width == g_fgW && bb->getDesc().height == g_fgH && + bb->getDesc().format == g_fgReal->getDesc().format) + { + nvrhi::TextureSlice slice; + cmd->copyTexture(bb, slice, g_fgReal, slice); + } + else + { + static bool s_fmtLogged = false; + if (!s_fmtLogged) + { + s_fmtLogged = true; + Msg("! [Upscale] DLSS-FG blit skipped — BB %ux%u fmt=%u vs FG %ux%u fmt=%u", + bb->getDesc().width, bb->getDesc().height, (u32)bb->getDesc().format, + g_fgW, g_fgH, (u32)g_fgReal->getDesc().format); + } + } + } + + g_fgPresentPending = true; + static bool s_fgOk = false; + if (!s_fgOk) + { + s_fgOk = true; + Msg("* [Upscale] DLSS-FG Evaluate OK"); + } + return true; +} + +bool Streamline_TakeFgPresent(nvrhi::ITexture*& outInterp, nvrhi::ITexture*& outReal) +{ + if (!g_fgPresentPending || !g_fgInterp || !g_fgReal) + return false; + outInterp = g_fgInterp.Get(); + outReal = g_fgReal.Get(); + g_fgPresentPending = false; + return true; +} + +} + +#else + +namespace xray::render::fg { + +bool Streamline_PreInstanceInit() { return false; } +void Streamline_GetRequiredInstanceExtensions(xr_vector&) {} +void Streamline_GetRequiredDeviceExtensions(void*, xr_vector&) {} +bool Streamline_SetVulkanInfo(const StreamlineVulkanInfo&) { return false; } +void Streamline_Shutdown() {} +bool Streamline_IsDLSSAvailable() { return false; } +bool Streamline_IsFGAvailable() { return false; } +bool Streamline_IsRRAvailable() { return false; } +bool Streamline_EvaluateDLSS(nvrhi::ICommandList*, const UpscaleInputs&) { return false; } +bool Streamline_EvaluateDLSSRR(nvrhi::ICommandList*, const UpscaleInputs&) { return false; } +bool Streamline_EvaluateDLSSG(nvrhi::ICommandList*, const DlssFgInputs&) { return false; } +bool Streamline_TakeFgPresent(nvrhi::ITexture*&, nvrhi::ITexture*&) { return false; } + +} + +#endif diff --git a/src/Layers/xrRender/Upscaling/StreamlineDLSS.h b/src/Layers/xrRender/Upscaling/StreamlineDLSS.h new file mode 100644 index 00000000000..2b2d8e0e4d4 --- /dev/null +++ b/src/Layers/xrRender/Upscaling/StreamlineDLSS.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include "IUpscaleBackend.h" + +namespace xray::render::fg { + +struct StreamlineVulkanInfo +{ + void* instance = nullptr; + void* physicalDevice = nullptr; + void* device = nullptr; + void* graphicsQueue = nullptr; + u32 graphicsQueueFamily = 0; + void* computeQueue = nullptr; + u32 computeQueueFamily = 0; + void* opticalFlowQueue = nullptr; + u32 opticalFlowQueueFamily = 0; +}; + +struct DlssFgInputs +{ + nvrhi::ITexture* backbuffer = nullptr; + nvrhi::ITexture* hudless = nullptr; + nvrhi::ITexture* depth = nullptr; + nvrhi::ITexture* motionVectors = nullptr; + u32 displayWidth = 0; + u32 displayHeight = 0; + u32 renderWidth = 0; + u32 renderHeight = 0; + float jitterX = 0.f; + float jitterY = 0.f; + bool reset = false; + float viewToClip[16]{}; + float clipToView[16]{}; + float clipToPrevClip[16]{}; + float prevClipToClip[16]{}; + float cameraPos[3]{}; + float cameraUp[3]{}; + float cameraRight[3]{}; + float cameraFwd[3]{}; + float cameraNear = 0.2f; + float cameraFar = 600.f; + float cameraFOV = 1.f; + float cameraAspect = 1.f; +}; + +bool Streamline_PreInstanceInit(); +void Streamline_GetRequiredInstanceExtensions(xr_vector& outExts); +void Streamline_GetRequiredDeviceExtensions(void* physicalDevice, xr_vector& outExts); +bool Streamline_SetVulkanInfo(const StreamlineVulkanInfo& info); +void Streamline_Shutdown(); + +bool Streamline_IsDLSSAvailable(); +bool Streamline_IsFGAvailable(); +bool Streamline_IsRRAvailable(); + +bool Streamline_EvaluateDLSS(nvrhi::ICommandList* cmd, const UpscaleInputs& inputs); +bool Streamline_EvaluateDLSSRR(nvrhi::ICommandList* cmd, const UpscaleInputs& inputs); +bool Streamline_EvaluateDLSSG(nvrhi::ICommandList* cmd, const DlssFgInputs& inputs); +bool Streamline_TakeFgPresent(nvrhi::ITexture*& outInterp, nvrhi::ITexture*& outReal); + +} diff --git a/src/Layers/xrRender/Upscaling/UpscaleBackends.cpp b/src/Layers/xrRender/Upscaling/UpscaleBackends.cpp new file mode 100644 index 00000000000..3ba6ef14b33 --- /dev/null +++ b/src/Layers/xrRender/Upscaling/UpscaleBackends.cpp @@ -0,0 +1,187 @@ +#include "stdafx.h" +#include "IUpscaleBackend.h" +#include "StreamlineDLSS.h" +#include "Layers/xrRender/Backend/VulkanBackend.h" + +extern ENGINE_API int ps_r_upscale; +extern ENGINE_API int ps_r_fsr_fg; +extern ENGINE_API int ps_r_dlss; +extern ENGINE_API int ps_r_dlss_fg; +extern ENGINE_API int ps_r_dlss_rr; + +namespace xray::render::fg { +namespace { + +class NullUpscaleBackend final : public IUpscaleBackend +{ +public: + bool Init(nvrhi::IDevice*) override { return true; } + void Shutdown() override {} + bool IsAvailable() const override { return false; } + UpscaleBackendType GetType() const override { return UpscaleBackendType::None; } + bool Evaluate(nvrhi::ICommandList*, const UpscaleInputs&) override { return false; } +}; + +class FSRUpscaleBackend final : public IUpscaleBackend +{ + bool m_ready = false; +public: + bool Init(nvrhi::IDevice*) override + { +#if defined(XRAY_USE_FSR) + m_ready = true; + Msg("* [Upscale] FSR backend initialized"); +#else + m_ready = false; + Msg("* [Upscale] FSR SDK not compiled in (XRAY_USE_FSR)"); +#endif + return m_ready; + } + void Shutdown() override { m_ready = false; } + bool IsAvailable() const override { return m_ready; } + UpscaleBackendType GetType() const override { return UpscaleBackendType::FSR; } + bool SupportsFG() const override { return m_ready; } + bool SupportsRR() const override + { +#if defined(XRAY_USE_FSR) + return m_ready; +#else + return false; +#endif + } + bool Evaluate(nvrhi::ICommandList* cmd, const UpscaleInputs& inputs) override + { + if (!m_ready || !cmd || !inputs.color || !inputs.output) + return false; + (void)ps_r_fsr_fg; + if (inputs.renderWidth == inputs.displayWidth && + inputs.renderHeight == inputs.displayHeight && + inputs.color != inputs.output) + { + nvrhi::TextureSlice slice; + cmd->copyTexture(inputs.output, slice, inputs.color, slice); + return true; + } + return false; + } +}; + +class DLSSUpscaleBackend final : public IUpscaleBackend +{ + bool m_ready = false; +public: + bool Init(nvrhi::IDevice*) override + { +#if defined(XRAY_USE_DLSS) + auto* vk = dynamic_cast(GEnv.Backend); + if (!vk) + { + Msg("! [Upscale] DLSS requires Vulkan backend"); + return false; + } + if (Streamline_IsDLSSAvailable()) + { + m_ready = true; + } + else + { + StreamlineVulkanInfo info{}; + info.instance = vk->GetVkInstance(); + info.physicalDevice = vk->GetVkPhysicalDevice(); + info.device = vk->GetVkDevice(); + info.graphicsQueueFamily = vk->GetGraphicsQueueFamily(); + m_ready = Streamline_SetVulkanInfo(info); + } + if (m_ready) + Msg("* [Upscale] DLSS/NGX backend ready (fg=%d rr=%d)", + Streamline_IsFGAvailable() ? 1 : 0, + Streamline_IsRRAvailable() ? 1 : 0); + else + Msg("! [Upscale] DLSS/NGX unavailable"); +#else + m_ready = false; + Msg("* [Upscale] DLSS/NGX SDK not compiled in (XRAY_USE_DLSS)"); +#endif + return m_ready; + } + void Shutdown() override + { +#if defined(XRAY_USE_DLSS) + Streamline_Shutdown(); +#endif + m_ready = false; + } + bool IsAvailable() const override { return m_ready; } + UpscaleBackendType GetType() const override { return UpscaleBackendType::DLSS; } + bool SupportsFG() const override + { +#if defined(XRAY_USE_DLSS) + return m_ready && Streamline_IsFGAvailable(); +#else + return false; +#endif + } + bool SupportsRR() const override + { +#if defined(XRAY_USE_DLSS) + return m_ready && Streamline_IsRRAvailable(); +#else + return false; +#endif + } + bool Evaluate(nvrhi::ICommandList* cmd, const UpscaleInputs& inputs) override + { + if (!m_ready || !cmd || !inputs.color || !inputs.output) + return false; +#if defined(XRAY_USE_DLSS) + UpscaleInputs in = inputs; + in.enableFG = in.enableFG && ps_r_dlss_fg != 0 && Streamline_IsFGAvailable(); + in.enableRR = in.enableRR && ps_r_dlss_rr != 0 && Streamline_IsRRAvailable(); + if (Streamline_EvaluateDLSS(cmd, in)) + return true; +#endif + if (inputs.renderWidth == inputs.displayWidth && + inputs.renderHeight == inputs.displayHeight && + inputs.color != inputs.output) + { + nvrhi::TextureSlice slice; + cmd->copyTexture(inputs.output, slice, inputs.color, slice); + return true; + } + return false; + } +}; + +} + +IUpscaleBackend* CreateNullUpscaleBackend() { return new NullUpscaleBackend(); } +IUpscaleBackend* CreateFSRUpscaleBackend() { return new FSRUpscaleBackend(); } +IUpscaleBackend* CreateDLSSUpscaleBackend() { return new DLSSUpscaleBackend(); } + +IUpscaleBackend* CreateUpscaleBackendAuto() +{ + if (ps_r_dlss != 0 && ps_r_upscale == 0) + ps_r_upscale = 2; + + if (ps_r_upscale == 2) { + auto* dlss = CreateDLSSUpscaleBackend(); + if (dlss->Init(GEnv.Backend ? GEnv.Backend->GetDevice() : nullptr) && dlss->IsAvailable()) + return dlss; + delete dlss; + Msg("! [Upscale] DLSS unavailable, falling back to FSR"); + ps_r_upscale = 1; + } + if (ps_r_upscale == 1) { + auto* fsr = CreateFSRUpscaleBackend(); + if (fsr->Init(GEnv.Backend ? GEnv.Backend->GetDevice() : nullptr) && fsr->IsAvailable()) + return fsr; + delete fsr; + Msg("! [Upscale] FSR unavailable"); + ps_r_upscale = 0; + } + auto* nullBackend = CreateNullUpscaleBackend(); + nullBackend->Init(nullptr); + return nullBackend; +} + +} diff --git a/src/Layers/xrRender/Upscaling/UpscalePassSetup.cpp b/src/Layers/xrRender/Upscaling/UpscalePassSetup.cpp new file mode 100644 index 00000000000..b14240a79a7 --- /dev/null +++ b/src/Layers/xrRender/Upscaling/UpscalePassSetup.cpp @@ -0,0 +1,352 @@ +#include "stdafx.h" +#include "UpscalePassSetup.h" +#include "Layers/xrRender/FrameGraph/FrameGraph.h" +#include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" +#include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/RenderContext/RenderContext.h" +#include "Layers/xrRender/RenderContext/RenderDevice.h" + +extern ENGINE_API int ps_r_fsr_fg; +extern ENGINE_API int ps_r_dlss_fg; +extern ENGINE_API int ps_r_dlss_rr; +extern ENGINE_API int ps_r_dlss_auto_exposure; +extern ENGINE_API float ps_r_fsr_sharpness; +extern ENGINE_API int ps_r_upscale; + +namespace xray::render::fg::passes { +using namespace framegraph; + +namespace { + +struct alignas(16) DlssRrGuideCB +{ + float cameraPos[4]; + float screenSize[2]; + float pad[2]; +}; + +void EnsureDisplayColor(nvrhi::IDevice* nv, UpscalePassState& state, u32 w, u32 h) +{ + if (state.displayColor && state.displayW == w && state.displayH == h) + return; + nvrhi::TextureDesc td; + td.width = w; + td.height = h; + td.format = nvrhi::Format::RGBA16_FLOAT; + td.isUAV = true; + td.isShaderResource = true; + td.isRenderTarget = true; + td.initialState = nvrhi::ResourceStates::UnorderedAccess; + td.keepInitialState = true; + td.debugName = "rt_Upscale_DisplayColor"; + state.displayColor = nv->createTexture(td); + state.displayW = w; + state.displayH = h; +} + +void EnsureRRGuideResources(fg::RenderDevice* device, UpscalePassState& state, u32 w, u32 h) +{ + nvrhi::IDevice* nv = device->GetNVRHIDevice(); + if (!state.rrGuidePipeline) + { + auto* loader = RImplementation.GetShaderLoader(); + if (!loader) + return; + auto cs = loader->LoadComputeShader("dlss_rr_guides"); + if (!cs.handle || !cs.reflection) + return; + auto& cache = GetPassResourceCache(); + state.rrGuideLayout = cache.GetOrCreateBindingLayoutFromReflection("DlssRrGuides", *cs.reflection, nv); + nvrhi::ComputePipelineDesc pd; + pd.CS = cs.handle; + pd.bindingLayouts = { state.rrGuideLayout }; + state.rrGuidePipeline = cache.GetOrCreateComputePipeline("DlssRrGuides", pd, nv); + state.rrGuideCB = cache.GetOrCreateVolatileCB("Upscale", "DlssRrGuideCB", sizeof(DlssRrGuideCB), device); + } + + if (state.rrDiffuseAlbedo && state.rrGuideW == w && state.rrGuideH == h) + return; + + auto make = [&](const char* name, nvrhi::Format fmt) { + nvrhi::TextureDesc td; + td.width = w; + td.height = h; + td.format = fmt; + td.isUAV = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::UnorderedAccess; + td.keepInitialState = true; + td.debugName = name; + return nv->createTexture(td); + }; + state.rrDiffuseAlbedo = make("rt_DlssRr_DiffuseAlbedo", nvrhi::Format::RGBA16_FLOAT); + state.rrSpecularAlbedo = make("rt_DlssRr_SpecularAlbedo", nvrhi::Format::RGBA16_FLOAT); + state.rrSpecularHitDist = make("rt_DlssRr_SpecularHitDist", nvrhi::Format::R16_FLOAT); + state.rrGuideW = w; + state.rrGuideH = h; +} + +void CopyMatrix(float dst[16], const Fmatrix& m) +{ + dst[0] = m._11; dst[1] = m._12; dst[2] = m._13; dst[3] = m._14; + dst[4] = m._21; dst[5] = m._22; dst[6] = m._23; dst[7] = m._24; + dst[8] = m._31; dst[9] = m._32; dst[10] = m._33; dst[11] = m._34; + dst[12] = m._41; dst[13] = m._42; dst[14] = m._43; dst[15] = m._44; +} + +} + +framegraph::VirtualResourceHandle setupUpscaleOrResolvePass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle sceneColor, + VirtualResourceHandle depth, + VirtualResourceHandle motionVectors, + VirtualResourceHandle exposure, + const UpscaleState& upscaleState, + IUpscaleBackend* backend, + u32 renderW, + u32 renderH, + UpscalePassState& state, + const UpscaleRRGuides* rrGuides) +{ + if (!backend || !backend->IsAvailable()) + return sceneColor; + if (!upscaleState.upscaleActive && !NeedsResolveToDisplay(upscaleState)) + return sceneColor; + + nvrhi::IDevice* nv = device->GetNVRHIDevice(); + EnsureDisplayColor(nv, state, upscaleState.displayWidth, upscaleState.displayHeight); + + const bool wantRR = rrGuides && ps_r_upscale == 2 && ps_r_dlss_rr != 0 && backend->SupportsRR(); + if (wantRR) + EnsureRRGuideResources(device, state, renderW, renderH); + + ResourceDesc outDesc; + outDesc.type = ResourceDesc::Type::Texture2D; + outDesc.width = upscaleState.displayWidth; + outDesc.height = upscaleState.displayHeight; + outDesc.format = nvrhi::Format::RGBA16_FLOAT; + outDesc.isUAV = true; + outDesc.isRenderTarget = true; + outDesc.isImported = true; + outDesc.isTransient = false; + VirtualResourceHandle outHandle = fg.ImportTexture("rt_Upscale_DisplayColor", state.displayColor.Get(), outDesc); + + VirtualResourceHandle rrDiffHandle{}, rrSpecHandle{}, rrHitHandle{}, rrNoisySpecHandle{}; + if (wantRR && state.rrDiffuseAlbedo) + { + ResourceDesc gd; + gd.type = ResourceDesc::Type::Texture2D; + gd.width = renderW; + gd.height = renderH; + gd.format = nvrhi::Format::RGBA16_FLOAT; + gd.isUAV = true; + gd.isImported = true; + gd.isTransient = false; + rrDiffHandle = fg.ImportTexture("rt_DlssRr_DiffuseAlbedo", state.rrDiffuseAlbedo.Get(), gd); + rrSpecHandle = fg.ImportTexture("rt_DlssRr_SpecularAlbedo", state.rrSpecularAlbedo.Get(), gd); + gd.format = nvrhi::Format::R16_FLOAT; + gd.isUAV = true; + rrHitHandle = fg.ImportTexture("rt_DlssRr_SpecularHitDist", state.rrSpecularHitDist.Get(), gd); + if (rrGuides->noisySpecular) + { + ResourceDesc ns = gd; + ns.format = nvrhi::Format::RGBA16_FLOAT; + ns.isUAV = false; + rrNoisySpecHandle = fg.ImportTexture("rt_DlssRr_NoisySpecularIn", rrGuides->noisySpecular, ns); + } + } + + if (wantRR && state.rrGuidePipeline && rrDiffHandle.is_valid() && rrNoisySpecHandle.is_valid()) + { + struct PackData + { + VirtualResourceHandle baseColor, normals, worldPos, depth, noisySpec; + VirtualResourceHandle outDiff, outSpec, outHit; + UpscalePassState* state = nullptr; + u32 w = 0, h = 0; + }; + fg.addCallbackPass( + "DLSS-RR Guides", + [&](FrameGraph& builder, PassHandle passHandle, PackData& data) { + RenderPassBuilder pb(builder, passHandle); + data.baseColor = pb.read(rrGuides->baseColor, ResourceState::ShaderResource); + data.normals = pb.read(rrGuides->normals, ResourceState::ShaderResource); + data.worldPos = pb.read(rrGuides->worldPos, ResourceState::ShaderResource); + data.depth = pb.read(rrGuides->depth.is_valid() ? rrGuides->depth : depth, ResourceState::ShaderResource); + data.noisySpec = pb.read(rrNoisySpecHandle, ResourceState::ShaderResource); + data.outDiff = pb.write(rrDiffHandle, ResourceState::UnorderedAccess); + data.outSpec = pb.write(rrSpecHandle, ResourceState::UnorderedAccess); + data.outHit = pb.write(rrHitHandle, ResourceState::UnorderedAccess); + pb.sideEffects(); + data.state = &state; + data.w = renderW; + data.h = renderH; + }, + [](const PackData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + if (!data.state || !data.state->rrGuidePipeline || !ctx) + return; + auto* base = fgGraph.GetPhysicalTexture(data.baseColor); + auto* nrm = fgGraph.GetPhysicalTexture(data.normals); + auto* wpos = fgGraph.GetPhysicalTexture(data.worldPos); + auto* depthTex = fgGraph.GetPhysicalTexture(data.depth); + auto* noisySpec = fgGraph.GetPhysicalTexture(data.noisySpec); + auto* outD = data.state->rrDiffuseAlbedo.Get(); + auto* outS = data.state->rrSpecularAlbedo.Get(); + auto* outH = data.state->rrSpecularHitDist.Get(); + if (!base || !nrm || !wpos || !depthTex || !noisySpec || !outD || !outS || !outH) + return; + + auto* cmd = ctx->GetCommandList(); + auto* nvDev = cmd->getDevice(); + auto& cache = GetPassResourceCache(); + auto* refl = RImplementation.GetShaderLoader()->GetCachedReflection("dlss_rr_guides", ".cs"); + if (!refl || !data.state->rrGuideCB) + return; + + DlssRrGuideCB cb{}; + cb.cameraPos[0] = Device.vCameraPosition.x; + cb.cameraPos[1] = Device.vCameraPosition.y; + cb.cameraPos[2] = Device.vCameraPosition.z; + cb.cameraPos[3] = 1.f; + cb.screenSize[0] = (float)data.w; + cb.screenSize[1] = (float)data.h; + cmd->writeBuffer(data.state->rrGuideCB, &cb, sizeof(cb)); + + BindingSetBuilder bsb(*refl, nvDev, "DlssRrGuides"); + bsb.ConstantBuffer("DlssRrGuideParams", data.state->rrGuideCB) + .Texture("g_BaseColor", base) + .Texture("g_Normal", nrm) + .Texture("g_WorldPos", wpos) + .Texture("g_NoisySpecular", noisySpec) + .Texture("g_Depth", depthTex, nvrhi::Format::R32_FLOAT) + .TextureUAV("u_DiffuseAlbedo", outD) + .TextureUAV("u_SpecularAlbedo", outS) + .TextureUAV("u_SpecularHitDist", outH); + auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.state->rrGuideLayout, nvDev); + nvrhi::ComputeState cs; + cs.pipeline = data.state->rrGuidePipeline; + cs.bindings = { set }; + cmd->setComputeState(cs); + cmd->dispatch((data.w + 7) / 8, (data.h + 7) / 8, 1); + }); + } + + struct PassData { + VirtualResourceHandle src; + VirtualResourceHandle dst; + VirtualResourceHandle depth; + VirtualResourceHandle motion; + VirtualResourceHandle exposure; + VirtualResourceHandle normals; + VirtualResourceHandle diffuseAlbedo; + VirtualResourceHandle specularAlbedo; + VirtualResourceHandle specularHit; + IUpscaleBackend* backend; + UpscalePassState* state; + UpscaleState upscale; + u32 renderW, renderH; + nvrhi::ITexture* noisyDiffuse = nullptr; + nvrhi::ITexture* noisySpecular = nullptr; + nvrhi::ITexture* hitDistance = nullptr; + bool wantRR = false; + }; + + auto& pass = fg.addCallbackPass( + "Upscale / Resolve", + [&](FrameGraph& builder, PassHandle passHandle, PassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.src = pb.read(sceneColor, ResourceState::ShaderResource); + if (depth.is_valid()) + data.depth = pb.read(depth, ResourceState::ShaderResource); + if (motionVectors.is_valid()) + data.motion = pb.read(motionVectors, ResourceState::ShaderResource); + if (exposure.is_valid()) + data.exposure = pb.read(exposure, ResourceState::ShaderResource); + if (wantRR && rrGuides && rrDiffHandle.is_valid() && rrSpecHandle.is_valid() && + rrHitHandle.is_valid() && rrNoisySpecHandle.is_valid() && state.rrGuidePipeline) + { + if (rrGuides->normals.is_valid()) + data.normals = pb.read(rrGuides->normals, ResourceState::ShaderResource); + data.diffuseAlbedo = pb.read(rrDiffHandle, ResourceState::ShaderResource); + data.specularAlbedo = pb.read(rrSpecHandle, ResourceState::ShaderResource); + data.specularHit = pb.read(rrHitHandle, ResourceState::ShaderResource); + data.noisyDiffuse = rrGuides->noisyDiffuse; + data.noisySpecular = rrGuides->noisySpecular; + data.hitDistance = rrGuides->hitDistance; + data.wantRR = true; + } + data.dst = pb.write(outHandle, ResourceState::UnorderedAccess); + pb.sideEffects(); + data.backend = backend; + data.state = &state; + data.upscale = upscaleState; + data.renderW = renderW; + data.renderH = renderH; + }, + [](const PassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + auto* src = fgGraph.GetPhysicalTexture(data.src); + auto* dst = data.state ? data.state->displayColor.Get() : nullptr; + if (!src || !dst || !ctx) + return; + + nvrhi::ICommandList* cmd = ctx->GetCommandList(); + UpscaleInputs in; + in.color = src; + in.depth = data.depth.is_valid() ? fgGraph.GetPhysicalTexture(data.depth) : nullptr; + in.motionVectors = data.motion.is_valid() ? fgGraph.GetPhysicalTexture(data.motion) : nullptr; + in.exposure = data.exposure.is_valid() ? fgGraph.GetPhysicalTexture(data.exposure) : nullptr; + in.normals = data.normals.is_valid() ? fgGraph.GetPhysicalTexture(data.normals) : nullptr; + in.diffuseAlbedo = data.diffuseAlbedo.is_valid() ? fgGraph.GetPhysicalTexture(data.diffuseAlbedo) : nullptr; + in.specularAlbedo = data.specularAlbedo.is_valid() ? fgGraph.GetPhysicalTexture(data.specularAlbedo) : nullptr; + in.specularHitDistance = data.specularHit.is_valid() ? fgGraph.GetPhysicalTexture(data.specularHit) : nullptr; + in.diffuseHitDistance = data.hitDistance; + in.roughness = in.normals; + in.noisyDiffuse = data.noisyDiffuse; + in.noisySpecular = data.noisySpecular; + in.output = dst; + in.renderWidth = data.renderW; + in.renderHeight = data.renderH; + in.displayWidth = data.upscale.displayWidth; + in.displayHeight = data.upscale.displayHeight; + in.jitterX = data.upscale.jitterX; + in.jitterY = data.upscale.jitterY; + in.sharpness = ps_r_fsr_sharpness; + in.frameIndex = Device.dwFrame; + in.frameTimeMs = Device.fTimeDelta > 1e-5f ? Device.fTimeDelta * 1000.f : 16.f; + in.reset = data.upscale.resetHistory; + in.enableFG = (ps_r_upscale == 1 && ps_r_fsr_fg) || (ps_r_upscale == 2 && ps_r_dlss_fg); + in.enableRR = data.wantRR && (ps_r_upscale == 2 && ps_r_dlss_rr); + if (ps_r_upscale == 2 && ps_r_dlss_auto_exposure) + in.exposure = nullptr; + CopyMatrix(in.worldToView, Device.mView); + CopyMatrix(in.viewToClip, Device.mProject); + + bool ok = false; + if (data.backend && data.backend->IsAvailable()) + ok = data.backend->Evaluate(cmd, in); + + if (!ok) { + static bool s_failLogged = false; + if (!s_failLogged) + { + s_failLogged = true; + Msg("! [Upscale] Evaluate failed — render %ux%u → display %ux%u", + data.renderW, data.renderH, data.upscale.displayWidth, data.upscale.displayHeight); + } + if (data.renderW == data.upscale.displayWidth && + data.renderH == data.upscale.displayHeight) + { + nvrhi::TextureSlice slice; + cmd->copyTexture(dst, slice, src, slice); + } + } + }); + + return pass.dst; +} + +} diff --git a/src/Layers/xrRender/Upscaling/UpscalePassSetup.h b/src/Layers/xrRender/Upscaling/UpscalePassSetup.h new file mode 100644 index 00000000000..a9d4f42e3ce --- /dev/null +++ b/src/Layers/xrRender/Upscaling/UpscalePassSetup.h @@ -0,0 +1,54 @@ +#pragma once + +#include "Layers/xrRender/FrameGraph/FGTypes.h" +#include "Layers/xrRender/FrameGraph/FGResource.h" +#include "UpscaleState.h" +#include "IUpscaleBackend.h" +#include + +namespace xray::render::framegraph { class FrameGraph; } +namespace xray::render::fg { class RenderDevice; } + +namespace xray::render::fg::passes { + +struct UpscalePassState +{ + nvrhi::TextureHandle displayColor; + nvrhi::TextureHandle rrDiffuseAlbedo; + nvrhi::TextureHandle rrSpecularAlbedo; + nvrhi::TextureHandle rrSpecularHitDist; + nvrhi::ComputePipelineHandle rrGuidePipeline; + nvrhi::BindingLayoutHandle rrGuideLayout; + nvrhi::IBuffer* rrGuideCB = nullptr; + u32 displayW = 0; + u32 displayH = 0; + u32 rrGuideW = 0; + u32 rrGuideH = 0; +}; + +struct UpscaleRRGuides +{ + framegraph::VirtualResourceHandle normals; + framegraph::VirtualResourceHandle baseColor; + framegraph::VirtualResourceHandle worldPos; + framegraph::VirtualResourceHandle depth; + nvrhi::ITexture* noisyDiffuse = nullptr; + nvrhi::ITexture* noisySpecular = nullptr; + nvrhi::ITexture* hitDistance = nullptr; +}; + +framegraph::VirtualResourceHandle setupUpscaleOrResolvePass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle motionVectors, + framegraph::VirtualResourceHandle exposure, + const UpscaleState& upscaleState, + IUpscaleBackend* backend, + u32 renderW, + u32 renderH, + UpscalePassState& state, + const UpscaleRRGuides* rrGuides = nullptr); + +} diff --git a/src/Layers/xrRender/Upscaling/UpscaleState.cpp b/src/Layers/xrRender/Upscaling/UpscaleState.cpp new file mode 100644 index 00000000000..ff4b5ffd01d --- /dev/null +++ b/src/Layers/xrRender/Upscaling/UpscaleState.cpp @@ -0,0 +1,56 @@ +#include "stdafx.h" +#include "UpscaleState.h" +#include +#include + +extern ENGINE_API float ps_r_render_scale; +extern ENGINE_API int ps_r_upscale; +extern ENGINE_API int ps_r_dlss; +extern ENGINE_API int ps_r_dlss_quality; + +namespace xray::render::fg { +namespace { + +float DlssQualityScale(int quality) +{ + switch (quality) + { + case 0: return 0.33f; + case 1: return 0.50f; + case 2: return 0.58f; + case 3: return 0.67f; + case 5: return 1.00f; + default: return 0.67f; + } +} + +} + +void UpdateUpscaleState(UpscaleState& state, u32 displayW, u32 displayH) +{ + if (ps_r_dlss != 0 && ps_r_upscale == 0) + ps_r_upscale = 2; + + state.displayWidth = std::max(1u, displayW); + state.displayHeight = std::max(1u, displayH); + + float scale = ps_r_render_scale; + if (scale <= 0.f) + scale = 1.0f; + scale = std::clamp(scale, 0.25f, 1.0f); + + if (ps_r_upscale == 0) + scale = 1.0f; + else if (ps_r_upscale == 2 && scale >= 0.999f && ps_r_dlss_quality != 5) + scale = DlssQualityScale(ps_r_dlss_quality); + + state.renderScale = scale; + state.renderWidth = std::max(1u, (u32)std::lround(float(state.displayWidth) * scale)); + state.renderHeight = std::max(1u, (u32)std::lround(float(state.displayHeight) * scale)); + state.upscaleActive = ps_r_upscale != 0 && + (state.renderWidth != state.displayWidth || + state.renderHeight != state.displayHeight || + ps_r_upscale == 2); +} + +} diff --git a/src/Layers/xrRender/Upscaling/UpscaleState.h b/src/Layers/xrRender/Upscaling/UpscaleState.h new file mode 100644 index 00000000000..9e43e6730b2 --- /dev/null +++ b/src/Layers/xrRender/Upscaling/UpscaleState.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace xray::render::fg { + +struct UpscaleState +{ + u32 displayWidth = 0; + u32 displayHeight = 0; + u32 renderWidth = 0; + u32 renderHeight = 0; + float renderScale = 1.0f; + float jitterX = 0.f; + float jitterY = 0.f; + float prevJitterX = 0.f; + float prevJitterY = 0.f; + bool upscaleActive = false; + bool resetHistory = false; +}; + +void UpdateUpscaleState(UpscaleState& state, u32 displayW, u32 displayH); + +inline bool NeedsResolveToDisplay(const UpscaleState& state) +{ + return state.renderWidth != state.displayWidth || state.renderHeight != state.displayHeight; +} + +} diff --git a/src/Layers/xrRender/Volumetrics/LightShaftSource.cpp b/src/Layers/xrRender/Volumetrics/LightShaftSource.cpp new file mode 100644 index 00000000000..b38a16eb342 --- /dev/null +++ b/src/Layers/xrRender/Volumetrics/LightShaftSource.cpp @@ -0,0 +1,20 @@ +#include "stdafx.h" +#include "LightShaftSource.h" +#include "Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h" +#include "xrEngine/device.h" + +namespace xray::render::fg +{ + +void LightShaftSource::PrepareGPUData() +{ + m_intensity = passes::ResolveSunShaftsIntensity(); +} + +void LightShaftSource::GetWorldBounds(Fvector& outMin, Fvector& outMax) const +{ + outMin.set(-FLT_MAX, -FLT_MAX, -FLT_MAX); + outMax.set(FLT_MAX, FLT_MAX, FLT_MAX); +} + +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/Volumetrics/LightShaftSource.h b/src/Layers/xrRender/Volumetrics/LightShaftSource.h new file mode 100644 index 00000000000..f642c805d0a --- /dev/null +++ b/src/Layers/xrRender/Volumetrics/LightShaftSource.h @@ -0,0 +1,22 @@ +#pragma once + +#include "IVolumetricSource.h" + +namespace xray::render::fg +{ + +class LightShaftSource : public IVolumetricSource +{ +public: + void PrepareGPUData() override; + const char* GetShaderName() const override { return "volumetric\\light_shafts"; } + void GetWorldBounds(Fvector& outMin, Fvector& outMax) const override; + const char* GetTypeName() const override { return "LightShaft"; } + + float GetIntensity() const { return m_intensity; } + +private: + float m_intensity = 0.f; +}; + +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/Volumetrics/ParticleEmitterSource.cpp b/src/Layers/xrRender/Volumetrics/ParticleEmitterSource.cpp new file mode 100644 index 00000000000..a03ec06749d --- /dev/null +++ b/src/Layers/xrRender/Volumetrics/ParticleEmitterSource.cpp @@ -0,0 +1,15 @@ +#include "stdafx.h" +#include "ParticleEmitterSource.h" + +namespace xray::render::fg +{ + +void ParticleEmitterSource::PrepareGPUData() {} + +void ParticleEmitterSource::GetWorldBounds(Fvector& outMin, Fvector& outMax) const +{ + outMin.set(-FLT_MAX, -FLT_MAX, -FLT_MAX); + outMax.set(FLT_MAX, FLT_MAX, FLT_MAX); +} + +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/Volumetrics/ParticleEmitterSource.h b/src/Layers/xrRender/Volumetrics/ParticleEmitterSource.h new file mode 100644 index 00000000000..da1154df476 --- /dev/null +++ b/src/Layers/xrRender/Volumetrics/ParticleEmitterSource.h @@ -0,0 +1,17 @@ +#pragma once + +#include "IVolumetricSource.h" + +namespace xray::render::fg +{ + +class ParticleEmitterSource : public IVolumetricSource +{ +public: + void PrepareGPUData() override; + const char* GetShaderName() const override { return "volumetric\\particle_inject"; } + void GetWorldBounds(Fvector& outMin, Fvector& outMax) const override; + const char* GetTypeName() const override { return "ParticleEmitter"; } +}; + +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/Volumetrics/VolumetricRenderer.cpp b/src/Layers/xrRender/Volumetrics/VolumetricRenderer.cpp index 572fc77fced..643fe78fd17 100644 --- a/src/Layers/xrRender/Volumetrics/VolumetricRenderer.cpp +++ b/src/Layers/xrRender/Volumetrics/VolumetricRenderer.cpp @@ -45,9 +45,6 @@ void VolumetricRenderer::Initialize(RenderDevice* device) return; } - ClearSources(); - RegisterSource(&m_worldFog); - m_initialized = true; m_hasHistory = false; Msg("* [VolumetricRenderer] Initialized froxel volume %ux%ux%u (+temporal)", @@ -83,6 +80,13 @@ void VolumetricRenderer::ClearSources() void VolumetricRenderer::BeginFrame() { + ClearSources(); + RegisterSource(&m_worldFog); + m_lightShaft.PrepareGPUData(); + if (m_lightShaft.GetIntensity() > 1e-4f) + RegisterSource(&m_lightShaft); + RegisterSource(&m_particleEmitter); + for (auto* s : m_sources) { if (s) @@ -92,8 +96,6 @@ void VolumetricRenderer::BeginFrame() void VolumetricRenderer::SwapFroxelHistory() { - // Mark that a complete lit volume exists for next-frame temporal. - // Actual copy is done in VolumetricPass execute (GPU copyTexture). m_hasHistory = (m_froxelVolume != nullptr && m_prevFroxelVolume != nullptr); } diff --git a/src/Layers/xrRender/Volumetrics/VolumetricRenderer.h b/src/Layers/xrRender/Volumetrics/VolumetricRenderer.h index ccc4806c70f..a95431923c6 100644 --- a/src/Layers/xrRender/Volumetrics/VolumetricRenderer.h +++ b/src/Layers/xrRender/Volumetrics/VolumetricRenderer.h @@ -2,6 +2,8 @@ #include "IVolumetricSource.h" #include "WorldFogSource.h" +#include "LightShaftSource.h" +#include "ParticleEmitterSource.h" #include "Layers/xrRender/FrameGraph/FGTypes.h" #include @@ -17,8 +19,8 @@ class RenderDevice; class VolumetricRenderer { public: - static constexpr u32 kFroxelX = 160; - static constexpr u32 kFroxelY = 90; + static constexpr u32 kFroxelX = 128; + static constexpr u32 kFroxelY = 72; static constexpr u32 kFroxelZ = 16; void Initialize(RenderDevice* device); @@ -26,11 +28,14 @@ class VolumetricRenderer void RegisterSource(IVolumetricSource* source); void ClearSources(); - void BeginFrame(); // refresh WorldFog from Environment - void SwapFroxelHistory(); // after frame: current → prev + void BeginFrame(); + void SwapFroxelHistory(); WorldFogSource& GetWorldFog() { return m_worldFog; } const WorldFogSource& GetWorldFog() const { return m_worldFog; } + LightShaftSource& GetLightShaft() { return m_lightShaft; } + ParticleEmitterSource& GetParticleEmitter() { return m_particleEmitter; } + const xr_vector& GetSources() const { return m_sources; } nvrhi::ITexture* GetFroxelVolume() const { return m_froxelVolume.Get(); } nvrhi::ITexture* GetPrevFroxelVolume() const { return m_prevFroxelVolume.Get(); } bool HasFroxelHistory() const { return m_hasHistory && m_prevFroxelVolume; } @@ -45,6 +50,8 @@ class VolumetricRenderer nvrhi::TextureHandle m_froxelVolume; nvrhi::TextureHandle m_prevFroxelVolume; WorldFogSource m_worldFog; + LightShaftSource m_lightShaft; + ParticleEmitterSource m_particleEmitter; xr_vector m_sources; }; diff --git a/src/Layers/xrRender/blenders/blender_deffer_flat.h b/src/Layers/xrRender/blenders/blender_deffer_flat.h index c31e5e5a587..7451babfc58 100644 --- a/src/Layers/xrRender/blenders/blender_deffer_flat.h +++ b/src/Layers/xrRender/blenders/blender_deffer_flat.h @@ -16,6 +16,8 @@ class CBlender_deffer_flat : public IBlender CBlender_deffer_flat(); virtual ~CBlender_deffer_flat(); + u32 GetTessellation() const { return oTessellation.IDselected; } + private: xrP_TOKEN oTessellation; }; diff --git a/src/Layers/xrRender/blenders/blender_deffer_model.h b/src/Layers/xrRender/blenders/blender_deffer_model.h index 884d119cc81..da2c8253b43 100644 --- a/src/Layers/xrRender/blenders/blender_deffer_model.h +++ b/src/Layers/xrRender/blenders/blender_deffer_model.h @@ -19,6 +19,8 @@ class CBlender_deffer_model : public IBlender CBlender_deffer_model(); virtual ~CBlender_deffer_model(); + u32 GetTessellation() const { return oTessellation.IDselected; } + private: xrP_TOKEN oTessellation; }; diff --git a/src/Layers/xrRender/fgEnvironmentRender.cpp b/src/Layers/xrRender/fgEnvironmentRender.cpp index 0310b3f0bc1..6cdbf665dfc 100644 --- a/src/Layers/xrRender/fgEnvironmentRender.cpp +++ b/src/Layers/xrRender/fgEnvironmentRender.cpp @@ -164,6 +164,18 @@ void FGEnvironmentRender::OnDeviceDestroy() m_skyPipeline = nullptr; m_skyInitialized = false; + m_cloudsVertexBuffer = nullptr; + m_cloudsIndexBuffer = nullptr; + m_cloudsPlaceholderTex = nullptr; + m_cloudsVS = nullptr; + m_cloudsPS = nullptr; + m_cloudsInputLayout = nullptr; + m_cloudsBindingLayout = nullptr; + m_cloudsPipeline = nullptr; + m_cloudsIndexCount = 0; + m_cloudsVertexCapacity = 0; + m_cloudsInitialized = false; + m_sunVertexBuffer = nullptr; m_sunIndexBuffer = nullptr; m_sunPlaceholderTex = nullptr; @@ -238,12 +250,7 @@ void FGEnvironmentRender::InitSkyResources() auto vsResult = shaderLoader->LoadVertexShader("sky_forward"); auto psResult = shaderLoader->LoadPixelShader("sky_forward"); - if (!vsResult.handle || !psResult.handle) - { - vsResult = shaderLoader->LoadVertexShader("sky2"); - psResult = shaderLoader->LoadPixelShader("sky2"); - } - R_ASSERT2(vsResult.handle && psResult.handle, "FGEnv: failed to load sky shaders"); + R_ASSERT2(vsResult.handle && psResult.handle, "FGEnv: failed to load sky_forward shaders"); m_skyVS = vsResult.handle; m_skyPS = psResult.handle; @@ -393,6 +400,229 @@ void FGEnvironmentRender::DrawSky(nvrhi::ICommandList* cmdList, nvrhi::IFramebuf cmdList->drawIndexed(nvrhi::DrawArguments{60, 1, 0, 0, 0}); } +void FGEnvironmentRender::InitCloudResources() +{ + if (m_cloudsInitialized) + return; + + auto* fgRenderer = static_cast(GEnv.Render); + auto* renderDevice = fgRenderer->GetRenderDevice(); + m_device = renderDevice->GetNVRHIDevice(); + R_ASSERT(m_device); + + nvrhi::TextureDesc texDesc; + texDesc.width = 1; + texDesc.height = 1; + texDesc.format = nvrhi::Format::RGBA8_UNORM; + texDesc.debugName = "FGEnv_CloudsPlaceholder"; + texDesc.initialState = nvrhi::ResourceStates::ShaderResource; + texDesc.keepInitialState = true; + m_cloudsPlaceholderTex = m_device->createTexture(texDesc); + + { + nvrhi::CommandListHandle uploadCmd = m_device->createCommandList(); + uploadCmd->open(); + u32 white = 0xFFFFFFFF; + uploadCmd->writeTexture(m_cloudsPlaceholderTex, 0, 0, &white, sizeof(white)); + uploadCmd->close(); + m_device->executeCommandList(uploadCmd); + } + + auto* shaderLoader = RImplementation.GetShaderLoader(); + R_ASSERT(shaderLoader); + auto vsResult = shaderLoader->LoadVertexShader("clouds_forward"); + auto psResult = shaderLoader->LoadPixelShader("clouds_forward"); + if (!vsResult.handle || !psResult.handle) + { + Msg("! [FGEnv] clouds_forward shaders missing — clouds disabled"); + return; + } + m_cloudsVS = vsResult.handle; + m_cloudsPS = psResult.handle; + + auto& cache = framegraph::GetPassResourceCache(); + m_cloudsBindingLayout = cache.GetOrCreateBindingLayoutFromReflection( + "FGEnv_Clouds", *vsResult.reflection, *psResult.reflection, m_device); + R_ASSERT(m_cloudsBindingLayout); + + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc() + .setName("POSITION") + .setFormat(nvrhi::Format::RGB32_FLOAT) + .setOffset(offsetof(passes::CloudVertex, position)) + .setElementStride(sizeof(passes::CloudVertex)), + nvrhi::VertexAttributeDesc() + .setName("COLOR") + .setFormat(nvrhi::Format::RGBA8_UNORM) + .setArraySize(2) + .setOffset(offsetof(passes::CloudVertex, dir)) + .setElementStride(sizeof(passes::CloudVertex)), + }; + m_cloudsInputLayout = cache.GetOrCreateInputLayout( + "FGEnv_Clouds", attribs, std::size(attribs), m_cloudsVS, m_device); + + nvrhi::RenderState renderState; + renderState.blendState.targets[0].setBlendEnable(true); + renderState.blendState.targets[0].setSrcBlend(nvrhi::BlendFactor::SrcAlpha); + renderState.blendState.targets[0].setDestBlend(nvrhi::BlendFactor::OneMinusSrcAlpha); + renderState.blendState.targets[0].setBlendOp(nvrhi::BlendOp::Add); + renderState.depthStencilState.setDepthTestEnable(false); + renderState.depthStencilState.setDepthWriteEnable(false); + renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); + + nvrhi::GraphicsPipelineDesc pipelineDesc; + pipelineDesc.setVertexShader(m_cloudsVS); + pipelineDesc.setPixelShader(m_cloudsPS); + pipelineDesc.addBindingLayout(m_cloudsBindingLayout); + pipelineDesc.setInputLayout(m_cloudsInputLayout); + pipelineDesc.setRenderState(renderState); + pipelineDesc.setPrimType(nvrhi::PrimitiveType::TriangleList); + + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + + m_cloudsPipeline = cache.GetOrCreatePipeline("FGEnv_Clouds", pipelineDesc, fbInfo, m_device); + R_ASSERT(m_cloudsPipeline); + + m_cloudsInitialized = true; +} + +void FGEnvironmentRender::DrawClouds(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, CEnvironment* environment, u32 width, u32 height) +{ + if (!environment || !cmdList || !framebuffer) + return; + if (environment->CloudsVerts.empty() || environment->CloudsIndices.empty()) + return; + + InitCloudResources(); + if (!m_cloudsInitialized) + return; + + CEnvDescriptor& env = environment->CurrentEnv; + if (env.clouds_color.w <= 0.001f) + return; + + const u32 vertCount = static_cast(environment->CloudsVerts.size()); + const u32 indexCount = static_cast(environment->CloudsIndices.size()); + + if (!m_cloudsVertexBuffer || m_cloudsVertexCapacity < vertCount) + { + nvrhi::BufferDesc vbDesc; + vbDesc.byteSize = vertCount * sizeof(passes::CloudVertex); + vbDesc.debugName = "FGEnv_CloudsVB"; + vbDesc.isVertexBuffer = true; + vbDesc.initialState = nvrhi::ResourceStates::VertexBuffer; + vbDesc.keepInitialState = true; + m_cloudsVertexBuffer = m_device->createBuffer(vbDesc); + m_cloudsVertexCapacity = vertCount; + } + + if (!m_cloudsIndexBuffer || m_cloudsIndexCount != indexCount) + { + nvrhi::BufferDesc ibDesc; + ibDesc.byteSize = indexCount * sizeof(u16); + ibDesc.debugName = "FGEnv_CloudsIB"; + ibDesc.isIndexBuffer = true; + ibDesc.initialState = nvrhi::ResourceStates::IndexBuffer; + ibDesc.keepInitialState = true; + m_cloudsIndexBuffer = m_device->createBuffer(ibDesc); + cmdList->writeBuffer(m_cloudsIndexBuffer, environment->CloudsIndices.data(), + indexCount * sizeof(u16)); + m_cloudsIndexCount = indexCount; + } + + Fmatrix mScale, mXFORM; + mScale.scale(10.f, 0.4f, 10.f); + mXFORM.rotateY(env.clouds_rotation); + mXFORM.mulB_43(mScale); + mXFORM.translate_over(Device.vCameraPosition); + + Fvector wd0, wd1; + Fvector4 wind_dir; + wd0.setHP(PI_DIV_4, 0); + wd1.setHP(PI_DIV_4 + PI_DIV_8, 0); + wind_dir.set(wd0.x, wd0.z, wd1.x, wd1.z).mul(0.5f).add(0.5f).mul(255.f); + const u32 C0 = color_rgba( + iFloor(wind_dir.x), iFloor(wind_dir.y), iFloor(wind_dir.w), iFloor(wind_dir.z)); + const u32 C1 = color_rgba( + iFloor(env.clouds_color.x * 255.f), + iFloor(env.clouds_color.y * 255.f), + iFloor(env.clouds_color.z * 255.f), + iFloor(env.clouds_color.w * 255.f)); + + xr_vector vertices(vertCount); + for (u32 i = 0; i < vertCount; ++i) + { + vertices[i].position = environment->CloudsVerts[i]; + vertices[i].dir = C0; + vertices[i].color = C1; + } + cmdList->writeBuffer(m_cloudsVertexBuffer, vertices.data(), vertCount * sizeof(passes::CloudVertex)); + + auto& cache = framegraph::GetPassResourceCache(); + auto* fgRenderer = static_cast(GEnv.Render); + auto* renderDevice = fgRenderer->GetRenderDevice(); + + passes::DynamicTransforms dynamicCB = {}; + passes::FillDynamicTransforms(dynamicCB, mXFORM); + auto dynamicCBBuffer = cache.GetOrCreateVolatileCB( + "FGEnv_Clouds", "DynamicCB", sizeof(passes::DynamicTransforms), renderDevice); + cmdList->writeBuffer(dynamicCBBuffer, &dynamicCB, sizeof(dynamicCB)); + + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(passes::StaticGlobals), renderDevice); + { + passes::StaticGlobals sg = passes::BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + + nvrhi::ITexture* clouds0Tex = nullptr; + nvrhi::ITexture* clouds1Tex = nullptr; + auto* texManager = renderDevice->GetFGResourceManager() + ? renderDevice->GetFGResourceManager()->GetTextureManager() : nullptr; + if (texManager && environment->Current[0] && environment->Current[1]) + { + const shared_str& name0 = environment->Current[0]->clouds_texture_name; + const shared_str& name1 = environment->Current[1]->clouds_texture_name; + if (name0.size()) + clouds0Tex = texManager->GetNVRHITexture(texManager->LoadTexture(name0.c_str())); + if (name1.size()) + clouds1Tex = texManager->GetNVRHITexture(texManager->LoadTexture(name1.c_str())); + } + if (!clouds0Tex) clouds0Tex = m_cloudsPlaceholderTex.Get(); + if (!clouds1Tex) clouds1Tex = m_cloudsPlaceholderTex.Get(); + + auto* vsRefl = RImplementation.GetShaderLoader()->GetCachedReflection("clouds_forward", ".vs"); + auto* psRefl = RImplementation.GetShaderLoader()->GetCachedReflection("clouds_forward", ".ps"); + if (!vsRefl || !psRefl) + return; + + framegraph::BindingSetBuilder bsb(*vsRefl, *psRefl, m_device, "FGEnv_Clouds"); + bsb.ConstantBuffer("dynamic_transforms", dynamicCBBuffer); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.Texture("s_clouds0", clouds0Tex); + bsb.Texture("s_clouds1", clouds1Tex); + + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), m_cloudsBindingLayout, m_device); + + nvrhi::Viewport viewport; + viewport.minX = 0; viewport.minY = 0; + viewport.maxX = static_cast(width); + viewport.maxY = static_cast(height); + viewport.minZ = 0.0f; viewport.maxZ = 1.0f; + + nvrhi::GraphicsState state; + state.pipeline = m_cloudsPipeline; + state.framebuffer = framebuffer; + state.viewport.addViewportAndScissorRect(viewport); + state.addBindingSet(bindingSet); + state.vertexBuffers = {{m_cloudsVertexBuffer, 0, 0}}; + state.indexBuffer = {m_cloudsIndexBuffer, nvrhi::Format::R16_UINT, 0}; + + cmdList->setGraphicsState(state); + cmdList->drawIndexed(nvrhi::DrawArguments{indexCount, 1, 0, 0, 0}); +} + void FGEnvironmentRender::InitSunResources() { if (m_sunInitialized) diff --git a/src/Layers/xrRender/fgEnvironmentRender.h b/src/Layers/xrRender/fgEnvironmentRender.h index 30f6f2af033..2f81859c3a9 100644 --- a/src/Layers/xrRender/fgEnvironmentRender.h +++ b/src/Layers/xrRender/fgEnvironmentRender.h @@ -43,10 +43,12 @@ class FGEnvironmentRender : public IEnvironmentRender const particles_systems::library_interface& particles_systems_library() override; void DrawSky(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, CEnvironment* environment, u32 width, u32 height); + void DrawClouds(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, CEnvironment* environment, u32 width, u32 height); void DrawSun(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, CEnvironment* environment, u32 width, u32 height); private: void InitSkyResources(); + void InitCloudResources(); void InitSunResources(); using RuntimeTextureList = xr_vector>; @@ -77,6 +79,18 @@ class FGEnvironmentRender : public IEnvironmentRender nvrhi::GraphicsPipelineHandle m_skyPipeline; bool m_skyInitialized = false; + nvrhi::BufferHandle m_cloudsVertexBuffer; + nvrhi::BufferHandle m_cloudsIndexBuffer; + nvrhi::TextureHandle m_cloudsPlaceholderTex; + nvrhi::ShaderHandle m_cloudsVS; + nvrhi::ShaderHandle m_cloudsPS; + nvrhi::InputLayoutHandle m_cloudsInputLayout; + nvrhi::BindingLayoutHandle m_cloudsBindingLayout; + nvrhi::GraphicsPipelineHandle m_cloudsPipeline; + u32 m_cloudsIndexCount = 0; + u32 m_cloudsVertexCapacity = 0; + bool m_cloudsInitialized = false; + nvrhi::BufferHandle m_sunVertexBuffer; nvrhi::BufferHandle m_sunIndexBuffer; nvrhi::TextureHandle m_sunPlaceholderTex; diff --git a/src/Layers/xrRender/fgFontRender.cpp b/src/Layers/xrRender/fgFontRender.cpp index 7147596e016..93a9678140f 100644 --- a/src/Layers/xrRender/fgFontRender.cpp +++ b/src/Layers/xrRender/fgFontRender.cpp @@ -62,7 +62,7 @@ void FGFontRender::InitResources() cbDesc.byteSize = sizeof(FontCB); cbDesc.isConstantBuffer = true; cbDesc.isVolatile = true; - cbDesc.maxVersions = 16; + cbDesc.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; cbDesc.debugName = "FGFontRender_CB"; m_constantBuffer = m_device->createBuffer(cbDesc); R_ASSERT2(m_constantBuffer, "FGFontRender: createBuffer(CB) failed"); diff --git a/src/Layers/xrRender/fgLensFlareRender.cpp b/src/Layers/xrRender/fgLensFlareRender.cpp index 9f098faeac6..438785e643e 100644 --- a/src/Layers/xrRender/fgLensFlareRender.cpp +++ b/src/Layers/xrRender/fgLensFlareRender.cpp @@ -55,7 +55,7 @@ void FGLensFlareRender::InitResources() cbDesc.byteSize = sizeof(passes::DynamicTransforms); cbDesc.isConstantBuffer = true; cbDesc.isVolatile = true; - cbDesc.maxVersions = 16; + cbDesc.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; cbDesc.debugName = "FGLensFlareRender_CB"; m_constantBuffer = m_device->createBuffer(cbDesc); R_ASSERT2(m_constantBuffer, "FGLensFlareRender: createBuffer(CB) failed"); diff --git a/src/Layers/xrRender/fgRainRender.cpp b/src/Layers/xrRender/fgRainRender.cpp index 4aeac28e8d6..bcb670f340d 100644 --- a/src/Layers/xrRender/fgRainRender.cpp +++ b/src/Layers/xrRender/fgRainRender.cpp @@ -96,7 +96,7 @@ void FGRainRender::InitResources() cbDesc.byteSize = sizeof(passes::SoftFXConstants); cbDesc.isConstantBuffer = true; cbDesc.isVolatile = true; - cbDesc.maxVersions = 16; + cbDesc.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; cbDesc.debugName = "FGRainRender_SoftCB"; m_constantBuffer = m_device->createBuffer(cbDesc); R_ASSERT2(m_constantBuffer, "FGRainRender: createBuffer(CB) failed"); diff --git a/src/Layers/xrRender/fgThunderboltRender.cpp b/src/Layers/xrRender/fgThunderboltRender.cpp index 1adcaa0d917..3c63a9b9bc6 100644 --- a/src/Layers/xrRender/fgThunderboltRender.cpp +++ b/src/Layers/xrRender/fgThunderboltRender.cpp @@ -51,7 +51,7 @@ void FGThunderboltRender::InitResources() cbDesc.byteSize = sizeof(passes::SoftFXConstants); cbDesc.isConstantBuffer = true; cbDesc.isVolatile = true; - cbDesc.maxVersions = 16; + cbDesc.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; cbDesc.debugName = "FGThunderboltRender_SoftCB"; m_constantBuffer = m_device->createBuffer(cbDesc); R_ASSERT2(m_constantBuffer, "FGThunderboltRender: createBuffer(CB) failed"); diff --git a/src/Layers/xrRender/fgUIShader.cpp b/src/Layers/xrRender/fgUIShader.cpp index a0087f54af2..9f4cff1c971 100644 --- a/src/Layers/xrRender/fgUIShader.cpp +++ b/src/Layers/xrRender/fgUIShader.cpp @@ -34,6 +34,13 @@ void fgUIShader::create(LPCSTR sh, LPCSTR tex) auto vsResult = shaderLoader->LoadVertexShader(sh, "main"); auto psResult = shaderLoader->LoadPixelShader(sh, "main"); + // Blender names like hud\fog_of_war have no r5 sources; prefer hud\default before stubs. + if ((!vsResult.handle || !psResult.handle) && sh && strstr(sh, "hud")) + { + vsResult = shaderLoader->LoadVertexShader("hud\\default", "main"); + psResult = shaderLoader->LoadPixelShader("hud\\default", "main"); + } + if (!vsResult.handle || !psResult.handle) { Msg("* [fgUIShader] Shader '%s' not found, falling back to stub_notransform_t", sh); diff --git a/src/Layers/xrRender/fg_rendertarget_pp.cpp b/src/Layers/xrRender/fg_rendertarget_pp.cpp new file mode 100644 index 00000000000..40e27948512 --- /dev/null +++ b/src/Layers/xrRender/fg_rendertarget_pp.cpp @@ -0,0 +1,84 @@ +#include "stdafx.h" +#include "r4_rendertarget.h" + +namespace xray::render::fg +{ +void CRenderTarget::u_calc_tc_noise(Fvector2& p0, Fvector2& p1) +{ + u32 tw = iCeil(512.f * param_noise_scale + EPS_S); + u32 th = iCeil(512.f * param_noise_scale + EPS_S); + if (!tw) tw = 1; + if (!th) th = 1; + + im_noise_time -= Device.fTimeDelta; + if (im_noise_time < 0) + { + im_noise_shift_w = ::Random.randI(tw); + im_noise_shift_h = ::Random.randI(th); + float fps_time = 1.f / param_noise_fps; + while (im_noise_time < 0) + im_noise_time += fps_time; + } + + float start_u = (float(im_noise_shift_w) + .5f) / float(tw); + float start_v = (float(im_noise_shift_h) + .5f) / float(th); + u32 _w = Device.dwWidth; + u32 _h = Device.dwHeight; + u32 cnt_w = _w / tw; + u32 cnt_h = _h / th; + p0.set(start_u, start_v); + p1.set(start_u + float(cnt_w) + 1.f, start_v + float(cnt_h) + 1.f); +} + +void CRenderTarget::u_calc_tc_duality_ss(Fvector2& r0, Fvector2& r1, Fvector2& l0, Fvector2& l1) +{ + float tw = float(Device.dwWidth); + float th = float(Device.dwHeight); + Fvector2 shift, p0, p1; + shift.set(.5f / tw, .5f / th); + shift.mul(param_blur); + p0.set(.5f / tw, .5f / th).add(shift); + p1.set((tw + .5f) / tw, (th + .5f) / th).add(shift); + + float shift_u = param_duality_h * .5f; + float shift_v = param_duality_v * .5f; + + r0.set(p0.x, p0.y); + r1.set(p1.x - shift_u, p1.y - shift_v); + l0.set(p0.x + shift_u, p0.y + shift_v); + l1.set(p1.x, p1.y); +} + +bool CRenderTarget::u_need_CM() +{ + return param_color_map_influence > 0.001f; +} + +bool CRenderTarget::u_need_PP() +{ + bool _blur = (param_blur > 0.001f); + bool _gray = (param_gray > 0.001f); + bool _noise = (param_noise > 0.001f); + bool _dual = (param_duality_h > 0.001f) || (param_duality_v > 0.001f); + + bool _cbase = false; + { + int _r = _abs(int(color_get_R(param_color_base)) - int(0x7f)); + int _g = _abs(int(color_get_G(param_color_base)) - int(0x7f)); + int _b = _abs(int(color_get_B(param_color_base)) - int(0x7f)); + if (_r > 2 || _g > 2 || _b > 2) + _cbase = true; + } + bool _cadd = false; + { + int _r = _abs((int)(param_color_add.x * 255)); + int _g = _abs((int)(param_color_add.y * 255)); + int _b = _abs((int)(param_color_add.z * 255)); + if (_r > 2 || _g > 2 || _b > 2) + _cadd = true; + } + return _blur || _gray || _noise || _dual || _cbase || _cadd || u_need_CM(); +} + +void CRenderTarget::phase_pp() {} +} // namespace xray::render::fg diff --git a/src/Layers/xrRender/light.cpp b/src/Layers/xrRender/light.cpp index cc94c8e91e1..7bb845f7ede 100644 --- a/src/Layers/xrRender/light.cpp +++ b/src/Layers/xrRender/light.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "light.h" +#include "Layers/xrRender/xrRender_console.h" namespace xray::render::fg { @@ -300,6 +301,7 @@ void light::EnsureOmniparts() L->set_volumetric_quality(m_volumetric_quality); L->set_volumetric_intensity(m_volumetric_intensity); L->set_volumetric_distance(m_volumetric_distance); + L->spatial_move(); } } @@ -337,6 +339,21 @@ void light::set_attenuation_params(float a0, float a1, float a2, float fo) falloff = fo; } +float light::get_LOD() const +{ + if (!flags.bShadow) + return 1.f; + + const float fovFactor = _sqr(90.f / std::max(Device.fFOV, 1.f)); + const float gScreen = float(Device.dwWidth * Device.dwHeight) * fovFactor * (EPS_S + ps_r__LOD); + const float ssaStart = _sqr(ps_r__GLOD_ssa_start / 3.f) / std::max(gScreen, 1.f); + const float ssaEnd = _sqr(ps_r__GLOD_ssa_end / 3.f) / std::max(gScreen, 1.f); + const float distSQ = Device.vCameraPosition.distance_to_sqr(spatial.sphere.P) + EPS; + const float ssa = ps_r2_slight_fade * spatial.sphere.R / distSQ; + const float denom = std::max(ssaStart - ssaEnd, 1e-8f); + return _sqrt(clampr((ssa - ssaEnd) / denom, 0.f, 1.f)); +} + #endif // (RENDER==R_R2) || (RENDER==R_R3) || (RENDER==R_R4) || (RENDER==R_GL) } // namespace xray::render::fg diff --git a/src/Layers/xrRender/light.h b/src/Layers/xrRender/light.h index a015696be4d..5ae58f19db2 100644 --- a/src/Layers/xrRender/light.h +++ b/src/Layers/xrRender/light.h @@ -159,9 +159,9 @@ class light : public IRender_Light, public SpatialBase void gi_generate(); void xform_calc(); void Export(light_Package& dest); - /// Ensure omnipart[6] faces exist and match this POINT light (for local shadow atlas). void EnsureOmniparts(); void set_attenuation_params(float a0, float a1, float a2, float fo); + [[nodiscard]] float get_LOD() const; #endif // (RENDER==R_R2) || (RENDER==R_R3) || (RENDER==R_R4) || (RENDER==R_GL) light(); diff --git a/src/Layers/xrRender/r4_rendertarget.h b/src/Layers/xrRender/r4_rendertarget.h index c6a0fef48c9..b7340aa4323 100644 --- a/src/Layers/xrRender/r4_rendertarget.h +++ b/src/Layers/xrRender/r4_rendertarget.h @@ -260,6 +260,20 @@ class CRenderTarget color_map_manager.SetTextures(tex0, tex1); } + float get_blur() const { return param_blur; } + float get_gray() const { return param_gray; } + float get_duality_h() const { return param_duality_h; } + float get_duality_v() const { return param_duality_v; } + float get_noise() const { return param_noise; } + float get_noise_scale() const { return param_noise_scale; } + float get_noise_fps() const { return param_noise_fps; } + u32 get_color_base() const { return param_color_base; } + u32 get_color_gray() const { return param_color_gray; } + const Fvector& get_color_add() const { return param_color_add; } + float get_cm_influence() const { return param_color_map_influence; } + float get_cm_interpolate() const { return param_color_map_interpolate; } + ColorMapManager& get_color_map_manager() { return color_map_manager; } + #ifdef DEBUG void dbg_addline(const Fvector& P0, const Fvector& P1, u32 c) { diff --git a/src/Layers/xrRender/r_FrameGraphRenderer.cpp b/src/Layers/xrRender/r_FrameGraphRenderer.cpp index 06ee7037732..484f20526d2 100644 --- a/src/Layers/xrRender/r_FrameGraphRenderer.cpp +++ b/src/Layers/xrRender/r_FrameGraphRenderer.cpp @@ -47,6 +47,7 @@ #include "FrameGraphPasses/SunPassSetup.h" // Sun disc rendering #include "FrameGraphPasses/SkinningPassSetup.h" #include "FrameGraphPasses/ParticlePassSetup.h" // Particle rendering (billboards/sprites) +#include "FrameGraphPasses/GlowPassSetup.h" #include "FrameGraphPasses/DistortionApplyPassSetup.h" // Distortion post-process #include "FrameGraphPasses/DecalPassSetup.h" // Screen-space box decals #include "Decals/DecalManager.h" // Decal manager @@ -65,6 +66,12 @@ #include "light.h" #include "FrameGraphPasses/MotionVectorPassSetup.h" #include "FrameGraphPasses/ReSTIRGIPassSetup.h" +#include "Upscaling/UpscaleState.h" +#include "Upscaling/IUpscaleBackend.h" +#include "Upscaling/UpscalePassSetup.h" +#include "Upscaling/StreamlineDLSS.h" +#include "Upscaling/DlssFgPassSetup.h" +#include "Denoising/IDenoiseBackend.h" #include "FrameGraphPasses/RibbonPassSetup.h" #include "FrameGraphPasses/TrailPassSetup.h" #include "Layers/xrRender/FrameGraph/Blackboard.h" @@ -80,11 +87,20 @@ #include "FrameGraphPasses/PathTracerPassSetup.h" #include "FrameGraphPasses/SSRPassSetup.h" #include "FrameGraphPasses/SSGIPassSetup.h" +#include "FrameGraphPasses/SSSSSPassSetup.h" +#include "FrameGraphPasses/IBLPrefilterPassSetup.h" #include "FrameGraphPasses/DofPassSetup.h" #include "FrameGraphPasses/TAAPassSetup.h" #include "FrameGraphPasses/ContactShadowsPassSetup.h" +#include "FrameGraphPasses/ShadowHZBPassSetup.h" +#include "FrameGraphPasses/ShadowMaskPassSetup.h" #include "FrameGraphPasses/BloomPassSetup.h" #include "FrameGraphPasses/CASPassSetup.h" +#include "FrameGraphPasses/PostProcessPassSetup.h" +#include "FrameGraphPasses/CameraModelPassSetup.h" +#include "FrameGraphPasses/LodPassSetup.h" +#include "Layers/xrRender/FLOD.h" +#include "Layers/xrRender/xrRender_console.h" #include "Layers/xrRender/fgRainRender.h" #include "Layers/xrRender/fgThunderboltRender.h" #include "Layers/xrRender/fgLensFlareRender.h" @@ -199,12 +215,29 @@ extern ENGINE_API int ps_r_bloom; extern ENGINE_API int ps_r_cas; extern ENGINE_API int ps_r_ssr; extern ENGINE_API int ps_r_ssgi; +extern ENGINE_API int ps_r_sssss; extern ENGINE_API int ps_r_contact_shadows; +extern ENGINE_API int ps_r_upscale; +extern ENGINE_API int ps_r_denoise; +extern ENGINE_API int ps_r_nrd_method; +extern ENGINE_API int ps_r_nrd_apply; +extern ENGINE_API int ps_r_dlss; +extern ENGINE_API int ps_r_dlss_rr; +extern ENGINE_API int ps_r_dlss_fg; namespace xray::render { using namespace fg; +bool FrameGraphRenderer::IsRTGIActive() const +{ + return passes::IsRTGIActive(m_rtAccelMgr.get()); +} + +static fg::UpscaleState g_upscaleState; +static xr_unique_ptr g_upscaleBackend; +static xr_unique_ptr g_denoiseBackend; + // Forward declaration and extern for accessing RImplementation namespace fg { extern xray::render::FrameGraphRenderer RImplementation; @@ -291,6 +324,10 @@ bool FrameGraphRenderer::Initialize(fg::RenderDevice* device) { m_smokeTrailManager->Initialize(device); m_volumetricRenderer->Initialize(device); + m_gpuCullingManager->Initialize(m_device); + if (m_rtAccelMgr && m_rtAccelMgr->IsSupported()) + m_gpuCullingManager->SetRTAccelStructManager(m_rtAccelMgr.get()); + // Create RenderContext for execution m_renderContext.reset(device->CreateContext()); if (!m_renderContext) @@ -434,6 +471,12 @@ void FrameGraphRenderer::Render() { ZoneScopedN("FrameGraphRenderer::Render"); if (!m_enabled) return; + if (g_pGamePersistent && + (g_pGamePersistent->MainMenuActiveOrLevelNotExist() || g_pGamePersistent->IsLoadingScreenShown())) + { + RenderMenu(); + return; + } VERIFY(m_framegraph != nullptr); @@ -518,7 +561,10 @@ void FrameGraphRenderer::Render() { // ═══════════════════════════════════════════════════════ { ZoneScopedN("FG::SetupPasses"); + auto t0 = std::chrono::high_resolution_clock::now(); SetupFrameGraphPasses(); + m_stats.fgSetupPassesMs = std::chrono::duration( + std::chrono::high_resolution_clock::now() - t0).count(); } // ═══════════════════════════════════════════════════════ @@ -534,10 +580,12 @@ void FrameGraphRenderer::Render() { else m_framegraph->SetAsyncCompute(nullptr, nullptr); - // Compile the graph (optimizes passes, calculates lifetimes, etc.) { ZoneScopedN("FG::Compile"); + auto t0 = std::chrono::high_resolution_clock::now(); m_framegraph->Compile(); + m_stats.fgCompileMs = std::chrono::duration( + std::chrono::high_resolution_clock::now() - t0).count(); } auto& cache = framegraph::GetPassResourceCache(); @@ -781,8 +829,15 @@ void FrameGraphRenderer::RenderStatsOverlay() stats.lightsOmni = clmStats.GetOmniCount(); u32 visCount = clmStats.GetVisibleLightCount(); stats.lightsHiZVisible = (visCount > 0) ? visCount : stats.lightsClustered; + stats.localShadowTiles = static_cast(clmStats.GetLocalShadowTiles().size()); + stats.localShadowCandidates = clmStats.GetLocalShadowCandidateCount(); + stats.localShadowDropped = clmStats.GetLocalShadowDroppedCount(); + stats.localShadowRedraw = clmStats.GetLocalShadowRedrawCount(); } + stats.mdiDrawCalls = passes::GetMdiDrawCallCount(); + stats.mdiMaxDrawCountSum = passes::GetMdiMaxDrawCountSum(); + if (m_detailManager) { stats.detailSlots = m_detailManager->slot_count; @@ -807,6 +862,8 @@ void FrameGraphRenderer::RenderStatsOverlay() stats.fgArenaCapacity = arenaStats.capacity; stats.fgArenaFallbacks = arenaStats.lastFallbackAllocs; } + stats.fgSetupPassesMs = m_stats.fgSetupPassesMs; + stats.fgCompileMs = m_stats.fgCompileMs; m_statsOverlay->SetRenderStats(stats); m_statsOverlay->SetInspectorPreview(m_inspectorPreview ? m_inspectorPreview.Get() : nullptr); @@ -920,6 +977,7 @@ void FrameGraphRenderer::SetupFrame() { ZoneScopedN("SetupFrame::CollectorBegin"); m_geometryCollector->BeginFrame(); m_hudBatches.clear(); + m_lodImpostors.clear(); m_worldParticleBatches.clear(); m_hudParticleBatches.clear(); @@ -933,6 +991,7 @@ void FrameGraphRenderer::SetupFrame() { { ZoneScopedN("SetupFrame::CollectorEnd"); + m_geometryCollector->Sort(); // opaque → aref → transparent (front-to-back) m_geometryCollector->EndFrame(); } } @@ -960,9 +1019,77 @@ framegraph::VirtualResourceHandle FrameGraphRenderer::CreateRT( void FrameGraphRenderer::SetupFrameGraphPasses() { // Subpixel Halton jitter for TAA (must run after camera projection is final) passes::ApplyTAAJitter(); + passes::ResetMdiDrawCounters(); - const u32 width = Device.dwWidth; - const u32 height = Device.dwHeight; + if ((ps_r_upscale != 0 || ps_r_dlss != 0) && !g_upscaleBackend) + g_upscaleBackend.reset(fg::CreateUpscaleBackendAuto()); + + { + static int s_denoiseCvar = -1; + static int s_nrdMethodCvar = -1; + static int s_dlssRrCvar = -1; + static int s_upscaleCvar = -1; + static int s_rrAvail = -1; + static u32 s_denoiseW = 0; + static u32 s_denoiseH = 0; + UpdateUpscaleState(g_upscaleState, Device.dwWidth, Device.dwHeight); + const u32 denoiseW = g_upscaleState.renderWidth ? g_upscaleState.renderWidth : Device.dwWidth; + const u32 denoiseH = g_upscaleState.renderHeight ? g_upscaleState.renderHeight : Device.dwHeight; + const int rrAvail = fg::Streamline_IsRRAvailable() ? 1 : 0; + const bool denoiseDirty = + s_denoiseCvar != ps_r_denoise || + s_nrdMethodCvar != ps_r_nrd_method || + s_dlssRrCvar != ps_r_dlss_rr || + s_upscaleCvar != ps_r_upscale || + s_rrAvail != rrAvail || + (g_denoiseBackend && (s_denoiseW != denoiseW || s_denoiseH != denoiseH)); + if (denoiseDirty && g_denoiseBackend) { + g_denoiseBackend->Shutdown(); + g_denoiseBackend.reset(); + } + if (ps_r_denoise != 0 && !g_denoiseBackend) + g_denoiseBackend.reset(fg::CreateDenoiseBackendAuto(ps_r_dlss_rr != 0 && ps_r_upscale == 2)); + if (g_denoiseBackend && g_denoiseBackend->IsAvailable()) + g_denoiseBackend->Resize(denoiseW, denoiseH); + s_denoiseCvar = ps_r_denoise; + s_nrdMethodCvar = ps_r_nrd_method; + s_dlssRrCvar = ps_r_dlss_rr; + s_upscaleCvar = ps_r_upscale; + s_rrAvail = rrAvail; + s_denoiseW = denoiseW; + s_denoiseH = denoiseH; + } + + // Recompute after backend auto-fallback may clear r_upscale + UpdateUpscaleState(g_upscaleState, Device.dwWidth, Device.dwHeight); + g_upscaleState.jitterX = passes::g_taa_jitter_px; + g_upscaleState.jitterY = passes::g_taa_jitter_py; + g_upscaleState.prevJitterX = passes::g_taa_jitter_prev_px; + g_upscaleState.prevJitterY = passes::g_taa_jitter_prev_py; + g_upscaleState.resetHistory = !m_hasPrevFrameData; + + const u32 displayWidth = g_upscaleState.displayWidth; + const u32 displayHeight = g_upscaleState.displayHeight; + const u32 width = g_upscaleState.renderWidth; + const u32 height = g_upscaleState.renderHeight; + const bool rtLightingWanted = + (ps_r_rt_gi || ps_r_path_tracer) && m_rtAccelMgr && m_rtAccelMgr->IsSupported(); + const bool rtgiActive = IsRTGIActive(); + const bool wantSunShaftCsm = ps_r_sun_shafts != 0; + if (rtLightingWanted) + { + auto& localShadow = m_blackboard->get_or_add(); + if (localShadow.initialized) + passes::ShutdownLocalShadowPass(m_device, localShadow); + if (!wantSunShaftCsm) + { + auto& shadowState = m_blackboard->get_or_add(); + if (shadowState.initialized) + passes::ShutdownShadowPass(m_device, shadowState); + } + } + const bool upscaleResolved = g_upscaleState.upscaleActive && + g_upscaleBackend && g_upscaleBackend->IsAvailable(); nvrhi::ITexture* backbufferTexture = GEnv.Backend->GetBackBuffer(); framegraph::VirtualResourceHandle backbufferHandle; @@ -970,8 +1097,8 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { if (backbufferTexture) { framegraph::ResourceDesc backbufferDesc; backbufferDesc.type = framegraph::ResourceDesc::Type::Texture2D; - backbufferDesc.width = width; - backbufferDesc.height = height; + backbufferDesc.width = displayWidth; + backbufferDesc.height = displayHeight; backbufferDesc.format = backbufferTexture->getDesc().format; backbufferDesc.isRenderTarget = true; backbufferDesc.isImported = true; @@ -1064,9 +1191,10 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { // ═══════════════════════════════════════════════════════ // HI-Z + GPU CULL - // r_depth_prepass ON: frustum cull first → depth → same-frame Hi-Z → - // occlusion compact (skipUpload) → Cluster → Forward. - // r_depth_prepass OFF: temporal Hi-Z (prev depth) → one cull → Forward. + // Linux + temporal depth: one cull with prev-frame Hi-Z; depth prepass + // still fills early-Z; same-frame Hi-Z for consumers; no cull#2. + // Otherwise (or no prev depth): frustum cull → depth → same-frame Hi-Z → + // occlusion compact when r_hiz_occlusion. // Detail/skinned/particle never use Hi-Z occlusion (flicker / self-occ). // ═══════════════════════════════════════════════════════ const bool useDepthPrepass = ps_r_depth_prepass != 0; @@ -1081,8 +1209,13 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { bool hasPrevDepth = m_hasPrevFrameData && m_prevFrameDepth && m_prevFrameWidth == width && m_prevFrameHeight == height; - // Temporal Hi-Z only when depth prepass is OFF (fallback path). - if (!useDepthPrepass && hasPrevDepth) { +#if !defined(XR_PLATFORM_APPLE) + const bool useTemporalHizCull = hasPrevDepth && (!useDepthPrepass || useHizOcclusion); +#else + const bool useTemporalHizCull = hasPrevDepth && !useDepthPrepass; +#endif + + if (useTemporalHizCull) { framegraph::ResourceDesc prevDepthDesc; prevDepthDesc.type = framegraph::ResourceDesc::Type::Texture2D; prevDepthDesc.debugName = "rt_PrevDepth"; @@ -1172,9 +1305,11 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { if (m_rtAccelMgr && m_rtAccelMgr->IsSupported()) m_gpuCullingManager->SetRTAccelStructManager(m_rtAccelMgr.get()); - // Depth-prepass path: frustum-only first; occlusion comes after same-frame Hi-Z. - // Fallback path: use temporal pyramid when available. +#if !defined(XR_PLATFORM_APPLE) + const bool forceFrustumOnly = !hizOutput.pyramid.is_valid(); +#else const bool forceFrustumOnly = useDepthPrepass || !hizOutput.pyramid.is_valid(); +#endif if (m_gpuCullingManager->IsEnabled()) { auto cullOutput = m_gpuCullingManager->SetupCullingPass( @@ -1332,9 +1467,19 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } if (m_gpuCullingManager->IsVariantPartitionEnabled()) - bindlessConfig.variantPartition = m_gpuCullingManager->GetStaticPartition().ToConfig(); + { + auto vp = m_gpuCullingManager->GetStaticPartition().ToConfig(); + // Cap rare variant bins — too many PSO switches hurts more than uber path + if (vp.variantCount > 8) + vp.variantCount = 0; + bindlessConfig.variantPartition = vp; + } } +#if !defined(XR_PLATFORM_APPLE) + bindlessConfig.useEqualDepth = useDepthPrepass; +#endif + // Cluster assign runs after Hi-Z is final (see below, post DepthPrepass). // ═══════════════════════════════════════════════════════ @@ -1373,7 +1518,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { ); } - // DetailCull: frustum / all-visible only (dummy Hi-Z cleared to far). + // DetailCull: prefer prev-frame Hi-Z when available; dummy only as last resort. // Same-frame pyramid is built after DepthPrepass — too late for grass CSM. if (!m_hizPyramid.is_valid() && m_gpuCullingManager && m_gpuCullingManager->GetDummyHiZTexture()) { @@ -1393,12 +1538,47 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { hizOutput.mipLevels = 1; } - // When depth-prepass path: force DetailCull onto cleared dummy (no occlusion). framegraph::VirtualResourceHandle detailHiz = m_hizPyramid; u32 detailHizW = hizOutput.width; u32 detailHizH = hizOutput.height; u32 detailHizMips = hizOutput.mipLevels; - if (useDepthPrepass && m_gpuCullingManager && m_gpuCullingManager->GetDummyHiZTexture()) + const Fmatrix* detailPrevVP = + (useTemporalHizCull || (!useDepthPrepass && m_hasPrevFrameData)) ? &m_prevViewProj : nullptr; + +#if defined(XR_PLATFORM_APPLE) + if (useDepthPrepass && hasPrevDepth) + { + framegraph::ResourceDesc prevDepthDesc; + prevDepthDesc.type = framegraph::ResourceDesc::Type::Texture2D; + prevDepthDesc.debugName = "rt_PrevDepth_Detail"; + prevDepthDesc.width = width; + prevDepthDesc.height = height; + prevDepthDesc.format = nvrhi::Format::D32; + prevDepthDesc.isDepthStencil = true; + prevDepthDesc.isImported = true; + prevDepthDesc.isTransient = false; + auto prevDepthHandle = m_framegraph->ImportTexture( + "rt_PrevDepth_Detail", m_prevFrameDepth, prevDepthDesc); + auto detailHizOut = passes::setupHiZBuildPass( + *m_framegraph, + m_device, + prevDepthHandle, + width, + height, + m_blackboard->get_or_add(), + "rt_HiZPyramid_Detail", + "Hi-Z Build Detail" + ); + detailHiz = detailHizOut.pyramid; + detailHizW = detailHizOut.width; + detailHizH = detailHizOut.height; + detailHizMips = detailHizOut.mipLevels; + detailPrevVP = &m_prevViewProj; + } + else +#endif + if (useDepthPrepass && !useTemporalHizCull && m_gpuCullingManager && + m_gpuCullingManager->GetDummyHiZTexture()) { framegraph::ResourceDesc dummyDesc; dummyDesc.type = framegraph::ResourceDesc::Type::Texture2D; @@ -1426,7 +1606,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { detailHizW, detailHizH, detailHizMips, - (useDepthPrepass || !m_hasPrevFrameData) ? nullptr : &m_prevViewProj, + detailPrevVP, m_gpuProfiler.get(), &m_blackboard->get_or_add() ); @@ -1434,12 +1614,12 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { // ═══════════════════════════════════════════════════════ // CASCADED SHADOW MAPS (sun CSM, before Forward) // ═══════════════════════════════════════════════════════ + if (!rtLightingWanted) { Fvector sunDir(0.3f, 0.8f, 0.2f); if (g_pGamePersistent) { const auto& env = g_pGamePersistent->Environment().CurrentEnv; - // toward sun (same as volumetric / lighting) sunDir.set(-env.sun_dir.x, -env.sun_dir.y, -env.sun_dir.z); } auto shadowOut = passes::setupCascadedShadowPass( @@ -1452,7 +1632,8 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { &m_hudBatches, &m_geometryCollector->GetBatches(), m_gpuCullingManager.get(), - m_blackboard->get_or_add()); + m_blackboard->get_or_add(), + skinnedDrawArgsBuffer); if (shadowOut.valid) { auto& shadowState = m_blackboard->get_or_add(); @@ -1465,7 +1646,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } } - // Local spot/OMNIPART shadow atlas (indoor lamps, flashlight, etc.) + if (!rtLightingWanted) { auto& clmLocal = fg::ClusteredLightManager::Instance(); clmLocal.AssignLocalShadowTiles(Device.vCameraPosition); @@ -1478,21 +1659,57 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { m_blackboard->get_or_add(), m_blackboard->get_or_add(), &m_geometryCollector->GetBatches(), - m_gpuCullingManager.get()); + m_gpuCullingManager.get(), + drawArgsBuffer); if (localOut.valid) { bindlessConfig.localShadowAtlas = localOut.atlasTex; + bindlessConfig.localShadowESM = localOut.esmTex; bindlessConfig.localShadowHandle = localOut.atlas; m_framegraph->GetRTRegistry().RegisterRT("rt_LocalShadowAtlas", localOut.atlas); } } } - passes::ResolveEnvSkyCubes(m_device, bindlessConfig.envSky0, bindlessConfig.envSky1); + passes::ShadowHZBOutput shadowHzbOut{}; + if (!rtLightingWanted) + { + shadowHzbOut = passes::setupShadowHZBPass( + *m_framegraph, + m_device, + m_blackboard->get_or_add(), + m_blackboard->get_or_add(), + bindlessConfig.shadowMapHandle); + if (shadowHzbOut.valid) + { + for (u32 i = 0; i < 3; ++i) + { + bindlessConfig.shadowHZB[i] = shadowHzbOut.hzb[i]; + bindlessConfig.shadowHZBHandles[i] = shadowHzbOut.handles[i]; + } + } + } - // Contact shadows: prev-frame depth + temporal history (cannot sample current DSV while attached) - if (ps_r_contact_shadows && m_hasPrevFrameData && m_prevFrameDepth) + passes::ResolveEnvSkyCubes(m_device, bindlessConfig.envSky0, bindlessConfig.envSky1); + nvrhi::ITexture* rawEnvSky0 = bindlessConfig.envSky0; + nvrhi::ITexture* rawEnvSky1 = bindlessConfig.envSky1; + { + auto& iblState = m_blackboard->get_or_add(); + auto ibl = passes::setupIBLPrefilterPass( + *m_framegraph, m_device, bindlessConfig.envSky0, bindlessConfig.envSky1, iblState); + if (ibl.spec0) + bindlessConfig.envSky0 = ibl.spec0; + if (ibl.spec1) + bindlessConfig.envSky1 = ibl.spec1; + bindlessConfig.envBrdfLut = ibl.brdfLut; + bindlessConfig.envSkySH = ibl.shBuffer; + bindlessConfig.envProbes = ibl.probeBuffer; + bindlessConfig.envProbeCubes = ibl.probeCubeArray; + } + + if (!rtLightingWanted && ps_r_contact_shadows && m_hasPrevFrameData && m_prevFrameDepth) bindlessConfig.contactDepth = m_prevFrameDepth; + if (!rtLightingWanted) { auto& contactState = m_blackboard->get_or_add(); bindlessConfig.contactHistory = passes::GetContactShadowHistory(contactState); @@ -1500,6 +1717,9 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { // Opaque depth fill → early-Z for Forward (LEQ, no depth clear). // Same-frame Hi-Z is built from this depth when r_depth_prepass is on. + // Cull1→Depth edge is required: Cull1 is otherwise orphaned when Cull2 + // replaces drawArgsBuffer, and Depth/Forward sample Cull1 compact buffers. + const framegraph::VirtualResourceHandle cull1DrawArgs = drawArgsBuffer; if (useDepthPrepass) { depthBuffer = passes::setupDepthPrepass( @@ -1511,9 +1731,9 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { width, height, bindlessConfig, - &m_blackboard->get_or_add()); + &m_blackboard->get_or_add(), + cull1DrawArgs); - // Same-frame Hi-Z from current depth → occlusion compact for static. hizOutput = passes::setupHiZBuildPass( *m_framegraph, m_device, @@ -1526,7 +1746,12 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { if (m_hizPyramid.is_valid()) m_framegraph->GetRTRegistry().RegisterRT("rt_HiZ", m_hizPyramid); - if (useHizOcclusion && m_gpuCullingManager && m_gpuCullingManager->IsEnabled() && +#if !defined(XR_PLATFORM_APPLE) + const bool needSameFrameOcclusionCull = useHizOcclusion && !useTemporalHizCull; +#else + const bool needSameFrameOcclusionCull = useHizOcclusion; +#endif + if (needSameFrameOcclusionCull && m_gpuCullingManager && m_gpuCullingManager->IsEnabled() && hizOutput.pyramid.is_valid()) { auto cullHiz = m_gpuCullingManager->SetupCullingPass( @@ -1536,19 +1761,22 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { hizOutput.height, hizOutput.mipLevels, m_geometryCollector.get(), - Device.mFullTransform, // same-frame: prev == current for Hi-Z UV + Device.mFullTransform, false, - true // skipUpload — reuse object buffers, re-compact with occlusion - ); + true, + cull1DrawArgs); if (cullHiz.drawArgsBuffer.is_valid()) drawArgsBuffer = cullHiz.drawArgsBuffer; } } - // Cluster assign: no Hi-Z light cull (temporal/same-frame both false-cull indoor lights). + // Cluster assign: Hi-Z cull with inflated light radius (light_hiz_cull.cs) + // and previous-frame pyramid to avoid same-frame false cull flicker. { auto& clmSetup = fg::ClusteredLightManager::Instance(); if (clmSetup.IsReady() && clmSetup.GetLightCount() > 0) { + const bool useClusterHiZ = + !rtLightingWanted && m_hasPrevFrameData && hizOutput.pyramid.is_valid(); passes::setupClusterLightPass( *m_framegraph, m_device, @@ -1556,16 +1784,36 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { width, height, &m_blackboard->get_or_add(), - framegraph::VirtualResourceHandle(), - 0, - 0, - 0, + useClusterHiZ ? hizOutput.pyramid : framegraph::VirtualResourceHandle(), + useClusterHiZ ? hizOutput.width : 0, + useClusterHiZ ? hizOutput.height : 0, + useClusterHiZ ? hizOutput.mipLevels : 0, m_prevViewProj, - false + useClusterHiZ ); } } + if (!rtLightingWanted) + { + auto maskOut = passes::setupShadowMaskPass( + *m_framegraph, + m_device, + depthBuffer, + width, + height, + bindlessConfig, + shadowHzbOut, + m_blackboard->get_or_add()); + if (maskOut.valid) + { + bindlessConfig.shadowMask = maskOut.mask; + bindlessConfig.shadowMaskHandle = maskOut.handle; + if (maskOut.handle.is_valid()) + m_framegraph->GetRTRegistry().RegisterRT("rt_ShadowMask", maskOut.handle); + } + } + auto forwardOutputs = passes::setupForwardColorPass( *m_framegraph, m_device, @@ -1599,15 +1847,13 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { ); } - // 2. Skinning Pass - Renders all skinned meshes (world + HUD) - // World skinned: NPCs, monsters with normal depth [0.0, 1.0] - // HUD skinned: First-person weapons/hands with depth [0.0, 0.1] + // 2. Skinning Pass - world skinned only (HUD deferred past RTGI/NRD) auto hudOutputs = passes::setupSkinningPass( *m_framegraph, m_device, forwardOutputs, m_geometryCollector.get(), - &m_hudBatches, + nullptr, m_materialCache.get(), width, height, @@ -1623,9 +1869,21 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { bindlessConfig.envSky1, bindlessConfig.hudShadowMap, bindlessConfig.shadowCascades, - bindlessConfig.localShadowAtlas + bindlessConfig.localShadowAtlas, + bindlessConfig.shadowHZB, + bindlessConfig.shadowMask, + bindlessConfig.localShadowESM ); + auto lodOutputs = passes::setupLodPass( + *m_framegraph, + m_device, + hudOutputs, + &m_lodImpostors, + width, + height, + m_blackboard->get_or_add()); + // ═══════════════════════════════════════════════════════ // DETAIL DRAW PASS (Graphics) // ═══════════════════════════════════════════════════════ @@ -1633,7 +1891,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { *m_framegraph, m_device, m_detailManager.get(), - hudOutputs, + lodOutputs, width, height, m_gpuProfiler.get(), @@ -1643,9 +1901,30 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { bindlessConfig.contactHistory, perlinReadyHandle, bindlessConfig.shadowCascades, - bindlessConfig.localShadowAtlas + bindlessConfig.localShadowAtlas, + bindlessConfig.localShadowESM ); + auto opaqueLit = detailOutputs; + if (!rtLightingWanted) + { + auto aoDepth = detailOutputs.depth.is_valid() ? detailOutputs.depth : depthBuffer; + auto aoNormal = detailOutputs.normal.is_valid() ? detailOutputs.normal : normalBuffer; + auto aoWorldPos = detailOutputs.worldPos.is_valid() ? detailOutputs.worldPos : worldPosBuffer; + auto aoColor = passes::setupAmbientOcclusionPass( + *m_framegraph, + m_device, + detailOutputs.albedo, + aoDepth, + aoNormal, + aoWorldPos, + width, + height, + m_blackboard->get_or_add()); + if (aoColor.is_valid()) + opaqueLit.albedo = aoColor; + } + // ═══════════════════════════════════════════════════════ // TRANSPARENT PASS (alpha-blended geometry) // ═══════════════════════════════════════════════════════ @@ -1663,10 +1942,18 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { for (int i = 0; i < 3; ++i) transparentConfig.shadowCascades[i] = bindlessConfig.shadowCascades[i]; transparentConfig.localShadowAtlas = bindlessConfig.localShadowAtlas; - transparentConfig.envSky0 = bindlessConfig.envSky0; - transparentConfig.envSky1 = bindlessConfig.envSky1; + transparentConfig.localShadowESM = bindlessConfig.localShadowESM; + transparentConfig.envSky0 = rawEnvSky0 ? rawEnvSky0 : bindlessConfig.envSky0; + transparentConfig.envSky1 = rawEnvSky1 ? rawEnvSky1 : bindlessConfig.envSky1; transparentConfig.contactDepth = bindlessConfig.contactDepth; transparentConfig.contactHistory = bindlessConfig.contactHistory; + for (u32 i = 0; i < 3; ++i) + { + transparentConfig.shadowHZB[i] = bindlessConfig.shadowHZB[i]; + transparentConfig.shadowHZBHandles[i] = bindlessConfig.shadowHZBHandles[i]; + } + transparentConfig.shadowMask = bindlessConfig.shadowMask; + transparentConfig.shadowMaskHandle = bindlessConfig.shadowMaskHandle; if (m_gpuCullingManager->IsVariantPartitionEnabled()) transparentConfig.variantPartition = m_gpuCullingManager->GetTransparentPartition().ToConfig(); @@ -1675,7 +1962,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { auto transparentOutputs = passes::setupTransparentPass( *m_framegraph, m_device, - detailOutputs, + opaqueLit, transparentConfig, width, height, m_blackboard->get_or_add() @@ -1696,118 +1983,134 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } } + framegraph::VirtualResourceHandle rtgiGuideDepth = transparentOutputs.depth; + framegraph::VirtualResourceHandle rtgiGuideNormal = transparentOutputs.normal; + framegraph::VirtualResourceHandle rtgiGuideWorldPos = transparentOutputs.worldPos; + framegraph::VirtualResourceHandle rtgiGuideBaseColor = transparentOutputs.baseColor; + if (rtgiActive) + { + framegraph::ResourceDesc gd; + gd.type = framegraph::ResourceDesc::Type::Texture2D; + gd.width = width; + gd.height = height; + gd.isTransient = true; + + gd.debugName = "rtgi_GuideDepth"; + gd.format = nvrhi::Format::D32; + gd.isDepthStencil = true; + auto guideDepth = m_framegraph->CreateTexture("rtgi_GuideDepth", gd); + + gd.isDepthStencil = false; + gd.isRenderTarget = true; + gd.debugName = "rtgi_GuideNormal"; + gd.format = nvrhi::Format::RGBA16_FLOAT; + auto guideNormal = m_framegraph->CreateTexture("rtgi_GuideNormal", gd); + + gd.debugName = "rtgi_GuideWorldPos"; + gd.format = nvrhi::Format::RGBA32_FLOAT; + auto guideWorldPos = m_framegraph->CreateTexture("rtgi_GuideWorldPos", gd); + + gd.debugName = "rtgi_GuideBaseColor"; + gd.format = nvrhi::Format::RGBA8_UNORM; + auto guideBaseColor = m_framegraph->CreateTexture("rtgi_GuideBaseColor", gd); + + struct GuideCopyData { + framegraph::VirtualResourceHandle srcDepth, srcNormal, srcWorldPos, srcBase; + framegraph::VirtualResourceHandle dstDepth, dstNormal, dstWorldPos, dstBase; + }; + m_framegraph->addCallbackPass( + "RTGI Guide Copy", + [&](framegraph::FrameGraph& builder, framegraph::PassHandle passHandle, GuideCopyData& data) { + framegraph::RenderPassBuilder pb(builder, passHandle); + data.srcDepth = pb.read(transparentOutputs.depth, framegraph::ResourceState::CopySource); + data.srcNormal = pb.read(transparentOutputs.normal, framegraph::ResourceState::CopySource); + data.srcWorldPos = pb.read(transparentOutputs.worldPos, framegraph::ResourceState::CopySource); + data.srcBase = pb.read(transparentOutputs.baseColor, framegraph::ResourceState::CopySource); + data.dstDepth = pb.write(guideDepth, framegraph::ResourceState::CopyDest); + data.dstNormal = pb.write(guideNormal, framegraph::ResourceState::CopyDest); + data.dstWorldPos = pb.write(guideWorldPos, framegraph::ResourceState::CopyDest); + data.dstBase = pb.write(guideBaseColor, framegraph::ResourceState::CopyDest); + }, + [](const GuideCopyData& data, const framegraph::FrameGraph& fgGraph, fg::RenderContext* ctx) { + auto* cmd = ctx->GetCommandList(); + auto* sd = fgGraph.GetPhysicalTexture(data.srcDepth); + auto* sn = fgGraph.GetPhysicalTexture(data.srcNormal); + auto* sw = fgGraph.GetPhysicalTexture(data.srcWorldPos); + auto* sb = fgGraph.GetPhysicalTexture(data.srcBase); + auto* dd = fgGraph.GetPhysicalTexture(data.dstDepth); + auto* dn = fgGraph.GetPhysicalTexture(data.dstNormal); + auto* dw = fgGraph.GetPhysicalTexture(data.dstWorldPos); + auto* db = fgGraph.GetPhysicalTexture(data.dstBase); + if (!cmd || !sd || !sn || !sw || !sb || !dd || !dn || !dw || !db) + return; + cmd->copyTexture(dd, nvrhi::TextureSlice(), sd, nvrhi::TextureSlice()); + cmd->copyTexture(dn, nvrhi::TextureSlice(), sn, nvrhi::TextureSlice()); + cmd->copyTexture(dw, nvrhi::TextureSlice(), sw, nvrhi::TextureSlice()); + cmd->copyTexture(db, nvrhi::TextureSlice(), sb, nvrhi::TextureSlice()); + }); + + rtgiGuideDepth = guideDepth; + rtgiGuideNormal = guideNormal; + rtgiGuideWorldPos = guideWorldPos; + rtgiGuideBaseColor = guideBaseColor; + + if (!m_hudBatches.empty()) + { + passes::setupHudOverlayPass( + *m_framegraph, + m_device, + transparentOutputs.albedo, + rtgiGuideDepth, + rtgiGuideNormal, + rtgiGuideBaseColor, + rtgiGuideWorldPos, + &m_hudBatches, + m_materialCache.get(), + width, + height, + m_gpuCullingManager.get(), + &m_blackboard->get_or_add(), + bindlessConfig.shadowMapArray, + bindlessConfig.contactDepth, + bindlessConfig.contactHistory, + bindlessConfig.envSky0, + bindlessConfig.envSky1, + bindlessConfig.hudShadowMap, + bindlessConfig.shadowCascades, + bindlessConfig.localShadowAtlas, + bindlessConfig.shadowHZB, + bindlessConfig.shadowMask, + bindlessConfig.localShadowESM, + true + ); + } + } + // ═══════════════════════════════════════════════════════ - // MOTION VECTOR PASS (Depth-based reprojection) + // MOTION VECTOR PASS (worldPos camera reprojection) // ═══════════════════════════════════════════════════════ passes::MotionVectorOutput motionOutput; if (m_hasPrevFrameData) { motionOutput = passes::setupMotionVectorPass( *m_framegraph, m_device, - transparentOutputs.depth, - passes::g_taa_unjittered_inv_full_transform, m_prevViewProj, + rtgiGuideDepth, + rtgiGuideWorldPos, + passes::g_taa_unjittered_full_transform, + m_prevViewProj, width, height, m_blackboard->get_or_add() ); } - // ═══════════════════════════════════════════════════════ - // PARTICLE PASS (after all opaque + transparent geometry) - // ═══════════════════════════════════════════════════════ - auto particleOutputs = passes::setupParticlePass( - *m_framegraph, - m_device, - transparentOutputs, - &m_worldParticleBatches, - &m_hudParticleBatches, - m_materialCache.get(), - width, - height, - hizOutput.pyramid, - hizOutput.width, - hizOutput.height, - hizOutput.mipLevels, - &m_blackboard->get_or_add() - ); - - // ═══════════════════════════════════════════════════════ - // RIBBON + TRAIL TEST PASSES (debug quads, off by default) - // Not wired to any emitter — kept behind r_test_trails. - // ═══════════════════════════════════════════════════════ - auto trailChainLayout = particleOutputs.layout; - if (ps_r_test_trails) - { - auto ribbonOutputs = passes::setupRibbonPass( - *m_framegraph, - m_device, - particleOutputs.layout, - width, - height, - &m_blackboard->get_or_add() - ); - - auto trailOutputs = passes::setupTrailPass( - *m_framegraph, - m_device, - ribbonOutputs.layout, - width, - height, - &m_blackboard->get_or_add() - ); - trailChainLayout = trailOutputs.layout; - } - - // ═══════════════════════════════════════════════════════ - // SMOKE TRAIL PASS (GPU-simulated weapon muzzle smoke) - // ═══════════════════════════════════════════════════════ - auto smokeOutputs = trailChainLayout; - if (ps_r_smoke_trail_enabled && m_smokeTrailManager && m_smokeTrailManager->IsReady()) - { - smokeOutputs = passes::setupSmokeTrailPass( - *m_framegraph, - m_device, - trailChainLayout, - m_smokeTrailManager.get(), - width, - height, - m_blackboard->get_or_add(), - m_detailManager ? m_detailManager->perlin4dTexture.Get() : nullptr - ); - } - - // Wet surfaces applied later (after lighting composites) — see below. - auto gbufferForWet = smokeOutputs; - - auto sceneColor = smokeOutputs.albedo; - - if (particleOutputs.distortionRT.is_valid()) { - sceneColor = passes::setupDistortionApplyPass( - *m_framegraph, m_device, sceneColor, particleOutputs.distortionRT, - particleOutputs.layout.worldPos, width, height, - m_blackboard->get_or_add()); - } + // Wet G-buffer snapshot before RTGI (particles deferred until after denoise). + auto gbufferForWet = transparentOutputs; + auto sceneColor = transparentOutputs.albedo; + framegraph::VirtualResourceHandle pendingDistortRT{}; + framegraph::VirtualResourceHandle pendingDistortWorldPos = transparentOutputs.worldPos; + framegraph::VirtualResourceHandle pendingDistortBaseColor = transparentOutputs.baseColor; // ═══════════════════════════════════════════════════════ - // ReSTIR GI (RT Shadows + Indirect Lighting) - // ═══════════════════════════════════════════════════════ - - if (ps_r_rt_gi && m_rtAccelMgr && m_rtAccelMgr->IsSupported() && m_rtAccelMgr->IsReady()) { - auto rtgiOutput = passes::setupReSTIRGIPass( - *m_framegraph, m_device, m_rtAccelMgr.get(), - transparentOutputs.depth, transparentOutputs.normal, - transparentOutputs.baseColor, transparentOutputs.worldPos, - prevNormalsHandle, prevWorldPosHandle, - motionOutput.motionVectors, - sceneColor, - passes::g_taa_unjittered_inv_full_transform, m_prevViewProj, - Device.vCameraPosition, ps_r_rt_gi_intensity, - width, height, - m_blackboard->get_or_add(), m_hasPrevFrameData - ); - sceneColor = rtgiOutput.sceneColor; - } - - // ═══════════════════════════════════════════════════════ - // DYNAMIC BLAS BUILD (shared by Path Tracer + ReSTIR GI) + // DYNAMIC BLAS BUILD (before ReSTIR / path tracer) // ═══════════════════════════════════════════════════════ bool needsRT = (ps_r_path_tracer || ps_r_rt_gi) && m_rtAccelMgr && m_rtAccelMgr->IsSupported(); @@ -1873,6 +2176,123 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } } + // ═══════════════════════════════════════════════════════ + // ReSTIR GI (RT Shadows + Indirect Lighting) + // ═══════════════════════════════════════════════════════ + + if (rtgiActive) { + const bool useDlssRR = ps_r_dlss_rr != 0 && ps_r_upscale == 2 && fg::Streamline_IsRRAvailable(); + const bool vendorDenoise = fg::IsVendorDenoiseActive(g_denoiseBackend.get()); + const bool useVendorDenoise = vendorDenoise && ps_r_nrd_apply != 0 && !useDlssRR; + const bool skipInTreeDenoise = useVendorDenoise || useDlssRR; + auto sceneColorBeforeRtgi = sceneColor; + auto& rtgiState = m_blackboard->get_or_add(); + { + auto& tpState = m_blackboard->get_or_add(); + rtgiState.waterUnderWorldPos = tpState.waterSceneWorldPos.Get(); + } + auto rtgiOutput = passes::setupReSTIRGIPass( + *m_framegraph, m_device, m_rtAccelMgr.get(), + rtgiGuideDepth, rtgiGuideNormal, + rtgiGuideBaseColor, rtgiGuideWorldPos, + prevNormalsHandle, prevWorldPosHandle, + motionOutput.motionVectors, + sceneColor, + passes::g_taa_unjittered_inv_full_transform, m_prevViewProj, + Device.vCameraPosition, ps_r_rt_gi_intensity, + width, height, + rtgiState, m_hasPrevFrameData, + skipInTreeDenoise, + transparentOutputs.worldPos, + useDlssRR + ); + sceneColor = rtgiOutput.sceneColor; + + if (useVendorDenoise) + { + struct DenoisePassData { + fg::IDenoiseBackend* backend = nullptr; + passes::ReSTIRGIPassState* state = nullptr; + framegraph::VirtualResourceHandle depth, normal, worldPos, baseColor, classifyWorldPos, motion, sceneIn, sceneOut; + bool reset = false; + }; + auto& rtgiState = m_blackboard->get_or_add(); + const bool denoiseReset = !m_hasPrevFrameData; + m_framegraph->addCallbackPass( + "RT Denoise", + [&, sceneColorBeforeRtgi, denoiseReset](framegraph::FrameGraph& builder, framegraph::PassHandle passHandle, DenoisePassData& data) { + framegraph::RenderPassBuilder pb(builder, passHandle); + data.depth = pb.read(rtgiGuideDepth, framegraph::ResourceState::ShaderResource); + data.normal = pb.read(rtgiGuideNormal, framegraph::ResourceState::ShaderResource); + data.worldPos = pb.read(rtgiGuideWorldPos, framegraph::ResourceState::ShaderResource); + data.baseColor = pb.read(rtgiGuideBaseColor, framegraph::ResourceState::ShaderResource); + data.classifyWorldPos = pb.read(transparentOutputs.worldPos, framegraph::ResourceState::ShaderResource); + if (motionOutput.motionVectors.is_valid()) + data.motion = pb.read(motionOutput.motionVectors, framegraph::ResourceState::ShaderResource); + data.sceneIn = pb.read(sceneColorBeforeRtgi, framegraph::ResourceState::ShaderResource); + data.sceneOut = pb.write(sceneColor, framegraph::ResourceState::UnorderedAccess); + pb.sideEffects(); + data.backend = g_denoiseBackend.get(); + data.state = &rtgiState; + data.reset = denoiseReset; + }, + [](const DenoisePassData& data, const framegraph::FrameGraph& fgGraph, fg::RenderContext* ctx) { + if (!data.backend || !data.state) return; + auto* depthTex = fgGraph.GetPhysicalTexture(data.depth); + auto* normalTex = fgGraph.GetPhysicalTexture(data.normal); + auto* worldPosTex = fgGraph.GetPhysicalTexture(data.worldPos); + auto* baseColorTex = fgGraph.GetPhysicalTexture(data.baseColor); + auto* classifyTex = fgGraph.GetPhysicalTexture(data.classifyWorldPos); + auto* motionTex = data.motion.is_valid() ? fgGraph.GetPhysicalTexture(data.motion) : nullptr; + auto* sceneInTex = fgGraph.GetPhysicalTexture(data.sceneIn); + auto* sceneOutTex = fgGraph.GetPhysicalTexture(data.sceneOut); + if (!depthTex || !normalTex || !worldPosTex || !sceneInTex || !sceneOutTex) + return; + if (!data.state->noisyDiffuse || !data.state->noisySpecular || !data.state->hitDistance || + !data.state->directLighting) + return; + + auto copyMat = [](float dst[16], const Fmatrix& m) { + dst[0] = m._11; dst[1] = m._12; dst[2] = m._13; dst[3] = m._14; + dst[4] = m._21; dst[5] = m._22; dst[6] = m._23; dst[7] = m._24; + dst[8] = m._31; dst[9] = m._32; dst[10] = m._33; dst[11] = m._34; + dst[12] = m._41; dst[13] = m._42; dst[14] = m._43; dst[15] = m._44; + }; + + fg::DenoiseInputs in; + in.noisyDiffuse = data.state->noisyDiffuse; + in.noisySpecular = data.state->noisySpecular; + in.hitDistance = data.state->hitDistance; + in.normals = normalTex; + in.depth = depthTex; + in.worldPos = worldPosTex; + in.baseColor = baseColorTex; + in.classifyWorldPos = classifyTex; + in.motionVectors = motionTex; + in.directLighting = data.state->directLighting; + in.sceneColorIn = sceneInTex; + in.outDiffuse = data.state->noisyDiffuse; + in.outSpecular = data.state->noisySpecular; + in.outSceneColor = sceneOutTex; + in.width = data.state->texWidth; + in.height = data.state->texHeight; + in.jitterX = passes::g_taa_jitter_px; + in.jitterY = passes::g_taa_jitter_py; + in.jitterPrevX = passes::g_taa_jitter_prev_px; + in.jitterPrevY = passes::g_taa_jitter_prev_py; + in.nearZ = 0.2f; + in.farZ = g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.far_plane : 500.f; + copyMat(in.viewToClip, Device.mProject); + copyMat(in.viewToClipPrev, Device.mProjectSaved); + copyMat(in.worldToView, Device.mView); + copyMat(in.worldToViewPrev, Device.mViewSaved); + in.frameIndex = Device.dwFrame; + in.reset = data.reset; + data.backend->Evaluate(ctx->GetCommandList(), in); + }); + } + } + // ═══════════════════════════════════════════════════════ // PATH TRACER (Reference / Ground-Truth Mode) // ═══════════════════════════════════════════════════════ @@ -1918,8 +2338,101 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } } + { + framegraph::DefaultOutputLayout fxLayout = transparentOutputs; + fxLayout.albedo = sceneColor; + if (rtgiActive && rtgiGuideDepth.is_valid()) + fxLayout.depth = rtgiGuideDepth; + if (rtgiActive && rtgiGuideWorldPos.is_valid()) + fxLayout.worldPos = rtgiGuideWorldPos; + if (rtgiActive && rtgiGuideNormal.is_valid()) + fxLayout.normal = rtgiGuideNormal; + if (rtgiActive && rtgiGuideBaseColor.is_valid()) + fxLayout.baseColor = rtgiGuideBaseColor; + auto particleOutputs = passes::setupParticlePass( + *m_framegraph, + m_device, + fxLayout, + &m_worldParticleBatches, + &m_hudParticleBatches, + m_materialCache.get(), + width, + height, + hizOutput.pyramid, + hizOutput.width, + hizOutput.height, + hizOutput.mipLevels, + &m_blackboard->get_or_add() + ); + + auto trailChainLayout = particleOutputs.layout; + if (ps_r_test_trails) + { + auto ribbonOutputs = passes::setupRibbonPass( + *m_framegraph, + m_device, + particleOutputs.layout, + width, + height, + &m_blackboard->get_or_add() + ); + auto trailOutputs = passes::setupTrailPass( + *m_framegraph, + m_device, + ribbonOutputs.layout, + width, + height, + &m_blackboard->get_or_add() + ); + trailChainLayout = trailOutputs.layout; + } + + auto smokeOutputs = trailChainLayout; + if (ps_r_smoke_trail_enabled && m_smokeTrailManager && m_smokeTrailManager->IsReady()) + { + smokeOutputs = passes::setupSmokeTrailPass( + *m_framegraph, + m_device, + trailChainLayout, + m_smokeTrailManager.get(), + width, + height, + m_blackboard->get_or_add(), + m_detailManager ? m_detailManager->perlin4dTexture.Get() : nullptr + ); + } + + sceneColor = smokeOutputs.albedo; + gbufferForWet = smokeOutputs; + + const auto glowDepth = (rtgiActive && rtgiGuideDepth.is_valid()) + ? rtgiGuideDepth + : (transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer); + sceneColor = passes::setupGlowBillboardPass( + *m_framegraph, + m_device, + sceneColor, + glowDepth, + width, + height, + m_blackboard->get_or_add()); + + if (particleOutputs.distortionRT.is_valid()) + pendingDistortRT = particleOutputs.distortionRT; + else if (transparentOutputs.distortion.is_valid()) + pendingDistortRT = transparentOutputs.distortion; + if (rtgiActive && rtgiGuideWorldPos.is_valid()) + pendingDistortWorldPos = rtgiGuideWorldPos; + else if (transparentOutputs.worldPos.is_valid()) + pendingDistortWorldPos = transparentOutputs.worldPos; + if (rtgiActive && rtgiGuideBaseColor.is_valid()) + pendingDistortBaseColor = rtgiGuideBaseColor; + else if (transparentOutputs.baseColor.is_valid()) + pendingDistortBaseColor = transparentOutputs.baseColor; + } + passes::RainShadowOutputs rainShadowOut{}; - { + { const float rainDensity = g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.rain_density : 0.f; @@ -1930,7 +2443,6 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { *m_framegraph, m_device, bindlessConfig, - m_blackboard->get_or_add(), m_blackboard->get_or_add()); } } @@ -1953,12 +2465,14 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { { if (fgRain->HasWork()) { - auto rainDepth = transparentOutputs.depth.is_valid() - ? transparentOutputs.depth - : depthBuffer; - auto rainWorldPos = transparentOutputs.worldPos.is_valid() - ? transparentOutputs.worldPos - : gbufferForWet.worldPos; + auto rainDepth = (rtgiActive && rtgiGuideDepth.is_valid()) + ? rtgiGuideDepth + : (transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer); + auto rainWorldPos = (rtgiActive && rtgiGuideWorldPos.is_valid()) + ? rtgiGuideWorldPos + : (transparentOutputs.worldPos.is_valid() + ? transparentOutputs.worldPos + : gbufferForWet.worldPos); sceneColor = passes::setupRainPass( *m_framegraph, sceneColor, @@ -1981,12 +2495,14 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { { if (fgTB->HasWork()) { - auto tbDepth = transparentOutputs.depth.is_valid() - ? transparentOutputs.depth - : depthBuffer; - auto tbWorldPos = transparentOutputs.worldPos.is_valid() - ? transparentOutputs.worldPos - : gbufferForWet.worldPos; + auto tbDepth = (rtgiActive && rtgiGuideDepth.is_valid()) + ? rtgiGuideDepth + : (transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer); + auto tbWorldPos = (rtgiActive && rtgiGuideWorldPos.is_valid()) + ? rtgiGuideWorldPos + : (transparentOutputs.worldPos.is_valid() + ? transparentOutputs.worldPos + : gbufferForWet.worldPos); sceneColor = passes::setupThunderboltPass( *m_framegraph, sceneColor, @@ -1997,12 +2513,25 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } } - // Wet surfaces AFTER lighting (variant C + wet SSR from SceneReflection). framegraph::VirtualResourceHandle ssrReflectionSource = sceneColor; { - ssrReflectionSource = passes::setupSceneReflectionCapture( - *m_framegraph, sceneColor, width, height); - m_framegraph->GetRTRegistry().RegisterRT("rt_SceneReflection", ssrReflectionSource); + const float rainForRefl = g_pGamePersistent + ? g_pGamePersistent->Environment().CurrentEnv.rain_density + : 0.f; + const bool needSceneReflection = + (ps_r_ssr != 0) || + (rainForRefl > 0.001f && ps_r2_ls_flags.test(R3FLAG_DYN_WET_SURF)); + if (needSceneReflection) + { + ssrReflectionSource = passes::setupSceneReflectionCapture( + *m_framegraph, + m_device, + sceneColor, + width, + height, + m_blackboard->get_or_add()); + m_framegraph->GetRTRegistry().RegisterRT("rt_SceneReflection", ssrReflectionSource); + } } framegraph::VirtualResourceHandle wetNormal = gbufferForWet.normal; { @@ -2021,15 +2550,21 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } auto wetIn = gbufferForWet; wetIn.albedo = sceneColor; - if (transparentOutputs.normal.is_valid()) + if (rtgiActive && rtgiGuideNormal.is_valid()) + wetIn.normal = rtgiGuideNormal; + else if (transparentOutputs.normal.is_valid()) wetIn.normal = transparentOutputs.normal; else if (normalBuffer.is_valid()) wetIn.normal = normalBuffer; - if (transparentOutputs.worldPos.is_valid()) + if (rtgiActive && rtgiGuideWorldPos.is_valid()) + wetIn.worldPos = rtgiGuideWorldPos; + else if (transparentOutputs.worldPos.is_valid()) wetIn.worldPos = transparentOutputs.worldPos; else if (worldPosBuffer.is_valid()) wetIn.worldPos = worldPosBuffer; - if (transparentOutputs.baseColor.is_valid()) + if (rtgiActive && rtgiGuideBaseColor.is_valid()) + wetIn.baseColor = rtgiGuideBaseColor; + else if (transparentOutputs.baseColor.is_valid()) wetIn.baseColor = transparentOutputs.baseColor; else if (baseColorBuffer.is_valid()) wetIn.baseColor = baseColorBuffer; @@ -2046,30 +2581,25 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { wetNormal = wetOut.normal; } - // Screen-space AO (SSAO / HBAO / HDAO / GTAO) after lighting composites. - { - auto aoDepth = transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer; - sceneColor = passes::setupAmbientOcclusionPass( - *m_framegraph, - m_device, - sceneColor, - aoDepth, - wetNormal, - gbufferForWet.worldPos, - width, - height, - m_blackboard->get_or_add()); - } + const bool froxelVolumetrics = + ps_r2_ls_flags.test(R3FLAG_VOLUMETRIC_SMOKE) && m_volumetricRenderer && m_volumetricRenderer->IsReady() + && g_pGamePersistent && !g_pGamePersistent->MainMenuActiveOrLevelNotExist() + && !Device.dwPrecacheFrame && !g_pGamePersistent->IsLoadingScreenShown(); + + const auto occludeDepth = (rtgiActive && rtgiGuideDepth.is_valid()) + ? rtgiGuideDepth + : (transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer); - // Classic sunshafts (shadow-mapped god rays) — CoP volumetric sun look + if (!rtgiActive) { - auto shaftDepth = transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer; + auto shaftDepth = occludeDepth; + auto shaftWorldPos = gbufferForWet.worldPos.is_valid() ? gbufferForWet.worldPos : worldPosBuffer; sceneColor = passes::setupSunShaftsPass( *m_framegraph, m_device, sceneColor, shaftDepth, - gbufferForWet.worldPos, + shaftWorldPos, bindlessConfig.shadowMapArray, bindlessConfig.shadowMapHandle, width, @@ -2078,12 +2608,10 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { bindlessConfig.shadowCascades); } - // Sun disc AFTER opaque/transparent depth is ready — depth-tested so geometry occludes it. - // (Drawn before lens flares; flares do not redraw the sun source.) sceneColor = passes::setupSunPass( *m_framegraph, sceneColor, - transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer, + occludeDepth, fgEnv, width, height @@ -2092,7 +2620,6 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { if (g_pGamePersistent && g_pGamePersistent->Environment().eff_LensFlare) { auto* effLF = g_pGamePersistent->Environment().eff_LensFlare; - // bSun=false: sun disc is drawn by setupSunPass with depth test effLF->Render(false, true, true); if (auto* fgLF = dynamic_cast(effLF->GetRenderer())) { @@ -2101,7 +2628,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { sceneColor = passes::setupLensFlarePass( *m_framegraph, sceneColor, - transparentOutputs.depth, + occludeDepth, fgLF); } } @@ -2110,30 +2637,66 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { // ═══════════════════════════════════════════════════════ // FROXEL VOLUMETRIC FOG (optional; off by default for classic look) // ═══════════════════════════════════════════════════════ - if (ps_r2_ls_flags.test(R3FLAG_VOLUMETRIC_SMOKE) && m_volumetricRenderer && m_volumetricRenderer->IsReady()) + if (!rtLightingWanted && froxelVolumetrics) { + passes::VolumetricLightingInputs volLighting{}; + volLighting.shadowMapArray = bindlessConfig.shadowMapArray; + for (int i = 0; i < 3; ++i) + { + volLighting.shadowCascades[i] = bindlessConfig.shadowCascades[i]; + volLighting.shadowHZB[i] = bindlessConfig.shadowHZB[i]; + } + volLighting.lightManager = &fg::ClusteredLightManager::Instance(); + sceneColor = passes::setupVolumetricPass( *m_framegraph, m_device, m_volumetricRenderer.get(), sceneColor, - transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer, + occludeDepth, width, height, m_blackboard->get_or_add(), - &m_worldParticleBatches); + &m_worldParticleBatches, + &volLighting); } - // ═══════════════════════════════════════════════════════ - // MODERN POST: Contact temporal → SSGI → SSR → TAA → Bloom - // ═══════════════════════════════════════════════════════ - auto postDepth = transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer; + if (!rtgiActive && !m_hudBatches.empty()) + { + sceneColor = passes::setupHudOverlayPass( + *m_framegraph, + m_device, + sceneColor, + transparentOutputs.depth, + transparentOutputs.normal, + transparentOutputs.baseColor, + transparentOutputs.worldPos, + &m_hudBatches, + m_materialCache.get(), + width, + height, + m_gpuCullingManager.get(), + &m_blackboard->get_or_add(), + bindlessConfig.shadowMapArray, + bindlessConfig.contactDepth, + bindlessConfig.contactHistory, + bindlessConfig.envSky0, + bindlessConfig.envSky1, + bindlessConfig.hudShadowMap, + bindlessConfig.shadowCascades, + bindlessConfig.localShadowAtlas, + bindlessConfig.shadowHZB, + bindlessConfig.shadowMask, + bindlessConfig.localShadowESM + ); + } + + auto postDepth = occludeDepth; auto postNormal = wetNormal.is_valid() ? wetNormal : (transparentOutputs.normal.is_valid() ? transparentOutputs.normal : gbufferForWet.normal); auto postBase = transparentOutputs.baseColor.is_valid() ? transparentOutputs.baseColor : baseColorBuffer; - // Update contact-shadow history for next frame (sampled as g_ContactHistory @ t28) - if (ps_r_contact_shadows) + if (!rtLightingWanted && ps_r_contact_shadows) { passes::setupContactShadowsPass( *m_framegraph, @@ -2147,7 +2710,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { m_blackboard->get_or_add()); } - if (ps_r_ssgi && postNormal.is_valid() && postBase.is_valid()) + if (!rtLightingWanted && ps_r_ssgi && postNormal.is_valid() && postBase.is_valid()) { sceneColor = passes::setupSSGIPass( *m_framegraph, @@ -2166,6 +2729,10 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { if (ps_r_ssr && postNormal.is_valid() && postBase.is_valid()) { + const auto ssrWorldPos = (rtgiActive && rtgiGuideWorldPos.is_valid()) + ? rtgiGuideWorldPos + : (transparentOutputs.worldPos.is_valid() ? transparentOutputs.worldPos + : (gbufferForWet.worldPos.is_valid() ? gbufferForWet.worldPos : worldPosBuffer)); sceneColor = passes::setupSSRPass( *m_framegraph, m_device, @@ -2174,8 +2741,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { postDepth, postNormal, postBase, - transparentOutputs.worldPos.is_valid() ? transparentOutputs.worldPos - : (gbufferForWet.worldPos.is_valid() ? gbufferForWet.worldPos : worldPosBuffer), + ssrWorldPos, motionOutput.motionVectors, width, height, @@ -2184,8 +2750,39 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { m_framegraph->GetRTRegistry().RegisterRT("rt_SSR", sceneColor); } - // TAA after SSR (restored — SSR hard-gate fixed in wet normals) - if (ps_r_taa) + if (!rtLightingWanted && ps_r_sssss && postNormal.is_valid() && postBase.is_valid()) + { + sceneColor = passes::setupSSSSSPass( + *m_framegraph, + m_device, + sceneColor, + postDepth, + postNormal, + postBase, + width, + height, + m_blackboard->get_or_add()); + m_framegraph->GetRTRegistry().RegisterRT("rt_SSSSS", sceneColor); + } + + if (!rtLightingWanted) + { + auto& iblState = m_blackboard->get_or_add(); + nvrhi::ITexture* rawSky0 = nullptr; + nvrhi::ITexture* rawSky1 = nullptr; + passes::ResolveEnvSkyCubes(m_device, rawSky0, rawSky1); + passes::setupIBLProbeCapturePass( + *m_framegraph, + m_device, + rawSky0, + rawSky1, + sceneColor, + postDepth, + iblState); + } + + // TAA after SSR (skipped when temporal upscaler owns history) + if (ps_r_taa && ps_r_upscale == 0) { sceneColor = passes::setupTAAPass( *m_framegraph, @@ -2214,36 +2811,114 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { ); m_exposureTexture = exposureOutput.exposureTexture; - if (ps_r_bloom) + // FSR/DLSS or 1:1 resolve to display resolution before bloom/tonemap/UI { - sceneColor = passes::setupBloomPass( + passes::UpscaleRRGuides rrGuides{}; + passes::UpscaleRRGuides* rrGuidesPtr = nullptr; + if (ps_r_upscale == 2 && ps_r_dlss_rr) + { + auto& rtgiState = m_blackboard->get_or_add(); + rrGuides.normals = rtgiGuideNormal; + rrGuides.baseColor = rtgiGuideBaseColor; + rrGuides.worldPos = rtgiGuideWorldPos; + rrGuides.depth = rtgiGuideDepth; + rrGuides.noisyDiffuse = rtgiState.noisyDiffuse.Get(); + rrGuides.noisySpecular = rtgiState.noisySpecular.Get(); + rrGuides.hitDistance = rtgiState.hitDistance.Get(); + rrGuidesPtr = &rrGuides; + } + const auto upscaleDepth = (ps_r_upscale == 2 && ps_r_dlss_rr && rtgiGuideDepth.is_valid()) + ? rtgiGuideDepth + : postDepth; + sceneColor = passes::setupUpscaleOrResolvePass( *m_framegraph, m_device, sceneColor, + upscaleDepth, + motionOutput.motionVectors, + exposureOutput.exposureTexture, + g_upscaleState, + g_upscaleBackend.get(), width, height, + m_blackboard->get_or_add(), + rrGuidesPtr); + } + const u32 postW = upscaleResolved ? displayWidth : width; + const u32 postH = upscaleResolved ? displayHeight : height; + const bool upscaleOwnsTemporal = ps_r_upscale != 0; + + if (pendingDistortRT.is_valid() && o.distortion) + { + sceneColor = passes::setupDistortionApplyPass( + *m_framegraph, + m_device, + sceneColor, + pendingDistortRT, + pendingDistortWorldPos, + pendingDistortBaseColor, + postW, + postH, + m_blackboard->get_or_add()); + } + + if (rtgiActive && m_rtAccelMgr) + { + const auto volWorldPos = rtgiGuideWorldPos.is_valid() + ? rtgiGuideWorldPos + : (transparentOutputs.worldPos.is_valid() ? transparentOutputs.worldPos : worldPosBuffer); + sceneColor = passes::setupRTVolumetricPass( + *m_framegraph, + m_device, + m_rtAccelMgr.get(), + sceneColor, + occludeDepth, + volWorldPos, + passes::g_taa_unjittered_inv_full_transform, + Device.vCameraPosition, + postW, + postH, + m_blackboard->get_or_add()); + } + + if (ps_r_bloom) + { + sceneColor = passes::setupBloomPass( + *m_framegraph, + m_device, + sceneColor, + postDepth, + postW, + postH, m_blackboard->get_or_add()); m_framegraph->GetRTRegistry().RegisterRT("rt_Bloom", sceneColor); } - // DOF on HDR (classic CoP combine-time blur), gated by r2_dof_enable - if (ps_r2_ls_flags.test(R2FLAG_DOF)) + if (!upscaleOwnsTemporal && ps_r2_ls_flags.test(R2FLAG_DOF)) { sceneColor = passes::setupDofPass( *m_framegraph, m_device, sceneColor, postDepth, - width, - height, + postW, + postH, m_blackboard->get_or_add()); m_framegraph->GetRTRegistry().RegisterRT("rt_DOF", sceneColor); } - // Tonemap/CAS on scene HDR only — UI/menu/loading must stay LDR and unfiltered + const bool needPP = m_pTarget && (m_pTarget->u_need_PP() + || (g_pGamePersistent && g_pGamePersistent->m_pGShaderConstants + && g_pGamePersistent->m_pGShaderConstants->m_blender_mode.x > 0.5f)); + const bool needCamera = ps_r_camera != 0 && ( + (ps_r_camera_distort_enable && ps_r_camera_distort > 1e-4f) || + (ps_r_camera_ca_enable && ps_r_camera_ca > 1e-4f) || + (ps_r_camera_vignette_enable && ps_r_camera_vignette > 1e-4f) || + (!upscaleOwnsTemporal && ps_r_camera_grain_enable && ps_r_camera_grain > 1e-4f) || + (!upscaleOwnsTemporal && ps_r_camera_mblur_enable && ps_r2_mblur > 1e-4f)); framegraph::VirtualResourceHandle tonemapTarget = backbufferHandle; - if (ps_r_cas) - tonemapTarget = framegraph::VirtualResourceHandle{}; // creates internal rt_Final + if (ps_r_cas || needPP || needCamera) + tonemapTarget = framegraph::VirtualResourceHandle{}; auto ldrOutput = passes::setupTonemapPass( *m_framegraph, @@ -2251,12 +2926,43 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { sceneColor, exposureOutput.exposureTexture, tonemapTarget, - width, - height, + postW, + postH, m_blackboard->get_or_add(), &m_blackboard->get_or_add() ); + if (needCamera) + { + framegraph::VirtualResourceHandle camTarget = + (needPP || ps_r_cas) ? framegraph::VirtualResourceHandle{} : backbufferHandle; + ldrOutput = passes::setupCameraModelPass( + *m_framegraph, + m_device, + ldrOutput, + motionOutput.motionVectors, + camTarget, + postW, + postH, + m_blackboard->get_or_add()); + } + + if (needPP) + { + framegraph::VirtualResourceHandle ppTarget = ps_r_cas + ? framegraph::VirtualResourceHandle{} + : backbufferHandle; + ldrOutput = passes::setupPostProcessPass( + *m_framegraph, + m_device, + ldrOutput, + ppTarget, + postW, + postH, + m_pTarget, + m_blackboard->get_or_add()); + } + if (ps_r_cas && backbufferHandle.is_valid()) { ldrOutput = passes::setupCASPass( @@ -2264,8 +2970,8 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { m_device, ldrOutput, backbufferHandle, - width, - height, + postW, + postH, m_blackboard->get_or_add()); } @@ -2273,8 +2979,8 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { auto sceneWithUI = passes::setupUIPass( *m_framegraph, ldrOutput, - width, - height + postW, + postH ); sceneWithUI = passes::setupFontPass(*m_framegraph, sceneWithUI); @@ -2282,11 +2988,38 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { sceneWithUI = passes::setupCursorPass( *m_framegraph, sceneWithUI, - width, - height + postW, + postH ); - sceneWithUI = passes::setupDebugDrawPass(*m_framegraph, sceneWithUI, width, height); + sceneWithUI = passes::setupDebugDrawPass(*m_framegraph, sceneWithUI, postW, postH); + + const bool useDlssFg = ps_r_upscale == 2 && ps_r_dlss_fg && fg::Streamline_IsFGAvailable(); + if (useDlssFg) + { + fg::ImGuiRendererNVRHI* imguiRendererFg = GEnv.Render->GetImGuiRendererNVRHI(); + sceneWithUI = passes::setupImGuiPass( + *m_framegraph, + sceneWithUI, + imguiRendererFg, + postW, + postH); + + passes::setupDlssFgPass( + *m_framegraph, + sceneWithUI, + framegraph::VirtualResourceHandle{}, + postDepth.is_valid() ? postDepth : depthBuffer, + motionOutput.motionVectors, + Device.mProject, + m_prevViewProj, + passes::g_taa_unjittered_full_transform, + width, + height, + postW, + postH, + !m_hasPrevFrameData); + } ldrOutput = sceneWithUI; @@ -2440,16 +3173,19 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { } } - fg::ImGuiRendererNVRHI* imguiRenderer = GEnv.Render->GetImGuiRendererNVRHI(); - auto finalOutput = passes::setupImGuiPass( - *m_framegraph, - ldrOutput, - imguiRenderer, - width, - height - ); + framegraph::VirtualResourceHandle finalOutput = ldrOutput; + if (!useDlssFg) + { + fg::ImGuiRendererNVRHI* imguiRenderer = GEnv.Render->GetImGuiRendererNVRHI(); + finalOutput = passes::setupImGuiPass( + *m_framegraph, + ldrOutput, + imguiRenderer, + width, + height + ); + } - // Store final output for presentation (now points to backbuffer) m_finalOutput = finalOutput; // ═══════════════════════════════════════════════════════ @@ -2573,25 +3309,8 @@ bool FrameGraphRenderer::ProcessVisualGeometry(dxRender_Visual* visual, const Fm return false; GeometryBatch batch; - batch.vertexBuffer = nvrhiVB; - batch.indexBuffer = nvrhiIB; - - // ═══════════════════════════════════════════════════════ - // INDEX/VERTEX OFFSET HANDLING - // ═══════════════════════════════════════════════════════ - // Different mesh types have different buffer layouts: - // - // SKINNED MESHES (CSkeletonX_ST, CSkeletonX_PM): - // - Have dedicated VB/IB (not shared pools) - // - vBase = 0, startIndex = 0 (ST) or SW.offset (PM) - // - Vanilla: _Render(rm_geom, vCount, 0, dwPrimitives) for ST - // - Vanilla: _Render(rm_geom, SW.num_verts, SW.offset, SW.num_tris) for PM - // - // STATIC/PROGRESSIVE MESHES (Fvisual, FProgressive, FTreeVisual): - // - Use shared VB/IB pools from level geometry - // - For now, always use iBase/iCount (max detail, no LOD) - // - TODO: Implement proper LOD selection for progressive meshes - // + batch.vertexBuffer = nvrhiVB.Get(); + batch.indexBuffer = nvrhiIB.Get(); u32 visualType = visual->getType(); bool isSkinned = (visualType == MT_SKELETON_GEOMDEF_ST || visualType == MT_SKELETON_GEOMDEF_PM); @@ -2615,9 +3334,9 @@ bool FrameGraphRenderer::ProcessVisualGeometry(dxRender_Visual* visual, const Fm } else if (visualType == MT_PROGRESSIVE) { const FSlideWindowItem& swi = static_cast(visual)->GetSWI(); if (swi.sw && swi.count > 0) { - const FSlideWindow& sw = swi.sw[0]; // LOD 0 = max detail + const FSlideWindow& sw = swi.sw[0]; batch.indexCount = sw.num_tris * 3; - batch.startIndex = meshVisual->iBase + sw.offset; // iBase + SW.offset + batch.startIndex = meshVisual->iBase + sw.offset; } else { batch.indexCount = meshVisual->iCount; batch.startIndex = meshVisual->iBase; @@ -2626,7 +3345,14 @@ bool FrameGraphRenderer::ProcessVisualGeometry(dxRender_Visual* visual, const Fm } else if (visualType == MT_TREE_PM) { const FSlideWindowItem* pSWI = static_cast(visual)->GetSWI(); if (pSWI && pSWI->sw && pSWI->count > 0) { - const FSlideWindow& sw = pSWI->sw[0]; // LOD 0 = max detail + u32 lodIdx = 0; + if (!isStatic) { + Fvector center = visual->vis.sphere.P; + const float dist = Device.vCameraPosition.distance_to(center) + EPS; + const float r = std::max(visual->vis.sphere.R, 0.1f); + lodIdx = u32(clampr(iFloor(dist / (r * 4.f)), 0, int(pSWI->count - 1))); + } + const FSlideWindow& sw = pSWI->sw[lodIdx]; batch.indexCount = sw.num_tris * 3; batch.startIndex = meshVisual->iBase + sw.offset; } else { @@ -2676,14 +3402,11 @@ bool FrameGraphRenderer::ProcessVisualGeometry(dxRender_Visual* visual, const Fm batch.pipeline = nullptr; batch.bindingSet = nullptr; - fg::ShaderKey shaderKey; - if (fg::ExtractShaderKey(visual, shaderKey)) { - static thread_local std::string s_debugNameBuffer; - s_debugNameBuffer = shaderKey.ToString(); - batch.debugName = s_debugNameBuffer.c_str(); - } else { + if (visual->shaderName.size()) + batch.debugName = visual->shaderName; + else batch.debugName = ""; - } + batch.CacheSortFlags(); if (!nvrhiVB || !nvrhiIB) { Msg("! [ProcessVisualGeometry] ERROR: Created batch with null buffers! VB=%p, IB=%p", @@ -2708,9 +3431,12 @@ bool FrameGraphRenderer::ProcessVisualGeometry(dxRender_Visual* visual, const Fm meshVisual->useAlternativeGeom ); - // Debug: Log allocation details for first few batches + // Dynamic/particle meshes never join mega-buffers (pool IDs stay UINT32_MAX). + // Only warn when pools were assigned but allocation still failed. static int s_allocDebug = 0; - if (s_allocDebug < 10 && !batch.megaBufferAlloc.valid) { + const bool poolsAssigned = + meshVisual->vbPoolID != UINT32_MAX && meshVisual->ibPoolID != UINT32_MAX; + if (s_allocDebug < 10 && !batch.megaBufferAlloc.valid && poolsAssigned) { Msg("! [MegaBuffer] Invalid alloc: vbPool=%u, vBase=%u, vCount=%u, ibPool=%u, iBase=%u, iCount=%u, alt=%d", meshVisual->vbPoolID, batch.baseVertex, meshVisual->vCount, meshVisual->ibPoolID, batch.startIndex, batch.indexCount, @@ -2767,8 +3493,8 @@ bool FrameGraphRenderer::ProcessHudGeometry(dxRender_Visual* visual, const Fmatr return false; GeometryBatch batch; - batch.vertexBuffer = nvrhiVB; - batch.indexBuffer = nvrhiIB; + batch.vertexBuffer = nvrhiVB.Get(); + batch.indexBuffer = nvrhiIB.Get(); batch.indexCount = meshVisual->iCount; batch.startIndex = meshVisual->iBase; batch.baseVertex = meshVisual->vBase; @@ -2876,17 +3602,25 @@ void FrameGraphRenderer::ProcessSingleParticleEffect( batch.isHUDMode = isHUDParticle; batch.particleCount = particleCount; batch.blendMode = QueryParticleBlendMode(pDef->m_ShaderName.c_str()); + batch.lightingMode = pDef->m_LightingMode; const bool isDistort = pDef->m_ShaderName.c_str() && strstr(pDef->m_ShaderName.c_str(), "distort") != nullptr; if (isDistort) batch.shaderVariant = passes::ParticleShaderVariant::Distort; + else if (batch.lightingMode == fg::PS::ParticleLightingMode::Emissive) + batch.shaderVariant = passes::ParticleShaderVariant::Emissive; + else if (batch.lightingMode == fg::PS::ParticleLightingMode::Lit) + batch.shaderVariant = passes::ParticleShaderVariant::Standard; + else if (batch.lightingMode == fg::PS::ParticleLightingMode::SixWay) + batch.shaderVariant = passes::ParticleShaderVariant::Standard; if (m_materialCache && pDef->m_TextureName.size()) { shared_str tex = ResolveParticleTextureName(pDef->m_TextureName, isDistort); batch.bindlessMaterialID = m_materialCache->PreRegisterParticleMaterial( - tex, batch.blendMode); + tex, batch.blendMode, pDef->m_ShaderName.c_str(), + pDef->m_SixWayPosXYZ.c_str(), pDef->m_SixWayNegXYZ.c_str()); } if (isHUDParticle) @@ -3086,13 +3820,6 @@ void FrameGraphRenderer::CollectVisibleGeometry() { if (!g_pGamePersistent) return; - // Classic: wait HOM raster before portal traverse / occ_visible - if (m_pProcessHOMTask) - { - ZoneScopedN("CollectVisibleGeometry::WaitHOM"); - TaskScheduler->Wait(*m_pProcessHOMTask); - m_pProcessHOMTask = nullptr; - } if (!ps_r_portal_cull) m_HOM.Disable(); @@ -3100,8 +3827,6 @@ void FrameGraphRenderer::CollectVisibleGeometry() { u32 submittedStatic = 0; bool justBuiltStaticCache = false; - // Build full unique static cache once (lazy sector fill caused GPU static - // upload to freeze after the first incomplete frame → see-through indoors). if (!m_staticCacheInitialized && !sectors.empty()) { ZoneScopedN("CollectVisibleGeometry::BuildStaticCache"); @@ -3121,11 +3846,16 @@ void FrameGraphRenderer::CollectVisibleGeometry() { m_cachedStaticBatches.size(), sectors.size()); } - // Portal/sector traverse — optional indoor filter (off by default) m_portalTraverseActive = false; if (ps_r_portal_cull) { ZoneScopedN("CollectVisibleGeometry::PortalTraverse"); + if (m_pProcessHOMTask) + { + ZoneScopedN("CollectVisibleGeometry::WaitHOM"); + TaskScheduler->Wait(*m_pProcessHOMTask); + m_pProcessHOMTask = nullptr; + } CFrustum viewFrustum; viewFrustum.CreateFromMatrix(Device.mFullTransform, FRUSTUM_P_LRTB | FRUSTUM_P_FAR); @@ -3153,25 +3883,87 @@ void FrameGraphRenderer::CollectVisibleGeometry() { Device.mFullTransform, traverseOptions, useHom ? &m_HOM : nullptr); + + // Outdoor (largest) sector owns trees/terrain/bushes. Starting from an + // indoor/near-door sector, portal PVS drops it as soon as outdoor + // portals leave the frustum — trees vanish on camera yaw. Always keep + // outdoor in the visible set; frustum/HOM still cull individual batches. + const auto outdoorId = Scene.largest_sector_id; + if (outdoorId != IRender_Sector::INVALID_SECTOR_ID && + outdoorId < sectors.size() && sectors[outdoorId]) + { + CSector* outdoor = sectors[outdoorId]; + if (outdoor->r_marker != PortalTraverser.i_marker) + { + outdoor->r_marker = PortalTraverser.i_marker; + outdoor->r_frustums.clear(); + outdoor->r_frustums.push_back(viewFrustum); + outdoor->r_scissors.clear(); + PortalTraverser.r_sectors.push_back(outdoor); + } + } + m_portalTraverseActive = !PortalTraverser.r_sectors.empty(); } } - // Build frame already filled the collector via ProcessVisualGeometry. + if (m_gpuCullingManager) + m_gpuCullingManager->SetStaticBatchSource( + m_staticCacheInitialized ? &m_cachedStaticBatches : nullptr); + if (!justBuiltStaticCache && m_staticCacheInitialized) { ZoneScopedN("CollectVisibleGeometry::StaticSubmit"); - xr_vector submitted(m_cachedStaticBatches.size(), 0); + const u32 batchCount = static_cast(m_cachedStaticBatches.size()); + if (m_staticSubmitMark.size() < batchCount) + m_staticSubmitMark.resize(batchCount, 0); + if (++m_staticSubmitEpoch == 0) + { + std::fill(m_staticSubmitMark.begin(), m_staticSubmitMark.end(), 0); + m_staticSubmitEpoch = 1; + } + const u8 epoch = m_staticSubmitEpoch; + + const bool gpuOpaqueStaticResident = + m_gpuCullingManager && m_gpuCullingManager->IsEnabled(); +#if defined(XR_PLATFORM_APPLE) + const bool tessFlagOn = false; +#else + const bool tessFlagOn = ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION); +#endif auto submitBatchId = [&](u32 bi) { - if (bi >= m_cachedStaticBatches.size() || (bi < submitted.size() && submitted[bi])) + if (bi >= batchCount || m_staticSubmitMark[bi] == epoch) return; - if (bi < submitted.size()) - submitted[bi] = 1; - const auto& batch = m_cachedStaticBatches[bi]; + m_staticSubmitMark[bi] = epoch; + auto& batch = m_cachedStaticBatches[bi]; + + if (gpuOpaqueStaticResident) + { + if (batch.isTerrain) + return; + if (!batch.sortFlagsValid) + batch.CacheSortFlags(); + if (!batch.IsStrictB2F()) + { + bool routeTess = false; + if (tessFlagOn) + { + constexpr u32 kMaxTessIndexCount = 8192; + const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(batch.bindlessMaterialID); + routeTess = mat && mat->tessMethod != 0 && batch.indexCount > 0 && + batch.indexCount <= kMaxTessIndexCount; + } + if (!routeTess) + return; + } + } + if (batch.visual && !occ_visible(batch.visual->vis)) return; - m_geometryCollector->Submit(batch); + if (!batch.sortFlagsValid) + batch.CacheSortFlags(); + m_geometryCollector->SubmitStatic(batch); submittedStatic++; }; @@ -3187,7 +3979,7 @@ void FrameGraphRenderer::CollectVisibleGeometry() { } else { - for (u32 bi = 0; bi < m_cachedStaticBatches.size(); ++bi) + for (u32 bi = 0; bi < batchCount; ++bi) submitBatchId(bi); } } @@ -3247,6 +4039,22 @@ void FrameGraphRenderer::add_Visual(IRenderable* root, IRenderVisual* V, Fmatrix bool isHUD = (root && root->renderable_HUD()); + if (!isHUD && visual->Type == MT_LOD) + { + auto* lod = static_cast(visual); + Fvector center; + xform.transform_tiny(center, lod->vis.sphere.P); + const float distSQ = Device.vCameraPosition.distance_to_sqr(center) + EPS; + const float ssa = lod->vis.sphere.R / distSQ; + const float gScreen = float(Device.dwWidth) * float(Device.dwHeight); + const float ssaStart = _sqr(ps_r__GLOD_ssa_start) / std::max(gScreen, 1.f); + if (ssa * lod->lod_factor < ssaStart) + { + m_lodImpostors.push_back({lod, xform}); + return; + } + } + ForEachLeafVisual(visual, [&](dxRender_Visual* leafVisual) { if (isHUD) { // HUD geometry - separate processing with different projection/culling @@ -3397,15 +4205,94 @@ namespace class CGlow : public IRender_Glow { public: + light* m_light = nullptr; + Fvector m_pos{}; + Fvector m_dir{ 0.f, -1.f, 0.f }; + float m_radius = 1.f; + Fcolor m_color{ 1.f, 1.f, 1.f, 1.f }; + shared_str m_texture; bool bActive{ false }; - void set_active(bool b) override { bActive = b; } + + explicit CGlow(light* L) : m_light(L) + { + fg::passes::GlowRegistry_Register(this); + if (!m_light) + return; + m_light->flags.type = IRender_Light::POINT; + m_light->flags.bShadow = false; + m_light->flags.bVolumetric = false; + m_light->set_active(false); + } + + ~CGlow() override + { + fg::passes::GlowRegistry_Unregister(this); + if (m_light) + { + m_light->set_active(false); + xr_delete(m_light); + } + } + + static bool CollectBillboard(void* glow, fg::passes::GlowBillboard& out) + { + auto* g = static_cast(glow); + if (!g || !g->bActive) + return false; + out.pos = g->m_pos; + out.radius = g->m_radius; + out.color = g->m_color; + out.texture = g->m_texture; + return true; + } + + void SyncLight() + { + if (!m_light) + return; + m_light->set_position(m_pos); + m_light->set_rotation(m_dir, m_dir); + m_light->set_range(_max(m_radius * 2.5f, 0.5f)); + m_light->set_color(m_color); + m_light->set_active(bActive); + } + + void set_active(bool b) override + { + bActive = b; + SyncLight(); + } bool get_active() override { return bActive; } - void set_position(const Fvector&) override {} - void set_direction(const Fvector&) override {} - void set_radius(float) override {} - void set_texture(LPCSTR) override {} - void set_color(const Fcolor&) override {} - void set_color(float, float, float) override {} + void set_position(const Fvector& P) override + { + m_pos = P; + SyncLight(); + } + void set_direction(const Fvector& D) override + { + m_dir = D; + if (m_dir.magnitude() < 1e-4f) + m_dir.set(0.f, -1.f, 0.f); + else + m_dir.normalize(); + SyncLight(); + } + void set_radius(float R) override + { + m_radius = _max(R, 0.05f); + SyncLight(); + } + void set_texture(LPCSTR name) override { m_texture = name; } + void set_color(const Fcolor& C) override + { + m_color = C; + SyncLight(); + } + void set_color(float r, float g, float b) override + { + m_color.set(r, g, b, 1.f); + SyncLight(); + } }; float EstimateSplatUVRadius(const fg::decals::MeshPickResult& pickResult, float worldRadius) @@ -3452,7 +4339,16 @@ IRender_ObjectSpecific* FrameGraphRenderer::ros_create(IRenderable*) { return xr void FrameGraphRenderer::ros_destroy(IRender_ObjectSpecific*& p) { xr_delete(p); } IRender_Light* FrameGraphRenderer::light_create() { return Lights.Create(); } -IRender_Glow* FrameGraphRenderer::glow_create() { return xr_new(); } +IRender_Glow* FrameGraphRenderer::glow_create() +{ + static bool s_glowCollectHooked = false; + if (!s_glowCollectHooked) + { + fg::passes::GlowRegistry_SetCollect(&CGlow::CollectBillboard); + s_glowCollectHooked = true; + } + return xr_new(static_cast(Lights.Create())); +} IRenderVisual* FrameGraphRenderer::model_Create(pcstr name, IReader* data) { return g_pModelPool->Create(name, data); } IRenderVisual* FrameGraphRenderer::model_CreateChild(pcstr name, IReader* data) { return g_pModelPool->CreateChild(name, data); } @@ -3501,13 +4397,14 @@ void FrameGraphRenderer::add_StaticWallmark(IWallMarkArray* pArray, const Fvecto if (!T || T->suppress_wm || !V || s <= EPS_L) return; auto* fgArray = static_cast(pArray); - u32 matID = fgArray->GenerateBindlessMaterialID(); + shared_str wallmarkTexture; + u32 matID = fgArray->GenerateBindlessMaterialID(&wallmarkTexture); if (matID == UINT32_MAX) return; Fvector N; N.mknormal(V[T->verts[0]], V[T->verts[1]], V[T->verts[2]]); float decalSize = s * 2.0f; - GetDecalManager()->AddStaticDecal(P, N, decalSize, matID); + GetDecalManager()->AddStaticDecal(P, N, decalSize, matID, wallmarkTexture.c_str()); } void FrameGraphRenderer::clear_static_wallmarks() @@ -3639,11 +4536,11 @@ IReader* fg_open_shader(pcstr shader) bool ssao_hdao_cs_shaders_exist() { - IReader* hdao_cs = fg_open_shader("ssao_hdao.cs"); - IReader* hdao_cs_msaa = fg_open_shader("ssao_hdao_msaa.cs"); - const bool exist = hdao_cs && hdao_cs_msaa; - FS.r_close(hdao_cs); - FS.r_close(hdao_cs_msaa); + IReader* hdao_ps = fg_open_shader("ao\\hdao.ps"); + if (!hdao_ps) + hdao_ps = fg_open_shader("ao/hdao.ps"); + const bool exist = hdao_ps != nullptr; + FS.r_close(hdao_ps); return exist; } } diff --git a/src/Layers/xrRender/r_FrameGraphRenderer.h b/src/Layers/xrRender/r_FrameGraphRenderer.h index a57578df049..1fb38e0b54a 100644 --- a/src/Layers/xrRender/r_FrameGraphRenderer.h +++ b/src/Layers/xrRender/r_FrameGraphRenderer.h @@ -21,6 +21,7 @@ #include "Layers/xrRender/Geometry/GeometryBatch.h" #include "Layers/xrRender/Profiler/GPUProfiler.h" #include "Layers/xrRender/Profiler/StatsOverlay.h" +#include "Layers/xrRender/FrameGraphPasses/LodPassSetup.h" struct ImDrawData; @@ -251,6 +252,8 @@ class FrameGraphRenderer: public xray::render::fg::FGRenderBase { float gbufferMs = 0.0f; float lightingMs = 0.0f; float tonemapMs = 0.0f; + float fgSetupPassesMs = 0.0f; + float fgCompileMs = 0.0f; u32 numDrawCalls = 0; u32 numTriangles = 0; }; @@ -286,6 +289,9 @@ class FrameGraphRenderer: public xray::render::fg::FGRenderBase { fg::decals::DecalManager* GetDecalManager() const { return m_decalManager.get(); } fg::decals::OverlayManager* GetOverlayManager() const { return m_overlayManager.get(); } + fg::RTAccelStructManager* GetRTAccelMgr() const { return m_rtAccelMgr.get(); } + bool IsRTGIActive() const; + public: struct _options { @@ -550,6 +556,7 @@ class FrameGraphRenderer: public xray::render::fg::FGRenderBase { // HUD geometry (separate from world geometry) xr_vector m_hudBatches; + xr_vector m_lodImpostors; // Particle systems (collected during same spatial query as geometry) xr_vector m_worldParticleBatches; // World-space particles @@ -562,6 +569,8 @@ class FrameGraphRenderer: public xray::render::fg::FGRenderBase { xr_vector m_cachedStaticBatches; xr_vector> m_sectorStaticBatchIds; xr_vector m_sectorCacheReady; + xr_vector m_staticSubmitMark; + u8 m_staticSubmitEpoch = 0; xr_map> m_visualCacheBatchIds; bool m_staticCacheInitialized = false; bool m_portalTraverseActive = false; diff --git a/src/Layers/xrRender/r_FrameGraphRenderer_Loader.cpp b/src/Layers/xrRender/r_FrameGraphRenderer_Loader.cpp index 3f89bb3c4cd..3017685d889 100644 --- a/src/Layers/xrRender/r_FrameGraphRenderer_Loader.cpp +++ b/src/Layers/xrRender/r_FrameGraphRenderer_Loader.cpp @@ -197,53 +197,11 @@ void FrameGraphRenderer::CompileLevelShader(u32 shaderID, const char* shaderName compiled.shaderName = shaderName; compiled.textureName = textureName; - // Use the CRender's ShaderLoader instance - if (!GEnv.Render->GetShaderLoader()) { - Msg("! [ERROR] ShaderLoader not available for shader: %s", shaderName); - return; - } - - // ═══════════════════════════════════════════════════ - // COMPILE VERTEX SHADER - // ═══════════════════════════════════════════════════ - auto vsResult = GEnv.Render->GetShaderLoader()->LoadVertexShader(shaderName, "main"); - - if (vsResult.handle) { - compiled.vsHandle = vsResult.handle; - // Move ownership of reflection data - compiled.vsReflection.reset(vsResult.reflection); - vsResult.reflection = nullptr; // Prevent double deletion - } else { - Msg("! [ERROR] Failed to compile VS for shader: %s", shaderName); - return; - } - - // ═══════════════════════════════════════════════════ - // COMPILE PIXEL SHADER - // ═══════════════════════════════════════════════════ - auto psResult = GEnv.Render->GetShaderLoader()->LoadPixelShader(shaderName, "main"); - - if (psResult.handle) { - compiled.psHandle = psResult.handle; - // Move ownership of reflection data - compiled.psReflection.reset(psResult.reflection); - psResult.reflection = nullptr; // Prevent double deletion - } else { - Msg("! [ERROR] Failed to compile PS for shader: %s", shaderName); - return; - } - - // ═══════════════════════════════════════════════════ - // GET MATERIAL INFO (from MaterialSystem) - // ═══════════════════════════════════════════════════ + // FrameGraph draws level geometry via bindless_forward / VariantPSOCache. + // Level chunk names (default, def_shaders\def_vertex, flora\leaf_wave, …) are + // blender keys — they are not files under shaders/r5/. Keep names + material + // flags for GetCompiledShaderNames / MaterialSystem; do not try to compile them. compiled.materialInfo = MaterialSystem::Instance().GetMaterialInfo(shaderName); - - Msg("* Compiled shader %u: %s (VS=%p, PS=%p, alphaTest=%d, transparent=%d)", - shaderID, shaderName, - compiled.vsHandle.Get(), - compiled.psHandle.Get(), - compiled.materialInfo.alphaTest, - compiled.materialInfo.transparent); } // ═══════════════════════════════════════════════════ @@ -253,80 +211,10 @@ void FrameGraphRenderer::PrecompileLevelPSOs() { ZoneScopedN("Precompile Level PSOs"); - auto* materialCache = GetMaterialCache(); - if (!materialCache) { - Msg("! [ERROR] MaterialCache not available - skipping PSO precompilation"); - return; - } - - // ═══════════════════════════════════════════════════ - // HARDCODE FRAMEBUFFER FORMATS (matches ForwardColorPassSetup) - // ═══════════════════════════════════════════════════ - nvrhi::Format colorFormat = nvrhi::Format::RGBA16_FLOAT; // HDR - nvrhi::Format depthFormat = nvrhi::Format::D32; - - u32 totalPSOs = 0; - - for (u32 shaderID = 0; shaderID < m_CompiledLevelShaders.size(); ++shaderID) { - auto& compiled = m_CompiledLevelShaders[shaderID]; - - if (!compiled.vsHandle || !compiled.psHandle) - continue; // Skip failed compilations - - // Update progress - float progress = float(shaderID) / float(m_CompiledLevelShaders.size()); - g_pGamePersistent->LoadTitle("st_precompiling_pso", progress); - - // ═══════════════════════════════════════════════════ - // FIND COMPATIBLE VERTEX FORMATS - // ═══════════════════════════════════════════════════ - xr_vector compatibleFormats; - for (u32 dcl_id = 0; dcl_id < BufferPool.nDC.size(); ++dcl_id) { - if (IsVertexFormatCompatible(BufferPool.nDC[dcl_id], compiled.vsReflection.get())) { - compatibleFormats.push_back(dcl_id); - } - } - - if (compatibleFormats.empty()) { - Msg("! Shader %u (%s) has no compatible vertex formats!", - shaderID, compiled.shaderName.c_str()); - continue; - } - - // ═══════════════════════════════════════════════════ - // PRECOMPILE PSOs FOR EACH FORMAT + PASS TYPE - // ═══════════════════════════════════════════════════ - for (u32 dcl_id : compatibleFormats) { - // 1. Forward Color PSO (always needed) - if (CreatePrecompiledPSO( - shaderID, - dcl_id, - RenderPassType::ForwardColor, - colorFormat, - depthFormat, - materialCache - )) { - totalPSOs++; - } - - // 2. Depth Prepass PSO (for opaque + alpha-tested) - if (!compiled.materialInfo.transparent) { - if (CreatePrecompiledPSO( - shaderID, - dcl_id, - RenderPassType::DepthPrepass, - nvrhi::Format::UNKNOWN, // No color output - depthFormat, - materialCache - )) { - totalPSOs++; - } - } - } - } - - Msg("* Precompiled %u PSOs for %u shaders across %u vertex formats", - totalPSOs, m_CompiledLevelShaders.size(), BufferPool.nDC.size()); + // Bindless materials create PSOs through VariantPSOCache at first use. + // Per-blender VS/PS handles are intentionally not compiled (see CompileLevelShader). + Msg("* Precompiled 0 PSOs for %u shaders (bindless path — VariantPSOCache)", + m_CompiledLevelShaders.size()); } void FrameGraphRenderer::level_Unload() diff --git a/src/Layers/xrRender/xrRender_console.cpp b/src/Layers/xrRender/xrRender_console.cpp index f85b153c7cf..b0b7fa7a378 100644 --- a/src/Layers/xrRender/xrRender_console.cpp +++ b/src/Layers/xrRender/xrRender_console.cpp @@ -24,11 +24,16 @@ #include "Layers/xrRender/r_FrameGraphRenderer.h" #include "Layers/xrRender/Shaders/SlangCompilerTest.h" #include "Layers/xrRender/PBRConverter/PBRTextureConverter.h" // Phase 2.5.3 +#include "Layers/xrRender/FrameGraphPasses/IBLPrefilterPassSetup.h" #ifdef DEBUG #include "Layers/xrRender/ResourceManager/TestTextureManager.h" #endif #endif +extern ENGINE_API int ps_r_ibl_prefilter; +extern ENGINE_API int ps_r_ibl_probe_auto; +extern ENGINE_API float ps_r_ibl_probe_radius; + // Detail manager debug extern ENGINE_API int dm_debug_trails; @@ -103,8 +108,20 @@ int ps_r_skinned_shadows = 1; // 1=NPCs/mutants cast into the sun CSM (self-shad int ps_r_depth_prepass = 1; // early-Z; enables same-frame Hi-Z path when r_hiz_occlusion int ps_r_hiz_occlusion = 1; // 1=static occlusion compact after same-frame Hi-Z (depth prepass) int ps_r_shadow_light_cull = 1; // 1=CPU light-frustum CSM casters (DRAWINDEX compact) -int ps_r_local_shadows = 1; // 1=spot/OMNIPART local shadow atlas (expensive: N× BuildLightFrustumCasters) -int ps_r_local_shadow_tiles = 12; // max atlas slices (hard cap MAX_LOCAL_SHADOW_TILES=16) +int ps_r_local_shadows = 1; +int ps_r_local_shadow_tiles = 96; +int ps_r_local_shadow_update_div = 1; +int ps_r_local_shadow_redraw_budget = 16; +int ps_r_local_shadow_skinned_max = 24; +float ps_r_local_shadow_near = 10.f; +float ps_r_local_shadow_mid = 30.f; +int ps_r_local_shadow_far_period = 5; +int ps_r_local_shadow_atlas = 2048; +int ps_r_local_shadow_filter = 0; +int ps_r_shadow_hzb = 1; +int ps_r_shadow_mask = 1; +int ps_r_cluster_debug = 0; // 0=off,1=tileXY checker,2=depth slice +int ps_r_cluster_tile_size = 64; // 32 or 64 — cluster XY tile pixels int ps_r_shadow_indoor_near_only = 1; // 1=few portal sectors → only cascade 0 int ps_r_portal_cull = 1; // 1=portal/sector PVS traversal (frustum) for static submit int ps_r_hom = 0; // 1=add CPU HOM occlusion on top of portal traversal (opt-in refinement) @@ -224,6 +241,23 @@ float ps_r2_tonemap_middlegray = 1.f; // r2-only float ps_r2_tonemap_adaptation = 1.f; // r2-only float ps_r2_tonemap_low_lum = 0.0001f; // r2-only float ps_r2_tonemap_amount = 0.7f; // r2-only +float ps_r_exposure_ev_bias = 0.f; +float ps_r_exposure_env_strength = 0.75f; +float ps_r_exposure_env_ref = 1.35f; +float ps_r_exposure_adapt_up = 1.5f; +float ps_r_exposure_adapt_down = 3.0f; +float ps_r_tonemap_white = 4.0f; +float ps_r_tonemap_contrast = 1.0f; +int ps_r_camera = 1; +int ps_r_camera_distort_enable = 1; +int ps_r_camera_ca_enable = 1; +int ps_r_camera_vignette_enable = 1; +int ps_r_camera_grain_enable = 1; +int ps_r_camera_mblur_enable = 1; +float ps_r_camera_distort = 0.04f; +float ps_r_camera_ca = 0.0015f; +float ps_r_camera_vignette = 0.25f; +float ps_r_camera_grain = 0.015f; float ps_r2_ls_bloom_kernel_g = 3.f; // r2-only float ps_r2_ls_bloom_kernel_b = .7f; // r2-only float ps_r2_ls_bloom_speed = 100.f; // r2-only @@ -273,9 +307,9 @@ Fvector3 ps_r2_dof = Fvector3().set(-1.25f, 1.4f, 600.f); float ps_r2_dof_sky = 30; // distance to sky float ps_r2_dof_kernel_size = 5.0f; // 7.0f -float ps_r3_dyn_wet_surf_near = 10.f; // view-space start falloff -float ps_r3_dyn_wet_surf_far = 50.f; // view-space end falloff (was 20 — too short) -int ps_r3_dyn_wet_surf_sm_res = 256; // 256 +float ps_r3_dyn_wet_surf_near = 10.f; +float ps_r3_dyn_wet_surf_far = 50.f; +int ps_r3_dyn_wet_surf_sm_res = 1024; // R4 Debug int ps_r4_debug_gpu_culling = 0; // 0=off, 1=show bounding spheres with cull state colors @@ -712,14 +746,68 @@ class CCC_Dof : public CCC_Vector3 } }; -#ifdef DEBUG +#if RENDER == R_R4 +class CCC_IBLProbeAdd : public IConsole_Command +{ +public: + CCC_IBLProbeAdd(LPCSTR N) : IConsole_Command(N) {} + void Execute(LPCSTR args) override + { + float radius = 8.f; + if (args && args[0]) + radius = std::max(1.f, (float)atof(args)); + auto* st = xray::render::fg::passes::GetActiveIBLState(); + if (!st) + { + Msg("! [IBL] state not ready"); + return; + } + const int idx = xray::render::fg::passes::IBLAddProbeAtCamera(*st, radius); + Msg("* [IBL] probe %d added r=%.1f (captures next frame)", idx, radius); + } +}; + +class CCC_IBLProbeClear : public IConsole_Command +{ +public: + CCC_IBLProbeClear(LPCSTR N) : IConsole_Command(N) {} + void Execute(LPCSTR) override + { + auto* st = xray::render::fg::passes::GetActiveIBLState(); + if (!st) + return; + xray::render::fg::passes::IBLClearProbes(*st); + Msg("* [IBL] probes cleared"); + } +}; + +class CCC_IBLProbeCapture : public IConsole_Command +{ +public: + CCC_IBLProbeCapture(LPCSTR N) : IConsole_Command(N) {} + void Execute(LPCSTR args) override + { + auto* st = xray::render::fg::passes::GetActiveIBLState(); + if (!st || st->probeCount == 0) + { + Msg("! [IBL] no probes"); + return; + } + int idx = int(st->probeCount) - 1; + if (args && args[0]) + idx = std::clamp((int)atoi(args), 0, int(st->probeCount) - 1); + xray::render::fg::passes::IBLRequestCapture(*st, idx); + Msg("* [IBL] capture requested for probe %d", idx); + } +}; +#endif + class CCC_SunshaftsIntensity : public CCC_Float { public: CCC_SunshaftsIntensity(LPCSTR N, float* V, float _min, float _max) : CCC_Float(N, V, _min, _max) {} virtual void Save(IWriter*) { ; } }; -#endif // Allow real-time fog config reload @@ -815,6 +903,23 @@ void xrRender_initconsole() CMD4(CCC_Float, "r2_tonemap_adaptation", &ps_r2_tonemap_adaptation, 0.01f, 10.0f); CMD4(CCC_Float, "r2_tonemap_lowlum", &ps_r2_tonemap_low_lum, 0.0001f, 1.0f); CMD4(CCC_Float, "r2_tonemap_amount", &ps_r2_tonemap_amount, 0.0000f, 1.0f); + CMD4(CCC_Float, "r_exposure_ev_bias", &ps_r_exposure_ev_bias, -4.0f, 4.0f); + CMD4(CCC_Float, "r_exposure_env_strength", &ps_r_exposure_env_strength, 0.0f, 1.0f); + CMD4(CCC_Float, "r_exposure_env_ref", &ps_r_exposure_env_ref, 0.01f, 16.0f); + CMD4(CCC_Float, "r_exposure_adapt_up", &ps_r_exposure_adapt_up, 0.01f, 20.0f); + CMD4(CCC_Float, "r_exposure_adapt_down", &ps_r_exposure_adapt_down, 0.01f, 20.0f); + CMD4(CCC_Float, "r_tonemap_white", &ps_r_tonemap_white, 0.5f, 16.0f); + CMD4(CCC_Float, "r_tonemap_contrast", &ps_r_tonemap_contrast, 0.8f, 1.25f); + CMD4(CCC_Integer, "r_camera", &ps_r_camera, 0, 1); + CMD4(CCC_Integer, "r_camera_distort_enable", &ps_r_camera_distort_enable, 0, 1); + CMD4(CCC_Integer, "r_camera_ca_enable", &ps_r_camera_ca_enable, 0, 1); + CMD4(CCC_Integer, "r_camera_vignette_enable", &ps_r_camera_vignette_enable, 0, 1); + CMD4(CCC_Integer, "r_camera_grain_enable", &ps_r_camera_grain_enable, 0, 1); + CMD4(CCC_Integer, "r_camera_mblur_enable", &ps_r_camera_mblur_enable, 0, 1); + CMD4(CCC_Float, "r_camera_distort", &ps_r_camera_distort, 0.0f, 0.5f); + CMD4(CCC_Float, "r_camera_ca", &ps_r_camera_ca, 0.0f, 0.05f); + CMD4(CCC_Float, "r_camera_vignette", &ps_r_camera_vignette, 0.0f, 2.0f); + CMD4(CCC_Float, "r_camera_grain", &ps_r_camera_grain, 0.0f, 0.2f); CMD4(CCC_Float, "r2_ls_bloom_kernel_scale", &ps_r2_ls_bloom_kernel_scale, 0.5f, 2.f); CMD4(CCC_Float, "r2_ls_bloom_kernel_g", &ps_r2_ls_bloom_kernel_g, 1.f, 7.f); CMD4(CCC_Float, "r2_ls_bloom_kernel_b", &ps_r2_ls_bloom_kernel_b, 0.01f, 1.f); @@ -994,9 +1099,43 @@ void xrRender_initconsole() CMD4(CCC_Integer, "r_path_tracer_bounces", &ps_r_path_tracer_bounces, 1, 16); CMD4(CCC_Integer, "r_rt_gi", &ps_r_rt_gi, 0, 1); CMD4(CCC_Float, "r_rt_gi_intensity", &ps_r_rt_gi_intensity, 0.0f, 4.0f); + CMD4(CCC_Integer, "r_rt_gi_spatial_samples", &ps_r_rt_gi_spatial_samples, 0, 16); + CMD4(CCC_Float, "r_rt_gi_spatial_radius", &ps_r_rt_gi_spatial_radius, 1.0f, 128.0f); + CMD4(CCC_Integer, "r_rt_gi_m_max", &ps_r_rt_gi_m_max, 1, 100); + CMD4(CCC_Integer, "r_rt_gi_local_samples", &ps_r_rt_gi_local_samples, 0, 16); + CMD4(CCC_Integer, "r_rt_di_candidates", &ps_r_rt_di_candidates, 1, 64); + CMD4(CCC_Integer, "r_rt_di_spatial_samples", &ps_r_rt_di_spatial_samples, 0, 16); + CMD4(CCC_Float, "r_rt_di_spatial_radius", &ps_r_rt_di_spatial_radius, 1.0f, 128.0f); + CMD4(CCC_Integer, "r_rt_di_m_max", &ps_r_rt_di_m_max, 1, 100); + CMD4(CCC_Integer, "r_rt_gi_atrous_steps", &ps_r_rt_gi_atrous_steps, 1, 6); + CMD4(CCC_Float, "r_rt_gi_temporal_alpha", &ps_r_rt_gi_temporal_alpha, 0.0f, 0.98f); + CMD4(CCC_Float, "r_rt_gi_ambient_scale", &ps_r_rt_gi_ambient_scale, 0.0f, 1.0f); + CMD4(CCC_Integer, "r_rt_gi_bounces", &ps_r_rt_gi_bounces, 1, 2); + CMD4(CCC_Integer, "r_rt_gi_cache_size", &ps_r_rt_gi_cache_size, 0, 1048576); + CMD4(CCC_Float, "r_rt_gi_cache_cell", &ps_r_rt_gi_cache_cell, 0.05f, 8.0f); + CMD4(CCC_Integer, "r_rt_vol_steps", &ps_r_rt_vol_steps, 0, 64); + CMD4(CCC_Integer, "r_rt_vol_light_samples", &ps_r_rt_vol_light_samples, 0, 8); CMD4(CCC_Integer, "r_taa", &ps_r_taa, 0, 1); CMD4(CCC_Float, "r_taa_sharpness", &ps_r_taa_sharpness, 0.0f, 1.0f); + CMD4(CCC_Integer, "r_taa_jitter", &ps_r_taa_jitter, 0, 1); + CMD4(CCC_Float, "r_render_scale", &ps_r_render_scale, 0.25f, 1.0f); + CMD4(CCC_Integer, "r_upscale", &ps_r_upscale, 0, 2); + CMD4(CCC_Integer, "r_upscale_quality", &ps_r_upscale_quality, 0, 5); + CMD4(CCC_Integer, "r_fsr_fg", &ps_r_fsr_fg, 0, 1); + CMD4(CCC_Float, "r_fsr_sharpness", &ps_r_fsr_sharpness, 0.0f, 1.0f); + CMD4(CCC_Integer, "r_dlss", &ps_r_dlss, 0, 1); + CMD4(CCC_Integer, "r_dlss_quality", &ps_r_dlss_quality, 0, 5); + CMD4(CCC_Integer, "r_dlss_fg", &ps_r_dlss_fg, 0, 1); + CMD4(CCC_Integer, "r_dlss_rr", &ps_r_dlss_rr, 0, 1); + CMD4(CCC_Float, "r_dlss_sharpness", &ps_r_dlss_sharpness, 0.0f, 1.0f); + CMD4(CCC_Integer, "r_dlss_auto_exposure", &ps_r_dlss_auto_exposure, 0, 1); + CMD4(CCC_Integer, "r_reflex", &ps_r_reflex, 0, 2); + CMD4(CCC_Integer, "r_denoise", &ps_r_denoise, 0, 1); + CMD4(CCC_Integer, "r_nrd_method", &ps_r_nrd_method, 0, 1); + CMD4(CCC_Integer, "r_nrd_apply", &ps_r_nrd_apply, 0, 1); + CMD4(CCC_Integer, "r_amd_rr", &ps_r_amd_rr, 0, 1); + CMD4(CCC_Integer, "r_ffx_denoiser", &ps_r_ffx_denoiser, 0, 1); CMD4(CCC_Integer, "r_bloom", &ps_r_bloom, 0, 1); CMD4(CCC_Integer, "r_cas", &ps_r_cas, 0, 1); CMD4(CCC_Float, "r_cas_sharpness", &ps_r_cas_sharpness, 0.0f, 1.0f); @@ -1008,7 +1147,19 @@ void xrRender_initconsole() CMD4(CCC_Integer, "r_hiz_occlusion", &ps_r_hiz_occlusion, 0, 1); CMD4(CCC_Integer, "r_shadow_light_cull", &ps_r_shadow_light_cull, 0, 1); CMD4(CCC_Integer, "r_local_shadows", &ps_r_local_shadows, 0, 1); - CMD4(CCC_Integer, "r_local_shadow_tiles", &ps_r_local_shadow_tiles, 1, 16); + CMD4(CCC_Integer, "r_shadow_hzb", &ps_r_shadow_hzb, 0, 1); + CMD4(CCC_Integer, "r_shadow_mask", &ps_r_shadow_mask, 0, 1); + CMD4(CCC_Integer, "r_local_shadow_tiles", &ps_r_local_shadow_tiles, 1, 512); + CMD4(CCC_Integer, "r_local_shadow_update_div", &ps_r_local_shadow_update_div, 1, 8); + CMD4(CCC_Integer, "r_local_shadow_redraw_budget", &ps_r_local_shadow_redraw_budget, 1, 128); + CMD4(CCC_Integer, "r_local_shadow_skinned_max", &ps_r_local_shadow_skinned_max, 0, 256); + CMD4(CCC_Float, "r_local_shadow_near", &ps_r_local_shadow_near, 1.f, 100.f); + CMD4(CCC_Float, "r_local_shadow_mid", &ps_r_local_shadow_mid, 5.f, 200.f); + CMD4(CCC_Integer, "r_local_shadow_far_period", &ps_r_local_shadow_far_period, 1, 30); + CMD4(CCC_Integer, "r_local_shadow_atlas", &ps_r_local_shadow_atlas, 1024, 8192); + CMD4(CCC_Integer, "r_local_shadow_filter", &ps_r_local_shadow_filter, 0, 1); + CMD4(CCC_Integer, "r_cluster_debug", &ps_r_cluster_debug, 0, 2); + CMD4(CCC_Integer, "r_cluster_tile_size", &ps_r_cluster_tile_size, 32, 64); CMD4(CCC_Integer, "r_shadow_indoor_near_only", &ps_r_shadow_indoor_near_only, 0, 1); CMD4(CCC_Integer, "r_portal_cull", &ps_r_portal_cull, 0, 1); CMD4(CCC_Integer, "r_hom", &ps_r_hom, 0, 1); @@ -1022,11 +1173,23 @@ void xrRender_initconsole() CMD4(CCC_Integer, "r_sky_ibl", &ps_r_sky_ibl, 0, 1); CMD4(CCC_Float, "r_sky_ibl_intensity", &ps_r_sky_ibl_intensity, 0.0f, 4.0f); + CMD4(CCC_Integer, "r_ibl_prefilter", &ps_r_ibl_prefilter, 0, 1); + CMD4(CCC_Integer, "r_ibl_probe_auto", &ps_r_ibl_probe_auto, 0, 1); + CMD4(CCC_Float, "r_ibl_probe_radius", &ps_r_ibl_probe_radius, 2.0f, 64.0f); +#if RENDER == R_R4 + CMD1(CCC_IBLProbeAdd, "r_ibl_probe_add"); + CMD1(CCC_IBLProbeClear, "r_ibl_probe_clear"); + CMD1(CCC_IBLProbeCapture, "r_ibl_probe_capture"); +#endif CMD4(CCC_Integer, "r_foliage_sss", &ps_r_foliage_sss, 0, 1); CMD4(CCC_Float, "r_foliage_sss_intensity", &ps_r_foliage_sss_intensity, 0.0f, 4.0f); CMD4(CCC_Integer, "r_ssgi", &ps_r_ssgi, 0, 1); CMD4(CCC_Integer, "r_ssgi_quality", &ps_r_ssgi_quality, 1, 3); CMD4(CCC_Float, "r_ssgi_intensity", &ps_r_ssgi_intensity, 0.0f, 4.0f); + CMD4(CCC_Integer, "r_sssss", &ps_r_sssss, 0, 1); + CMD4(CCC_Float, "r_sssss_width", &ps_r_sssss_width, 0.1f, 4.0f); + CMD4(CCC_Float, "r_sssss_strength", &ps_r_sssss_strength, 0.0f, 2.0f); + CMD4(CCC_Integer, "r_sssss_debug", &ps_r_sssss_debug, 0, 1); // Smoke Trail (weapon muzzle smoke) CMD4(CCC_Integer, "r_smoke_trail", &ps_r_smoke_trail_enabled, 0, 1); diff --git a/src/Layers/xrRender/xrRender_console.h b/src/Layers/xrRender/xrRender_console.h index 24c6cd60137..fd16753d1bf 100644 --- a/src/Layers/xrRender/xrRender_console.h +++ b/src/Layers/xrRender/xrRender_console.h @@ -36,7 +36,19 @@ extern ECORE_API int ps_r_depth_prepass; // 1=opaque depth prepass for early-Z i extern ECORE_API int ps_r_hiz_occlusion; // 1=Hi-Z occlusion in GPU cull, 0=frustum only extern ECORE_API int ps_r_shadow_light_cull; // 1=light-frustum CSM caster cull extern ECORE_API int ps_r_local_shadows; // 1=spot/omni local shadow atlas -extern ECORE_API int ps_r_local_shadow_tiles; // max atlas slices (1..16) +extern ECORE_API int ps_r_local_shadow_tiles; +extern ECORE_API int ps_r_local_shadow_update_div; // period multiplier for soft refresh tiers +extern ECORE_API int ps_r_local_shadow_redraw_budget; // max soft tiles redrawn per frame +extern ECORE_API int ps_r_local_shadow_skinned_max; +extern ECORE_API float ps_r_local_shadow_near; // full-rate distance (m) +extern ECORE_API float ps_r_local_shadow_mid; // mid-rate distance (m) +extern ECORE_API int ps_r_local_shadow_far_period; // soft refresh period beyond mid +extern ECORE_API int ps_r_local_shadow_atlas; // atlas edge: 1024/2048/4096/8192 +extern ECORE_API int ps_r_local_shadow_filter; // 0=PCF depth, 1=ESM +extern ECORE_API int ps_r_shadow_hzb; // 1=per-cascade Shadow-HZB for hierarchical CSM +extern ECORE_API int ps_r_shadow_mask; // 1=half-res screen shadow mask (sun/contact/local) +extern ECORE_API int ps_r_cluster_debug; // 0=off,1=tileXY,2=slice heatmap +extern ECORE_API int ps_r_cluster_tile_size; // 32 or 64 (applied next grid rebuild) extern ECORE_API int ps_r_shadow_indoor_near_only; // 1=indoor portal sets use cascade 0 only extern ECORE_API int ps_r_portal_cull; // 1=portal/sector visibility filter extern ECORE_API int ps_r_hom; // 1=CPU HOM occlusion on top of portal traversal @@ -128,6 +140,23 @@ extern ECORE_API float ps_r2_tonemap_middlegray; // r2-only extern ECORE_API float ps_r2_tonemap_adaptation; // r2-only extern ECORE_API float ps_r2_tonemap_low_lum; // r2-only extern ECORE_API float ps_r2_tonemap_amount; // r2-only +extern ECORE_API float ps_r_exposure_ev_bias; +extern ECORE_API float ps_r_exposure_env_strength; +extern ECORE_API float ps_r_exposure_env_ref; +extern ECORE_API float ps_r_exposure_adapt_up; +extern ECORE_API float ps_r_exposure_adapt_down; +extern ECORE_API float ps_r_tonemap_white; +extern ECORE_API float ps_r_tonemap_contrast; +extern ECORE_API int ps_r_camera; +extern ECORE_API int ps_r_camera_distort_enable; +extern ECORE_API int ps_r_camera_ca_enable; +extern ECORE_API int ps_r_camera_vignette_enable; +extern ECORE_API int ps_r_camera_grain_enable; +extern ECORE_API int ps_r_camera_mblur_enable; +extern ECORE_API float ps_r_camera_distort; +extern ECORE_API float ps_r_camera_ca; +extern ECORE_API float ps_r_camera_vignette; +extern ECORE_API float ps_r_camera_grain; extern ECORE_API float ps_r2_ls_bloom_kernel_scale; // r2-only // gauss extern ECORE_API float ps_r2_ls_bloom_kernel_g; // r2-only // gauss extern ECORE_API float ps_r2_ls_bloom_kernel_b; // r2-only // bilinear diff --git a/src/xrCore/FS.cpp b/src/xrCore/FS.cpp index 6a7fd7cd6a1..987614c3c2c 100644 --- a/src/xrCore/FS.cpp +++ b/src/xrCore/FS.cpp @@ -75,7 +75,10 @@ void VerifyPath(pcstr path) string1024 tmp; for (int i = 0; path[i]; i++) { - if (path[i] != _DELIMITER || i == 0) + // Engine paths use '\'; callers may also pass '/'. Split on both so nested + // cache dirs like shaders_cache_fg/vk/ao/ssao.ps create intermediates on POSIX. + const char c = path[i]; + if ((c != _DELIMITER && c != '/') || i == 0) continue; CopyMemory(tmp, path, i); tmp[i] = 0; diff --git a/src/xrCore/Profiler/Profiler.cpp b/src/xrCore/Profiler/Profiler.cpp index ecacaad2a67..a3d9a2005bb 100644 --- a/src/xrCore/Profiler/Profiler.cpp +++ b/src/xrCore/Profiler/Profiler.cpp @@ -42,6 +42,9 @@ void FrameEnd() return; CPUProfiler::Instance().FrameEnd(); +#if XRAY_TRACY_ENABLED + FrameMark; +#endif } CPUProfiler& GetCPUProfiler() diff --git a/src/xrCore/log.cpp b/src/xrCore/log.cpp index 98f886d4041..145b5863894 100644 --- a/src/xrCore/log.cpp +++ b/src/xrCore/log.cpp @@ -34,8 +34,11 @@ void AddOne(pcstr split) { ScopeLock scope{ &logCS }; - OutputDebugString(split); - OutputDebugString("\n"); + if (!(LogExecCB && LogCB)) + { + OutputDebugString(split); + OutputDebugString("\n"); + } LogFile.push_back(split); diff --git a/src/xrEngine/Environment_editor.cpp b/src/xrEngine/Environment_editor.cpp index d52c6c2ccce..e95ab88129b 100644 --- a/src/xrEngine/Environment_editor.cpp +++ b/src/xrEngine/Environment_editor.cpp @@ -7,6 +7,7 @@ #include "IGame_Level.h" #include "editor_helper.h" +#include "xr_ioc_cmd.h" // Phase 5: Grass wind tuning parameters (defined here in xrEngine, externed in render DLL) ENGINE_API float ps_r3_grass_wind_multiplier = 1.0f; // Multiplier for environment wind strength @@ -20,8 +21,9 @@ ENGINE_API u32 ps_r3_grass_wind_octaves = 5; // FBM octave count ENGINE_API Fvector3 ps_r3_grass_color_tip = {0.35f, 0.45f, 0.18f}; // Blade tip color (vibrant green) ENGINE_API Fvector3 ps_r3_grass_color_base = {0.28f, 0.38f, 0.15f}; // Blade base color (duller brown-green) ENGINE_API float ps_r3_grass_color_variation = 0.15f; // Per-blade color variation (±%) -ENGINE_API Fvector3 ps_r3_grass_sss_color = {0.55f, 0.85f, 0.25f}; // Subsurface scattering tint -ENGINE_API float ps_r3_grass_sss_intensity = 0.9f; // SSS strength (CoP billboards) +// Tint multiplies albedo in EvaluateFoliageSSS — keep near-white so blade color comes from texture. +ENGINE_API Fvector3 ps_r3_grass_sss_color = {1.0f, 1.0f, 1.0f}; +// Intensity: use r_foliage_sss / r_foliage_sss_intensity (shared with trees/leaves) // Per-object-ID color tints (64 grass types max) // Default all to white (1,1,1) = no tint @@ -597,11 +599,18 @@ void CEnvironment::on_tool_frame() ImGui::SeparatorText("Subsurface Scattering"); + { + bool foliageSssOn = (ps_r_foliage_sss != 0); + if (ImGui::Checkbox("Foliage SSS (r_foliage_sss)", &foliageSssOn)) + ps_r_foliage_sss = foliageSssOn ? 1 : 0; + ItemHelp("Shared enable for grass, leaves, bushes (console: r_foliage_sss)"); + } + ImGui::ColorEdit3("SSS color", reinterpret_cast(&ps_r3_grass_sss_color)); - ItemHelp("Color of light transmitted through grass blades (backlit effect)"); + ItemHelp("Optional grass tint (trees use albedo). White = texture color only"); - ImGui::DragFloat("SSS intensity", &ps_r3_grass_sss_intensity, 0.01f, 0.0f, 1.0f); - ItemHelp("Strength of subsurface scattering effect"); + ImGui::DragFloat("SSS intensity (r_foliage_sss_intensity)", &ps_r_foliage_sss_intensity, 0.01f, 0.0f, 4.0f); + ItemHelp("Shared strength for grass + foliage (console: r_foliage_sss_intensity)"); ImGui::SeparatorText("Per-Object Tint"); diff --git a/src/xrEngine/IRenderBackend.h b/src/xrEngine/IRenderBackend.h index 5a4028380fb..9bc340c0ca5 100644 --- a/src/xrEngine/IRenderBackend.h +++ b/src/xrEngine/IRenderBackend.h @@ -53,6 +53,8 @@ class ENGINE_API IRenderBackend virtual bool IsInitialized() const = 0; virtual void Shutdown() = 0; virtual void WaitForIdle() = 0; + virtual void LockDevice() {} + virtual void UnlockDevice() {} virtual DeviceState GetDeviceState() const { return DeviceState::Normal; } // ═══════ NVRHI Access ═══════ @@ -80,9 +82,26 @@ class ENGINE_API IRenderBackend virtual u32 GetCurrentBackBufferIndex() const { return 0; } virtual u32 GetBackBufferCount() const { return 1; } virtual void Present(bool vsync) = 0; + virtual bool PresentFrameGeneration(nvrhi::ITexture* interpolated, nvrhi::ITexture* real) { (void)interpolated; (void)real; return false; } virtual std::pair GetBackBufferSize() const = 0; virtual void ResizeSwapChain(u32 width, u32 height) {} + enum class LatencyMarker : u32 + { + SimulationStart = 0, + SimulationEnd, + RenderSubmitStart, + RenderSubmitEnd, + PresentStart, + PresentEnd, + OutOfBandPresentStart, + OutOfBandPresentEnd, + }; + virtual bool IsLowLatencyAvailable() const { return false; } + virtual void ApplyLowLatencyMode(int mode, u32 minIntervalUs) { (void)mode; (void)minIntervalUs; } + virtual void LatencySleep() {} + virtual void SetLatencyMarker(LatencyMarker marker) { (void)marker; } + // ═══════ Frame Sync ═══════ virtual void BeginFrame() = 0; virtual void EndFrame() = 0; diff --git a/src/xrEngine/device.cpp b/src/xrEngine/device.cpp index 9209a45d2f8..ddec48fc7fd 100644 --- a/src/xrEngine/device.cpp +++ b/src/xrEngine/device.cpp @@ -8,6 +8,7 @@ #include "XR_IOConsole.h" #include "xr_input.h" +#include "xr_ioc_cmd.h" #include "IGame_Level.h" #include "IGame_Persistent.h" @@ -64,6 +65,11 @@ bool CRenderDevice::RenderBegin() GEnv.Render->Begin(); } + // Vulkan may fail swapchain acquire (OUT_OF_DATE) without opening a command list. + // Abort this frame; GetDeviceState() will request NeedReset on the next attempt. + if (GEnv.Backend && !GEnv.Backend->IsInFrame()) + return false; + g_bRendering = true; return true; } @@ -305,7 +311,15 @@ void CRenderDevice::ProcessFrame() const u64 frameStartNs = SDL_GetTicksNS(); + const bool reflexPacing = GEnv.Backend && GEnv.Backend->IsLowLatencyAvailable(); + if (reflexPacing) + GEnv.Backend->LatencySleep(); + + if (GEnv.Backend) + GEnv.Backend->SetLatencyMarker(IRenderBackend::LatencyMarker::SimulationStart); FrameMove(); + if (GEnv.Backend) + GEnv.Backend->SetLatencyMarker(IRenderBackend::LatencyMarker::SimulationEnd); OnCameraUpdated(); @@ -318,7 +332,11 @@ void CRenderDevice::ProcessFrame() seqFrameMT.Process(); }); + if (GEnv.Backend) + GEnv.Backend->SetLatencyMarker(IRenderBackend::LatencyMarker::RenderSubmitStart); DoRender(); + if (GEnv.Backend) + GEnv.Backend->SetLatencyMarker(IRenderBackend::LatencyMarker::RenderSubmitEnd); TaskScheduler->Wait(processSeqParallel); @@ -328,7 +346,9 @@ void CRenderDevice::ProcessFrame() else if (Paused() || g_pGameLevel == nullptr) fpsCap = ps_fps_limit_in_menu; - if (fpsCap > 0) + const bool skipAppFpsCap = reflexPacing && !GEnv.isDedicatedServer && + (ps_r_reflex != 0 || fpsCap > 0); + if (fpsCap > 0 && !skipAppFpsCap) { const u64 targetFrameNs = 1'000'000'000ull / static_cast(fpsCap); const u64 deadlineNs = frameStartNs + targetFrameNs; diff --git a/src/xrEngine/xr_ioc_cmd.cpp b/src/xrEngine/xr_ioc_cmd.cpp index d31dadf968e..27539a75d72 100644 --- a/src/xrEngine/xr_ioc_cmd.cpp +++ b/src/xrEngine/xr_ioc_cmd.cpp @@ -26,8 +26,42 @@ ENGINE_API int ps_r_path_tracer = 0; ENGINE_API int ps_r_path_tracer_bounces = 8; ENGINE_API int ps_r_rt_gi = 0; ENGINE_API float ps_r_rt_gi_intensity = 1.0f; +ENGINE_API int ps_r_rt_gi_spatial_samples = 8; +ENGINE_API float ps_r_rt_gi_spatial_radius = 40.0f; +ENGINE_API int ps_r_rt_gi_m_max = 8; +ENGINE_API int ps_r_rt_gi_local_samples = 16; +ENGINE_API int ps_r_rt_di_candidates = 12; +ENGINE_API int ps_r_rt_di_spatial_samples = 6; +ENGINE_API float ps_r_rt_di_spatial_radius = 32.0f; +ENGINE_API int ps_r_rt_di_m_max = 20; +ENGINE_API int ps_r_rt_gi_atrous_steps = 6; +ENGINE_API float ps_r_rt_gi_temporal_alpha = 0.96f; +ENGINE_API float ps_r_rt_gi_ambient_scale = 0.65f; +ENGINE_API int ps_r_rt_gi_bounces = 2; +ENGINE_API int ps_r_rt_gi_cache_size = 262144; +ENGINE_API float ps_r_rt_gi_cache_cell = 0.75f; +ENGINE_API int ps_r_rt_vol_steps = 16; +ENGINE_API int ps_r_rt_vol_light_samples = 2; ENGINE_API int ps_r_taa = 1; ENGINE_API float ps_r_taa_sharpness = 0.65f; +ENGINE_API int ps_r_taa_jitter = 1; +ENGINE_API float ps_r_render_scale = 1.0f; +ENGINE_API int ps_r_upscale = 0; +ENGINE_API int ps_r_upscale_quality = 3; +ENGINE_API int ps_r_fsr_fg = 0; +ENGINE_API float ps_r_fsr_sharpness = 0.2f; +ENGINE_API int ps_r_dlss = 0; +ENGINE_API int ps_r_dlss_quality = 3; +ENGINE_API int ps_r_dlss_fg = 0; +ENGINE_API int ps_r_dlss_rr = 0; +ENGINE_API float ps_r_dlss_sharpness = 0.0f; +ENGINE_API int ps_r_dlss_auto_exposure = 1; +ENGINE_API int ps_r_reflex = 1; +ENGINE_API int ps_r_denoise = 1; +ENGINE_API int ps_r_nrd_method = 0; +ENGINE_API int ps_r_nrd_apply = 1; +ENGINE_API int ps_r_amd_rr = 1; +ENGINE_API int ps_r_ffx_denoiser = 1; ENGINE_API int ps_r_bloom = 1; ENGINE_API int ps_r_cas = 1; ENGINE_API float ps_r_cas_sharpness = 0.6f; @@ -38,12 +72,19 @@ ENGINE_API int ps_r_ssr_quality = 2; ENGINE_API int ps_r_ssr_debug = 0; // 0=off 1=weight 2=mirrorUV 3=confHeat ENGINE_API int ps_r_sky_ibl = 1; -ENGINE_API float ps_r_sky_ibl_intensity = 1.0f; +ENGINE_API float ps_r_sky_ibl_intensity = 0.55f; +ENGINE_API int ps_r_ibl_prefilter = 1; +ENGINE_API int ps_r_ibl_probe_auto = 1; +ENGINE_API float ps_r_ibl_probe_radius = 10.f; ENGINE_API int ps_r_foliage_sss = 1; ENGINE_API float ps_r_foliage_sss_intensity = 0.45f; ENGINE_API int ps_r_ssgi = 1; ENGINE_API int ps_r_ssgi_quality = 2; ENGINE_API float ps_r_ssgi_intensity = 0.65f; +ENGINE_API int ps_r_sssss = 1; +ENGINE_API float ps_r_sssss_width = 1.35f; +ENGINE_API float ps_r_sssss_strength = 1.0f; +ENGINE_API int ps_r_sssss_debug = 0; ENGINE_API Fvector4 ps_dev_param_1 = {0, 0, 0, 0}; ENGINE_API Fvector4 ps_dev_param_2 = {0, 0, 0, 0}; ENGINE_API Fvector4 ps_dev_param_3 = {0, 0, 0, 0}; diff --git a/src/xrEngine/xr_ioc_cmd.h b/src/xrEngine/xr_ioc_cmd.h index 8353d12ec2f..3aba01d1552 100644 --- a/src/xrEngine/xr_ioc_cmd.h +++ b/src/xrEngine/xr_ioc_cmd.h @@ -35,8 +35,42 @@ extern ENGINE_API int ps_r_path_tracer; extern ENGINE_API int ps_r_path_tracer_bounces; extern ENGINE_API int ps_r_rt_gi; extern ENGINE_API float ps_r_rt_gi_intensity; +extern ENGINE_API int ps_r_rt_gi_spatial_samples; +extern ENGINE_API float ps_r_rt_gi_spatial_radius; +extern ENGINE_API int ps_r_rt_gi_m_max; +extern ENGINE_API int ps_r_rt_gi_local_samples; +extern ENGINE_API int ps_r_rt_di_candidates; +extern ENGINE_API int ps_r_rt_di_spatial_samples; +extern ENGINE_API float ps_r_rt_di_spatial_radius; +extern ENGINE_API int ps_r_rt_di_m_max; +extern ENGINE_API int ps_r_rt_gi_atrous_steps; +extern ENGINE_API float ps_r_rt_gi_temporal_alpha; +extern ENGINE_API float ps_r_rt_gi_ambient_scale; +extern ENGINE_API int ps_r_rt_gi_bounces; +extern ENGINE_API int ps_r_rt_gi_cache_size; +extern ENGINE_API float ps_r_rt_gi_cache_cell; +extern ENGINE_API int ps_r_rt_vol_steps; +extern ENGINE_API int ps_r_rt_vol_light_samples; extern ENGINE_API int ps_r_taa; extern ENGINE_API float ps_r_taa_sharpness; +extern ENGINE_API int ps_r_taa_jitter; +extern ENGINE_API float ps_r_render_scale; +extern ENGINE_API int ps_r_upscale; +extern ENGINE_API int ps_r_upscale_quality; +extern ENGINE_API int ps_r_fsr_fg; +extern ENGINE_API float ps_r_fsr_sharpness; +extern ENGINE_API int ps_r_dlss; +extern ENGINE_API int ps_r_dlss_quality; +extern ENGINE_API int ps_r_dlss_fg; +extern ENGINE_API int ps_r_dlss_rr; +extern ENGINE_API float ps_r_dlss_sharpness; +extern ENGINE_API int ps_r_dlss_auto_exposure; +extern ENGINE_API int ps_r_reflex; +extern ENGINE_API int ps_r_denoise; +extern ENGINE_API int ps_r_nrd_method; +extern ENGINE_API int ps_r_nrd_apply; +extern ENGINE_API int ps_r_amd_rr; +extern ENGINE_API int ps_r_ffx_denoiser; extern ENGINE_API int ps_r_bloom; extern ENGINE_API int ps_r_cas; extern ENGINE_API float ps_r_cas_sharpness; @@ -47,11 +81,18 @@ extern ENGINE_API int ps_r_ssr_quality; extern ENGINE_API int ps_r_ssr_debug; extern ENGINE_API int ps_r_sky_ibl; extern ENGINE_API float ps_r_sky_ibl_intensity; +extern ENGINE_API int ps_r_ibl_prefilter; +extern ENGINE_API int ps_r_ibl_probe_auto; +extern ENGINE_API float ps_r_ibl_probe_radius; extern ENGINE_API int ps_r_foliage_sss; extern ENGINE_API float ps_r_foliage_sss_intensity; extern ENGINE_API int ps_r_ssgi; extern ENGINE_API int ps_r_ssgi_quality; extern ENGINE_API float ps_r_ssgi_intensity; +extern ENGINE_API int ps_r_sssss; +extern ENGINE_API float ps_r_sssss_width; +extern ENGINE_API float ps_r_sssss_strength; +extern ENGINE_API int ps_r_sssss_debug; extern ENGINE_API Fvector4 ps_dev_param_1; extern ENGINE_API Fvector4 ps_dev_param_2; extern ENGINE_API Fvector4 ps_dev_param_3; diff --git a/src/xrGame/GamePersistent.cpp b/src/xrGame/GamePersistent.cpp index c9f6ca9ec68..9b77106b3a0 100644 --- a/src/xrGame/GamePersistent.cpp +++ b/src/xrGame/GamePersistent.cpp @@ -126,9 +126,6 @@ void CGamePersistent::OnAppStart() inherited::OnAppStart(); - if (GEnv.Render) - GEnv.Render->ConvertLegacyAssetsToPBR(); - #ifdef XR_PLATFORM_WINDOWS ansel = xr_new(); ansel->Load(); diff --git a/src/xrGame/Torch.cpp b/src/xrGame/Torch.cpp index 4c95e06960c..05ec179f960 100644 --- a/src/xrGame/Torch.cpp +++ b/src/xrGame/Torch.cpp @@ -18,6 +18,8 @@ #include "ActorEffector.h" #include "CustomOutfit.h" #include "ActorHelmet.h" +#include "xrEngine/IGame_Persistent.h" +#include "xrEngine/ShadersExternalData.h" constexpr pcstr TORCH_DEFINITION = "torch_definition"; static const float TORCH_INERTION_CLAMP = PI_DIV_6; @@ -39,8 +41,10 @@ CTorch::CTorch() light_render->set_type(IRender_Light::SPOT); light_render->set_shadow(true); + light_render->set_hud_mode(true); light_omni->set_type(IRender_Light::POINT); light_omni->set_shadow(false); + light_omni->set_hud_mode(true); // Disabling shift by x and z axes for 1st render, // because we don't have dynamic lighting in it. @@ -514,6 +518,8 @@ void CNightVisionEffector::Start(const shared_str& sect, CActor* pA, bool play_s { m_pActor = pA; AddEffector(m_pActor, effNightvision, sect); + if (g_pGamePersistent && g_pGamePersistent->m_pGShaderConstants) + g_pGamePersistent->m_pGShaderConstants->m_blender_mode.x = 1.f; if (play_sound) { PlaySounds(eStartSound); @@ -535,6 +541,8 @@ void CNightVisionEffector::Stop(const float factor, bool play_sound) m_sounds.StopSound("NightVisionOnSnd"); m_sounds.StopSound("NightVisionIdleSnd"); } + if (g_pGamePersistent && g_pGamePersistent->m_pGShaderConstants) + g_pGamePersistent->m_pGShaderConstants->m_blender_mode.x = 0.f; } bool CNightVisionEffector::IsActive() diff --git a/src/xrGame/Weapon.cpp b/src/xrGame/Weapon.cpp index fc3397846de..826647027f6 100644 --- a/src/xrGame/Weapon.cpp +++ b/src/xrGame/Weapon.cpp @@ -28,6 +28,7 @@ #include "xrNetServer/NET_Messages.h" #include "xrCore/xr_token.h" #include "GamePersistent.h" +#include "xrEngine/ShadersExternalData.h" #define WEAPON_REMOVE_TIME 60000 #define ROTATION_TIME 0.25f @@ -1776,6 +1777,16 @@ void CWeapon::UpdateHudAdditonal(Fmatrix& trans) clamp(m_zoom_params.m_fZoomRotationFactor, 0.f, 1.f); } + + if (g_pGamePersistent && g_pGamePersistent->m_pGShaderConstants) + { + const float zoom = m_zoom_params.m_fZoomRotationFactor; + g_pGamePersistent->m_pGShaderConstants->hud_params.set( + zoom, + IsZoomed() ? zoom : 0.f, + 0.015f, + 1.f); + } } void CWeapon::SetAmmoElapsed(int ammo_count) diff --git a/src/xr_3da/CMakeLists.txt b/src/xr_3da/CMakeLists.txt index 8ca0e2abbd9..21a005da1ad 100644 --- a/src/xr_3da/CMakeLists.txt +++ b/src/xr_3da/CMakeLists.txt @@ -44,3 +44,17 @@ set_target_properties(xr_3da PROPERTIES install(TARGETS xr_3da RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" ) + +if(XRAY_USE_DLSS AND XRAY_DLSS_RUNTIME_DIR AND EXISTS "${XRAY_DLSS_RUNTIME_DIR}") + add_custom_command(TARGET xr_3da POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${XRAY_DLSS_RUNTIME_DIR}" + "$" + COMMAND ${CMAKE_COMMAND} -E chdir "$" + ${CMAKE_COMMAND} -E create_symlink libnvidia-ngx-dlss.so.310.5.3 libnvidia-ngx-dlss.so + COMMAND ${CMAKE_COMMAND} -E chdir "$" + ${CMAKE_COMMAND} -E create_symlink libnvidia-ngx-dlssd.so.310.5.3 libnvidia-ngx-dlssd.so + COMMAND ${CMAKE_COMMAND} -E chdir "$" + ${CMAKE_COMMAND} -E create_symlink libnvidia-ngx-dlssg.so.310.5.3 libnvidia-ngx-dlssg.so + COMMENT "Copy DLSS runtime libraries next to xr_3da") +endif() From ca7623712dd8af9fd1c879117d6938e0a69a5471 Mon Sep 17 00:00:00 2001 From: FoxConED Date: Wed, 29 Jul 2026 10:30:14 +0300 Subject: [PATCH 249/261] xrCore: fix GCC16 SDL_arraysize/_Countof build break --- src/xrCore/xrDebug.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xrCore/xrDebug.cpp b/src/xrCore/xrDebug.cpp index ae27b46039e..eaa3a48500a 100644 --- a/src/xrCore/xrDebug.cpp +++ b/src/xrCore/xrDebug.cpp @@ -83,7 +83,7 @@ AssertionResult xrDebug::ShowMessage(pcstr title, pcstr message, bool simpleMode { SDL_MESSAGEBOX_ERROR, windowHandler ? windowHandler->GetApplicationWindow() : nullptr, - title, message, SDL_arraysize(buttons), buttons, nullptr + title, message, (int)(sizeof(buttons) / sizeof(buttons[0])), buttons, nullptr }; int button = -1; @@ -289,7 +289,7 @@ AssertionResult xrDebug::Fail(bool& ignoreAlways, const ErrorLocation& loc, cons // we must hide the window if (windowHandler && !DebuggerIsPresent()) windowHandler->OnFatalError(); - DEBUG_BREAK; + DEBUG_BREAK; } // switch (result) } From 8ac08d0973e75be49b966d4329d715df2440a63f Mon Sep 17 00:00:00 2001 From: FoxConED Date: Wed, 29 Jul 2026 10:31:10 +0300 Subject: [PATCH 250/261] xrEngine: drop duplicate Thunderbolt ED_ShowParams after merge --- src/xrEngine/Environment_editor.cpp | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/xrEngine/Environment_editor.cpp b/src/xrEngine/Environment_editor.cpp index e83f3c90b62..bdad19f244b 100644 --- a/src/xrEngine/Environment_editor.cpp +++ b/src/xrEngine/Environment_editor.cpp @@ -304,31 +304,6 @@ void CEnvDescriptorMixer::ed_show_params(const CEnvironment& env) } } -void CEffect_Thunderbolt::ED_ShowParams() -{ - using namespace xray::imgui; - - float altitude[2] = { rad2deg(p_var_alt.x) , rad2deg(p_var_alt.y) }; - if (ImGui::DragFloat2("altitude", altitude, 0.5f, -360.0f, 360.f)) - p_var_alt = { rad2deg(altitude[0]) , rad2deg(altitude[1]) }; - - float deltalongitude = rad2deg(p_var_long); - if (ImGui::DragFloat("wind direction", &deltalongitude, 0.5f, -360.0f, 360.f)) - p_var_long = deg2rad(deltalongitude); - - ImGui::DragFloat("minimum distance factor", &p_min_dist, 0.001f, 0.0f, MAX_DIST_FACTOR); - ItemHelp("Distance from far plane"); - - float tilt = rad2deg(p_tilt); - if (ImGui::DragFloat("tilt", &tilt, 0.01f, 15.0f, 30.f)) - p_tilt = deg2rad(tilt); - - ImGui::DragFloat("second probability", &p_second_prop, 0.001f, 0.0f, 1.0f); - ImGui::DragFloat("sky color", &p_sky_color, 0.001f, 0.0f, 1.0f); - ImGui::DragFloat("sun color", &p_sun_color, 0.001f, 0.0f, 1.0f); - ImGui::DragFloat("fog color", &p_fog_color, 0.001f, 0.0f, 1.0f); -} - void CEnvironment::on_tool_frame() { #ifdef MASTER_GOLD From 68e4069e28a6ee338544a32462bc96e7711ace8e Mon Sep 17 00:00:00 2001 From: FoxConED Date: Wed, 29 Jul 2026 10:38:36 +0300 Subject: [PATCH 251/261] xrRender: restore lighting stack after OpenXRay FG merge --- .../xrRender/Denoising/NRDDenoiseBackend.cpp | 18 +- src/Layers/xrRender/FrameGraph/IPass.h | 1 + .../FrameGraphPasses/DecalPassSetup.cpp | 139 +- .../FrameGraphPasses/DetailPassSetup.cpp | 243 +- .../FrameGraphPasses/DetailPassSetup.h | 26 +- .../DistortionApplyPassSetup.cpp | 30 +- .../DistortionApplyPassSetup.h | 3 +- .../ForwardColorPassSetup.cpp | 381 ++- .../FrameGraphPasses/ForwardColorPassSetup.h | 2 + .../FrameGraphPasses/ParticlePassSetup.cpp | 3 +- .../FrameGraphPasses/ReSTIRGIPassSetup.h | 3 +- .../FrameGraphPasses/SkinningPassSetup.cpp | 839 ++++++- .../FrameGraphPasses/SkinningPassSetup.h | 102 +- .../FrameGraphPasses/TransparentPassSetup.cpp | 591 ++++- .../FrameGraphPasses/TransparentPassSetup.h | 30 + src/Layers/xrRender/GPUCullingManager.cpp | 2036 +++++++++++++++-- src/Layers/xrRender/GPUCullingManager.h | 163 +- .../ShaderVariant/VariantPSOCache.cpp | 224 +- .../xrRender/ShaderVariant/VariantPSOCache.h | 21 +- .../xrRender/Upscaling/DlssFgPassSetup.cpp | 10 +- .../xrRender/Upscaling/UpscalePassSetup.cpp | 6 +- src/Layers/xrRender/fgRainRender.cpp | 352 ++- src/Layers/xrRender/r_FrameGraphRenderer.h | 1 + src/Layers/xrRender/xrRender_console.cpp | 1 + src/Layers/xrRender/xrRender_console.h | 1 + 25 files changed, 4616 insertions(+), 610 deletions(-) diff --git a/src/Layers/xrRender/Denoising/NRDDenoiseBackend.cpp b/src/Layers/xrRender/Denoising/NRDDenoiseBackend.cpp index 691ac8379fc..f78fb29f380 100644 --- a/src/Layers/xrRender/Denoising/NRDDenoiseBackend.cpp +++ b/src/Layers/xrRender/Denoising/NRDDenoiseBackend.cpp @@ -57,7 +57,7 @@ struct NrdCompositeCB float fogColor[4]; }; -void CopyMatrix(float dst[16], const Fmatrix& m) +void NrdCopyMatrix(float dst[16], const Fmatrix& m) { dst[0] = m._11; dst[1] = m._12; dst[2] = m._13; dst[3] = m._14; dst[4] = m._21; dst[5] = m._22; dst[6] = m._23; dst[7] = m._24; @@ -424,10 +424,10 @@ class NRDDenoiseBackend final : public IDenoiseBackend worldToClipPrev.mul(Device.mProjectSaved, Device.mViewSaved); NrdPackCB packCB{}; - CopyMatrix(packCB.worldToView, Device.mView); - CopyMatrix(packCB.worldToViewPrev, Device.mViewSaved); - CopyMatrix(packCB.worldToClip, worldToClip); - CopyMatrix(packCB.worldToClipPrev, worldToClipPrev); + NrdCopyMatrix(packCB.worldToView, Device.mView); + NrdCopyMatrix(packCB.worldToViewPrev, Device.mViewSaved); + NrdCopyMatrix(packCB.worldToClip, worldToClip); + NrdCopyMatrix(packCB.worldToClipPrev, worldToClipPrev); if (inputs.worldToView[0] != 0.f || inputs.worldToView[5] != 0.f || inputs.worldToView[10] != 0.f) memcpy(packCB.worldToView, inputs.worldToView, sizeof(packCB.worldToView)); if (inputs.worldToViewPrev[0] != 0.f || inputs.worldToViewPrev[5] != 0.f || inputs.worldToViewPrev[10] != 0.f) @@ -534,10 +534,10 @@ class NRDDenoiseBackend final : public IDenoiseBackend m_nrdFrameIndex = 0; nrd::CommonSettings common{}; - CopyMatrix(common.viewToClipMatrix, Device.mProject); - CopyMatrix(common.viewToClipMatrixPrev, Device.mProjectSaved); - CopyMatrix(common.worldToViewMatrix, Device.mView); - CopyMatrix(common.worldToViewMatrixPrev, Device.mViewSaved); + NrdCopyMatrix(common.viewToClipMatrix, Device.mProject); + NrdCopyMatrix(common.viewToClipMatrixPrev, Device.mProjectSaved); + NrdCopyMatrix(common.worldToViewMatrix, Device.mView); + NrdCopyMatrix(common.worldToViewMatrixPrev, Device.mViewSaved); common.motionVectorScale[0] = 1.f / (float)w; common.motionVectorScale[1] = 1.f / (float)h; diff --git a/src/Layers/xrRender/FrameGraph/IPass.h b/src/Layers/xrRender/FrameGraph/IPass.h index 5e675f9e7f0..5c4a4a9af6b 100644 --- a/src/Layers/xrRender/FrameGraph/IPass.h +++ b/src/Layers/xrRender/FrameGraph/IPass.h @@ -24,6 +24,7 @@ struct DefaultOutputLayout { VirtualResourceHandle albedo; // RT0: Lit HDR color (RGBA16_FLOAT) VirtualResourceHandle normal; // RT1: World normal.xyz + Roughness.a (RGBA16_FLOAT) VirtualResourceHandle baseColor; // RT2: Unlit diffuse albedo.rgb + Metallic.a (RGBA8_UNORM) + VirtualResourceHandle worldPos; // RT3: World position.xyz (RGBA32_FLOAT) VirtualResourceHandle depth; // Depth/Stencil (D32) VirtualResourceHandle distortion; // Distortion buffer (RG = UV offset, A = intensity) }; diff --git a/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.cpp index e7640120eb3..2d0aff4968a 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "DecalPassSetup.h" +#include "PassCommon.h" #include "ShaderConstants.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/IPass.h" @@ -25,6 +26,7 @@ struct DecalPassData { VirtualResourceHandle depth; VirtualResourceHandle normal; VirtualResourceHandle sceneColor; + VirtualResourceHandle worldPos; fg::RenderDevice* device; decals::DecalManager* decalMgr; DecalPassState* passState; @@ -56,33 +58,49 @@ static void InitializeDecalResources(fg::RenderDevice* device, const nvrhi::Fram state.inputLayout = nvDevice->createInputLayout(&posAttr, 1, state.vs); state.bindingLayout = cache.GetOrCreateBindingLayoutFromReflection( - "Decal", *vsResult.reflection, *psResult.reflection, nvDevice); - - nvrhi::GraphicsPipelineDesc pipeDesc; - pipeDesc.setVertexShader(state.vs); - pipeDesc.setPixelShader(state.ps); - pipeDesc.setInputLayout(state.inputLayout); - pipeDesc.addBindingLayout(state.bindingLayout); - - if (GEnv.Backend && GEnv.Backend->GetBindlessLayout()) - pipeDesc.addBindingLayout(GEnv.Backend->GetBindlessLayout()); - - pipeDesc.setPrimType(nvrhi::PrimitiveType::TriangleList); - pipeDesc.renderState.depthStencilState.setDepthTestEnable(false); - pipeDesc.renderState.depthStencilState.setDepthWriteEnable(false); - pipeDesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::Front); - - auto& blend = pipeDesc.renderState.blendState.targets[0]; - blend.setBlendEnable(true); - blend.setSrcBlend(nvrhi::BlendFactor::SrcAlpha); - blend.setDestBlend(nvrhi::BlendFactor::InvSrcAlpha); - blend.setBlendOp(nvrhi::BlendOp::Add); - blend.setSrcBlendAlpha(nvrhi::BlendFactor::One); - blend.setDestBlendAlpha(nvrhi::BlendFactor::InvSrcAlpha); - blend.setBlendOpAlpha(nvrhi::BlendOp::Add); - - state.pipeline = cache.GetOrCreatePipeline("Decal", pipeDesc, fbInfo, nvDevice); - state.initialized = state.pipeline != nullptr; + "Decal_v2", *vsResult.reflection, *psResult.reflection, nvDevice); + + auto makePipe = [&](const char* name, bool multiply) -> nvrhi::GraphicsPipelineHandle { + nvrhi::GraphicsPipelineDesc pipeDesc; + pipeDesc.setVertexShader(state.vs); + pipeDesc.setPixelShader(state.ps); + pipeDesc.setInputLayout(state.inputLayout); + pipeDesc.addBindingLayout(state.bindingLayout); + + if (GEnv.Backend && GEnv.Backend->GetBindlessLayout()) + pipeDesc.addBindingLayout(GEnv.Backend->GetBindlessLayout()); + + pipeDesc.setPrimType(nvrhi::PrimitiveType::TriangleList); + pipeDesc.renderState.depthStencilState.setDepthTestEnable(false); + pipeDesc.renderState.depthStencilState.setDepthWriteEnable(false); + pipeDesc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::Front); + + auto& blend = pipeDesc.renderState.blendState.targets[0]; + blend.setBlendEnable(true); + if (multiply) + { + blend.setSrcBlend(nvrhi::BlendFactor::DstColor); + blend.setDestBlend(nvrhi::BlendFactor::SrcColor); + blend.setBlendOp(nvrhi::BlendOp::Add); + blend.setSrcBlendAlpha(nvrhi::BlendFactor::Zero); + blend.setDestBlendAlpha(nvrhi::BlendFactor::One); + blend.setBlendOpAlpha(nvrhi::BlendOp::Add); + } + else + { + blend.setSrcBlend(nvrhi::BlendFactor::SrcAlpha); + blend.setDestBlend(nvrhi::BlendFactor::InvSrcAlpha); + blend.setBlendOp(nvrhi::BlendOp::Add); + blend.setSrcBlendAlpha(nvrhi::BlendFactor::One); + blend.setDestBlendAlpha(nvrhi::BlendFactor::InvSrcAlpha); + blend.setBlendOpAlpha(nvrhi::BlendOp::Add); + } + return cache.GetOrCreatePipeline(name, pipeDesc, fbInfo, nvDevice); + }; + + state.multiplyPipeline = makePipe("DecalMult_v2", true); + state.alphaPipeline = makePipe("DecalAlpha_v2", false); + state.initialized = state.multiplyPipeline != nullptr && state.alphaPipeline != nullptr; } DefaultOutputLayout setupDecalPass( @@ -110,6 +128,7 @@ DefaultOutputLayout setupDecalPass( data.depth = passBuilder.read(inputs.depth, ResourceState::DepthStencilRead); data.normal = passBuilder.read(inputs.normal, ResourceState::ShaderResource); data.sceneColor = passBuilder.readWrite(inputs.albedo, ResourceState::RenderTarget); + data.worldPos = passBuilder.read(inputs.worldPos, ResourceState::ShaderResource); }, [](const DecalPassData& data, const FrameGraph& fg, fg::RenderContext* ctx) { @@ -119,7 +138,8 @@ DefaultOutputLayout setupDecalPass( auto* depthTex = fg.GetPhysicalTexture(data.depth); auto* normalTex = fg.GetPhysicalTexture(data.normal); auto* colorTex = fg.GetPhysicalTexture(data.sceneColor); - if (!depthTex || !normalTex || !colorTex) + auto* worldPosTex = fg.GetPhysicalTexture(data.worldPos); + if (!depthTex || !normalTex || !colorTex || !worldPosTex) return; data.decalMgr->Upload(ctx); @@ -137,21 +157,15 @@ DefaultOutputLayout setupDecalPass( return; auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } auto* vsReflection = GEnv.Render->GetShaderLoader()->GetCachedReflection("decal_box", ".vs"); auto* psReflection = GEnv.Render->GetShaderLoader()->GetCachedReflection("decal_box", ".ps"); - auto* materialBuffer = MaterialBuffer::Instance().GetBuffer(); - - framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Decal"); - bsb.ConstantBuffer("static_globals", staticGlobalsCB); - bsb.BufferSRV("g_Decals", data.decalMgr->GetDecalBuffer()); - bsb.Texture("g_Depth", depthTex); - bsb.Texture("g_Normal", normalTex); - bsb.BufferSRV("g_Materials", materialBuffer); - - auto bindingSet = cache.GetOrCreateBindingSet( - bsb.Build(), data.passState->bindingLayout, nvDevice); + auto drawCB = cache.GetOrCreateVolatileCB("Frame", "DecalDrawCB", 16, data.device); nvrhi::Viewport viewport; viewport.minX = 0; @@ -161,26 +175,45 @@ DefaultOutputLayout setupDecalPass( viewport.minZ = 0.0f; viewport.maxZ = 1.0f; - nvrhi::GraphicsState gfxState; - gfxState.pipeline = data.passState->pipeline; - gfxState.framebuffer = framebuffer; - gfxState.viewport.addViewportAndScissorRect(viewport); - gfxState.addBindingSet(bindingSet); - auto* backend = data.device->GetBackend(); nvrhi::IDescriptorTable* bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; - if (bindlessTable) - gfxState.addBindingSet(bindlessTable); - - gfxState.vertexBuffers = { { data.decalMgr->GetCubeVB(), 0, 0 } }; - gfxState.indexBuffer = { data.decalMgr->GetCubeIB(), nvrhi::Format::R16_UINT, 0 }; - - cmdList->setGraphicsState(gfxState); nvrhi::DrawArguments args; args.vertexCount = decals::CUBE_INDEX_COUNT; args.instanceCount = data.decalMgr->GetActiveCount(); - cmdList->drawIndexed(args); + + auto drawWith = [&](nvrhi::IGraphicsPipeline* pipeline, u32 wantMultiply) { + if (!pipeline) + return; + u32 cbData[4] = { wantMultiply, 0, 0, 0 }; + cmdList->writeBuffer(drawCB, cbData, sizeof(cbData)); + + framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Decal"); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.ConstantBuffer("DecalDrawCB", drawCB); + bsb.BufferSRV("g_Decals", data.decalMgr->GetDecalBuffer()); + bsb.Texture("g_Depth", depthTex); + bsb.Texture("g_WorldPos", worldPosTex); + BindBindlessMaterialTables(bsb); + + auto bindingSet = cache.GetOrCreateBindingSet( + bsb.Build(), data.passState->bindingLayout, nvDevice); + + nvrhi::GraphicsState gfxState; + gfxState.pipeline = pipeline; + gfxState.framebuffer = framebuffer; + gfxState.viewport.addViewportAndScissorRect(viewport); + gfxState.addBindingSet(bindingSet); + if (bindlessTable) + gfxState.addBindingSet(bindlessTable); + gfxState.vertexBuffers = { { data.decalMgr->GetCubeVB(), 0, 0 } }; + gfxState.indexBuffer = { data.decalMgr->GetCubeIB(), nvrhi::Format::R16_UINT, 0 }; + cmdList->setGraphicsState(gfxState); + cmdList->drawIndexed(args); + }; + + drawWith(data.passState->multiplyPipeline, 1); + drawWith(data.passState->alphaPipeline, 0); } ); diff --git a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp index 48e775c44a9..d041e25f01c 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp @@ -2,6 +2,7 @@ #include "stdafx.h" #include "PassCommon.h" #include "DetailPassSetup.h" +#include "IBLPrefilterPassSetup.h" #include "ShaderConstants.h" #include "Layers/xrRender/FGDetailManager.h" #include "Layers/xrRender/FrameGraph/IPass.h" @@ -13,7 +14,11 @@ #include "Layers/xrRender/FrameGraph/PassResourceCache.h" #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" #include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/xrRender_console.h" #include "Layers/xrRender/ClusteredLightManager.h" +#include "Layers/xrRender/Bindless/MaterialBuffer.h" +#include "Layers/xrRender/Bindless/TerrainMaterialBuffer.h" +#include "Layers/xrRender/Bindless/VariantTextureBuffer.h" // Detail rendering console variables extern ENGINE_API float ps_r__Detail_l_aniso; @@ -30,8 +35,8 @@ extern ENGINE_API Fvector3 ps_r3_grass_color_tip; extern ENGINE_API Fvector3 ps_r3_grass_color_base; extern ENGINE_API float ps_r3_grass_color_variation; extern ENGINE_API Fvector3 ps_r3_grass_sss_color; -extern ENGINE_API float ps_r3_grass_sss_intensity; extern ENGINE_API Fvector3 ps_r3_grass_object_tints[64]; +// Grass SSS intensity uses shared r_foliage_sss / r_foliage_sss_intensity (ShaderConstants.h) // Grass blade geometry parameters (defined in xrEngine) extern ENGINE_API float ps_r3_grass_blade_width; @@ -53,23 +58,57 @@ DefaultOutputLayout setupDetailPass( const DefaultOutputLayout& forwardInputs, u32 width, u32 height, - xray::profiler::GPUProfiler* gpuProfiler + xray::profiler::GPUProfiler* gpuProfiler, + nvrhi::ITexture* shadowMapArray, + VirtualResourceHandle shadowMapHandle, + nvrhi::ITexture* contactDepth, + nvrhi::ITexture* contactHistory + , VirtualResourceHandle perlinReady + , nvrhi::ITexture* const* shadowCascades + , nvrhi::ITexture* localShadowAtlas + , nvrhi::ITexture* localShadowESM ) { - if (detailManager && !detailManager->graphicsPipeline) + if (detailManager) { - nvrhi::FramebufferInfo fbInfo; - fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); - fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); - fbInfo.colorFormats.push_back(nvrhi::Format::RGBA8_UNORM); - fbInfo.colorFormats.push_back(nvrhi::Format::RGBA32_FLOAT); - fbInfo.depthFormat = nvrhi::Format::D32; - detailManager->CreateGraphicsPipeline(device, fbInfo); + static u32 s_detailPipeEpoch = 0; + constexpr u32 kDetailPipeEpoch = 4; + if (s_detailPipeEpoch != kDetailPipeEpoch) + { + detailManager->graphicsPipeline = nullptr; + detailManager->decalGraphicsPipeline = nullptr; + detailManager->billboardGraphicsPipeline = nullptr; + detailManager->graphicsBindingLayout = nullptr; + detailManager->decalBindingLayout = nullptr; + detailManager->billboardBindingLayout = nullptr; + s_detailPipeEpoch = kDetailPipeEpoch; + } + } + if (detailManager && (!detailManager->graphicsPipeline || !detailManager->decalGraphicsPipeline)) + { + if (detailManager->graphicsPipeline && !detailManager->decalGraphicsPipeline) + detailManager->graphicsPipeline = nullptr; + // Shaders must be loaded before PSO creation (else "invalid parameters") + if (!detailManager->vertexShader || !detailManager->pixelShader) + { + if (auto* loader = GEnv.Render->GetShaderLoader()) + detailManager->LoadGraphicsShaders(loader); + } + if (detailManager->vertexShader && detailManager->pixelShader) + { + nvrhi::FramebufferInfo fbInfo; + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA8_UNORM); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA32_FLOAT); + fbInfo.depthFormat = nvrhi::Format::D32; + detailManager->CreateGraphicsPipeline(device, fbInfo); + } } auto& passData = fg.addCallbackPass( "DetailDraw", - [&, width, height, gpuProfiler]( + [&, width, height, gpuProfiler, shadowMapArray, shadowMapHandle, contactDepth, contactHistory, perlinReady, shadowCascades, localShadowAtlas, localShadowESM]( FrameGraph& builder, PassHandle passHandle, DetailPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); @@ -78,6 +117,13 @@ DefaultOutputLayout setupDetailPass( data.device = device; data.detailManager = detailManager; data.gpuProfiler = gpuProfiler; + data.shadowMapArray = shadowMapArray; + for (int i = 0; i < 3; ++i) + data.shadowCascades[i] = (shadowCascades && shadowCascades[i]) ? shadowCascades[i] : shadowMapArray; + data.localShadowAtlas = localShadowAtlas; + data.localShadowESM = localShadowESM; + data.contactDepth = contactDepth; + data.contactHistory = contactHistory; data.inputColor = passBuilder.read(forwardInputs.albedo); data.depth = passBuilder.readWrite(forwardInputs.depth, ResourceState::DepthStencilWrite); @@ -85,10 +131,17 @@ DefaultOutputLayout setupDetailPass( data.outputNormal = passBuilder.readWrite(forwardInputs.normal, ResourceState::RenderTarget); if (forwardInputs.baseColor.is_valid()) data.baseColor = passBuilder.readWrite(forwardInputs.baseColor, ResourceState::RenderTarget); + if (forwardInputs.worldPos.is_valid()) + data.worldPos = passBuilder.readWrite(forwardInputs.worldPos, ResourceState::RenderTarget); + if (shadowMapHandle.is_valid()) + data.shadowMap = passBuilder.read(shadowMapHandle, ResourceState::ShaderResource); + if (perlinReady.is_valid()) + passBuilder.read(perlinReady); data.outputs.albedo = data.outputColor; data.outputs.normal = data.outputNormal; data.outputs.baseColor = data.baseColor; + data.outputs.worldPos = data.worldPos; data.outputs.depth = data.depth; }, [](const DetailPassData& data, const FrameGraph& fg, fg::RenderContext* ctx) @@ -130,6 +183,7 @@ DefaultOutputLayout setupDetailPass( nvrhi::ITexture* normalTexture = fg.GetPhysicalTexture(data.outputNormal); auto* baseColorRT = data.baseColor.is_valid() ? fg.GetPhysicalTexture(data.baseColor) : nullptr; + auto* worldPosRT = data.worldPos.is_valid() ? fg.GetPhysicalTexture(data.worldPos) : nullptr; nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(colorTexture); @@ -137,9 +191,12 @@ DefaultOutputLayout setupDetailPass( fbDesc.addColorAttachment(normalTexture); if (baseColorRT) fbDesc.addColorAttachment(baseColorRT); + if (worldPosRT) + fbDesc.addColorAttachment(worldPosRT); fbDesc.setDepthAttachment(depthTexture); - nvrhi::FramebufferHandle framebuffer = data.device->GetNVRHIDevice()->createFramebuffer(fbDesc); + auto framebuffer = framegraph::GetPassResourceCache().GetOrCreateFramebuffer( + "DetailDraw", fbDesc, data.device->GetNVRHIDevice()); if (!framebuffer) return; @@ -151,6 +208,10 @@ DefaultOutputLayout setupDetailPass( auto& cache = framegraph::GetPassResourceCache(); auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), renderDevice); + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } auto detailGlobalsCB = cache.GetOrCreateVolatileCB("Detail", "DetailGlobals", sizeof(FGDetailManager::DetailFrameConstants), renderDevice); auto dynLightCB = cache.GetOrCreateVolatileCB("Detail", "DynLight", 48, renderDevice); @@ -177,11 +238,17 @@ DefaultOutputLayout setupDetailPass( frameConstants.perlin4d_texture_index = dm->perlin4dBindlessIndex; frameConstants.grass_color_tip.set(ps_r3_grass_color_tip.x, ps_r3_grass_color_tip.y, ps_r3_grass_color_tip.z, 0.0f); frameConstants.grass_color_base.set(ps_r3_grass_color_base.x, ps_r3_grass_color_base.y, ps_r3_grass_color_base.z, 0.0f); - frameConstants.grass_sss_color.set(ps_r3_grass_sss_color.x, ps_r3_grass_sss_color.y, ps_r3_grass_sss_color.z, ps_r3_grass_sss_intensity); + // Same enable/intensity as trees/leaves (r_foliage_sss*, parallax.z path) + const float grassSss = + (ps_r_foliage_sss != 0) ? ps_r_foliage_sss_intensity : 0.0f; + frameConstants.grass_sss_color.set( + ps_r3_grass_sss_color.x, ps_r3_grass_sss_color.y, ps_r3_grass_sss_color.z, grassSss); frameConstants.grass_color_variation = ps_r3_grass_color_variation; frameConstants.grass_blade_height = ps_r3_grass_blade_height; frameConstants.buildDetailsIndex = dm->buildDetailsBindlessIndex; frameConstants.buildDetailsPbrIndex = dm->buildDetailsPbrBindlessIndex; + frameConstants.grassVeinIndex = dm->grassVeinBindlessIndex; + frameConstants.pad0 = frameConstants.pad1 = frameConstants.pad2 = 0; cmdList->writeBuffer(detailGlobalsCB, &frameConstants, sizeof(frameConstants)); u8 dummyLight[48] = {}; @@ -209,27 +276,76 @@ DefaultOutputLayout setupDetailPass( data.gpuProfiler->BeginPass(cmdList, "Details.Draw"); auto* nvDev = data.device->GetNVRHIDevice(); + nvrhi::ITexture* dummy2D = cache.GetDummyShadowMap2D(nvDev); + + auto bindShadowMap = [&](framegraph::BindingSetBuilder& bsb) { + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = data.shadowCascades[i] ? data.shadowCascades[i] + : (i == 0 && data.shadowMapArray ? data.shadowMapArray : dummy2D); + if (t) + bsb.Texture(kNames[i], t); + } + nvrhi::ITexture* contactHist = data.contactHistory + ? data.contactHistory + : cache.GetDummyContactHistory(nvDev); + if (contactHist) + bsb.Texture("g_ContactHistory", contactHist); + nvrhi::ITexture* localAtlas = data.localShadowAtlas + ? data.localShadowAtlas + : cache.GetDummyShadowMap(nvDev); + if (localAtlas) + bsb.Texture("g_LocalShadowAtlas", localAtlas); + nvrhi::ITexture* localEsm = data.localShadowESM + ? data.localShadowESM + : cache.GetDummyLocalShadowESM(nvDev); + if (localEsm) + bsb.Texture("g_LocalShadowESM", localEsm); + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + nvrhi::ITexture* dummyHzb = cache.GetDummyContactDepth(nvDev); + for (u32 i = 0; i < 3; ++i) + { + if (dummyHzb) + bsb.Texture(kHzb[i], dummyHzb); + } + }; auto* shaderLoader = GEnv.Render->GetShaderLoader(); auto* grassVsRefl = shaderLoader->GetCachedReflection("detail_gpu", ".vs"); auto* grassPsRefl = shaderLoader->GetCachedReflection("detail_gpu", ".ps"); + // detail_*.ps includes bindless_common.h → reflection keeps g_Materials / + // g_TerrainMaterials / g_VariantTextures (t8/t9/t10) even when unused. + auto bindBindlessMaterialTables = [&](framegraph::BindingSetBuilder& bsb) { + BindBindlessMaterialTables(bsb); + }; + auto makeGrassBindingSet = [&](nvrhi::BufferHandle visibleIndicesBuffer) { framegraph::BindingSetBuilder bsb(*grassVsRefl, *grassPsRefl, nvDev, "Detail.Grass"); bsb.ConstantBuffer("static_globals", staticGlobalsCB); bsb.ConstantBuffer("DetailGlobals", detailGlobalsCB); bsb.ConstantBuffer("$Globals", dynLightCB); + bindBindlessMaterialTables(bsb); bsb.BufferSRV("visible_indices", visibleIndicesBuffer); bsb.BufferSRV("grass_object_tints", dm->cachedGrassTintsBuffer); bsb.BufferSRV("all_instances", dm->generatedInstancesBuffer); bsb.BufferSRV("slot_data", dm->slotDataBuffer); + bsb.BufferSRV("slot_indirection", dm->cachedDummySlotIndirection); bsb.Texture("g_Perlin4D", dm->perlin4dTexture); bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); + if (ClusteredLightManager::Instance().GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", ClusteredLightManager::Instance().GetShadowDataBuffer()); bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); - auto bindDesc = bsb.Build(); - bindDesc.bindings.push_back(nvrhi::BindingSetItem::TypedBuffer_SRV(32, dm->cachedDummySlotIndirection)); - return cache.GetOrCreateBindingSet(bindDesc, dm->graphicsBindingLayout, nvDev); + bindShadowMap(bsb); + const auto& ibl = GetCurrentIBLBindResources(); + nvrhi::ITexture* sky0 = ibl.spec0 ? ibl.spec0 : cache.GetDummyCubeMap(nvDev); + nvrhi::ITexture* sky1 = ibl.spec1 ? ibl.spec1 : sky0; + if (sky0) bsb.Texture("s_env0", sky0); + if (sky1) bsb.Texture("s_env1", sky1); + BindIBLResources(bsb, ibl, nvDev); + return cache.GetOrCreateBindingSet(bsb.Build(), dm->graphicsBindingLayout, nvDev); }; auto* bbVsRefl = shaderLoader->GetCachedReflection("detail_billboard", ".vs"); @@ -239,6 +355,7 @@ DefaultOutputLayout setupDetailPass( framegraph::BindingSetBuilder bsb(*bbVsRefl, *bbPsRefl, nvDev, "Detail.Billboard"); bsb.ConstantBuffer("static_globals", staticGlobalsCB); bsb.ConstantBuffer("DetailGlobals", detailGlobalsCB); + bindBindlessMaterialTables(bsb); bsb.BufferSRV("visible_indices", visibleIndicesBuffer); bsb.BufferSRV("detail_models", dm->detailModelsBuffer); bsb.BufferSRV("pulled_vertices", dm->pulledVertexBuffer); @@ -246,8 +363,17 @@ DefaultOutputLayout setupDetailPass( bsb.BufferSRV("slot_data", dm->slotDataBuffer); bsb.Texture("g_Perlin4D", dm->perlin4dTexture); bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); + if (ClusteredLightManager::Instance().GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", ClusteredLightManager::Instance().GetShadowDataBuffer()); bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); + bindShadowMap(bsb); + const auto& ibl = GetCurrentIBLBindResources(); + nvrhi::ITexture* sky0 = ibl.spec0 ? ibl.spec0 : cache.GetDummyCubeMap(nvDev); + nvrhi::ITexture* sky1 = ibl.spec1 ? ibl.spec1 : sky0; + if (sky0) bsb.Texture("s_env0", sky0); + if (sky1) bsb.Texture("s_env1", sky1); + BindIBLResources(bsb, ibl, nvDev); return cache.GetOrCreateBindingSet(bsb.Build(), layout, nvDev); }; @@ -257,25 +383,46 @@ DefaultOutputLayout setupDetailPass( dm->billboardDrawArgsBuffer && dm->pulledIndexBuffer && dm->maxPulledIndexCount > 0) { nvrhi::BindingSetHandle bbBindingSet = makePulledBindingSet(dm->visibleBillboardInstancesBuffer, dm->billboardBindingLayout); + if (!bbBindingSet) + { + static bool s_logged = false; + if (!s_logged) + { + Msg("! [DetailPass] Billboard binding set failed (missing t8/t9/t10 or CSM)"); + s_logged = true; + } + } + else + { + nvrhi::GraphicsState state; + state.framebuffer = framebuffer; + state.viewport.addViewportAndScissorRect(nvrhi::Viewport((float)data.width, (float)data.height)); + state.pipeline = dm->billboardGraphicsPipeline; + state.bindings = { bbBindingSet }; + if (bindlessTable) + state.addBindingSet(bindlessTable); + state.indexBuffer = { dm->pulledIndexBuffer, nvrhi::Format::R16_UINT, 0 }; + state.indirectParams = dm->billboardDrawArgsBuffer; - nvrhi::GraphicsState state; - state.framebuffer = framebuffer; - state.viewport.addViewportAndScissorRect(nvrhi::Viewport((float)data.width, (float)data.height)); - state.pipeline = dm->billboardGraphicsPipeline; - state.bindings = { bbBindingSet }; - if (bindlessTable) - state.addBindingSet(bindlessTable); - state.indexBuffer = { dm->pulledIndexBuffer, nvrhi::Format::R16_UINT, 0 }; - state.indirectParams = dm->billboardDrawArgsBuffer; - - cmdList->setGraphicsState(state); - cmdList->drawIndexedIndirect(0); + cmdList->setGraphicsState(state); + cmdList->drawIndexedIndirect(0); + } } - else + else if (!billboardMode) { for (u32 lod = 0; lod < FGDetailManager::LOD_COUNT; lod++) { nvrhi::BindingSetHandle bindingSet = makeGrassBindingSet(dm->visibleInstancesBuffer[lod]); + if (!bindingSet) + { + static bool s_logged = false; + if (!s_logged) + { + Msg("! [DetailPass] Grass binding set failed (layout/SRV mismatch — check t50/t51)"); + s_logged = true; + } + continue; + } nvrhi::GraphicsState state; state.framebuffer = framebuffer; @@ -300,28 +447,41 @@ DefaultOutputLayout setupDetailPass( framegraph::BindingSetBuilder decalBsb(*decalVsRefl, *decalPsRefl, nvDev, "Detail.Decal"); decalBsb.ConstantBuffer("static_globals", staticGlobalsCB); decalBsb.ConstantBuffer("DetailGlobals", detailGlobalsCB); + bindBindlessMaterialTables(decalBsb); decalBsb.BufferSRV("visible_indices", dm->visibleDecalInstancesBuffer); decalBsb.BufferSRV("detail_models", dm->detailModelsBuffer); decalBsb.BufferSRV("decal_vertices", dm->pulledVertexBuffer); decalBsb.BufferSRV("all_instances", dm->generatedInstancesBuffer); decalBsb.BufferSRV("slot_data", dm->slotDataBuffer); decalBsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); + if (ClusteredLightManager::Instance().GetShadowDataBuffer()) + decalBsb.BufferSRV("g_ShadowData", ClusteredLightManager::Instance().GetShadowDataBuffer()); decalBsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); decalBsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); + bindShadowMap(decalBsb); + const auto& ibl = GetCurrentIBLBindResources(); + nvrhi::ITexture* sky0 = ibl.spec0 ? ibl.spec0 : cache.GetDummyCubeMap(nvDev); + nvrhi::ITexture* sky1 = ibl.spec1 ? ibl.spec1 : sky0; + if (sky0) decalBsb.Texture("s_env0", sky0); + if (sky1) decalBsb.Texture("s_env1", sky1); + BindIBLResources(decalBsb, ibl, nvDev); nvrhi::BindingSetHandle decalBindingSet = cache.GetOrCreateBindingSet(decalBsb.Build(), dm->decalBindingLayout, nvDev); - nvrhi::GraphicsState state; - state.framebuffer = framebuffer; - state.viewport.addViewportAndScissorRect(nvrhi::Viewport((float)data.width, (float)data.height)); - state.pipeline = dm->decalGraphicsPipeline; - state.bindings = { decalBindingSet }; - if (bindlessTable) - state.addBindingSet(bindlessTable); - state.indexBuffer = { dm->pulledIndexBuffer, nvrhi::Format::R16_UINT, 0 }; - state.indirectParams = dm->decalDrawArgsBuffer; - - cmdList->setGraphicsState(state); - cmdList->drawIndexedIndirect(0); + if (decalBindingSet) + { + nvrhi::GraphicsState state; + state.framebuffer = framebuffer; + state.viewport.addViewportAndScissorRect(nvrhi::Viewport((float)data.width, (float)data.height)); + state.pipeline = dm->decalGraphicsPipeline; + state.bindings = { decalBindingSet }; + if (bindlessTable) + state.addBindingSet(bindlessTable); + state.indexBuffer = { dm->pulledIndexBuffer, nvrhi::Format::R16_UINT, 0 }; + state.indirectParams = dm->decalDrawArgsBuffer; + + cmdList->setGraphicsState(state); + cmdList->drawIndexedIndirect(0); + } } if (data.gpuProfiler) @@ -333,6 +493,7 @@ DefaultOutputLayout setupDetailPass( outputs.albedo = passData.outputColor; outputs.normal = passData.outputNormal; outputs.baseColor = passData.baseColor; + outputs.worldPos = passData.worldPos; outputs.depth = passData.depth; return outputs; } diff --git a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.h index 08241ed449c..2947a4789ed 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.h @@ -40,6 +40,14 @@ struct DetailPassData { framegraph::VirtualResourceHandle outputColor; framegraph::VirtualResourceHandle outputNormal; framegraph::VirtualResourceHandle baseColor; + framegraph::VirtualResourceHandle worldPos; + framegraph::VirtualResourceHandle shadowMap; + nvrhi::ITexture* shadowMapArray = nullptr; + nvrhi::ITexture* shadowCascades[3] = {}; + nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* localShadowESM = nullptr; + nvrhi::ITexture* contactDepth = nullptr; + nvrhi::ITexture* contactHistory = nullptr; fg::RenderDevice* device; fg::FGDetailManager* detailManager; framegraph::DefaultOutputLayout outputs; @@ -47,13 +55,7 @@ struct DetailPassData { u32 height; xray::profiler::GPUProfiler* gpuProfiler; }; -// Lambda-based detail pass setup with GPU culling support -// Renders detail objects (grass, vegetation) using: -// - GPU compute culling (frustum + Hi-Z occlusion) -// - Single unified draw call via DrawIndexedInstancedIndirect -// - Interactive grass system (wind + entity interactions) -// - Virtual texturing for interaction atlas -// Renders AFTER forward color pass (details on top of world geometry) + framegraph::DefaultOutputLayout setupDetailPass( framegraph::FrameGraph& fg, fg::RenderDevice* device, @@ -61,7 +63,15 @@ framegraph::DefaultOutputLayout setupDetailPass( const framegraph::DefaultOutputLayout& forwardInputs, u32 width, u32 height, - xray::profiler::GPUProfiler* gpuProfiler = nullptr + xray::profiler::GPUProfiler* gpuProfiler = nullptr, + nvrhi::ITexture* shadowMapArray = nullptr, + framegraph::VirtualResourceHandle shadowMapHandle = {}, + nvrhi::ITexture* contactDepth = nullptr, + nvrhi::ITexture* contactHistory = nullptr, + framegraph::VirtualResourceHandle perlinReady = {}, + nvrhi::ITexture* const* shadowCascades = nullptr, + nvrhi::ITexture* localShadowAtlas = nullptr, + nvrhi::ITexture* localShadowESM = nullptr ); } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.cpp index c5a94072717..3050b7accdf 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.cpp @@ -20,7 +20,8 @@ using namespace framegraph; struct DistortionApplyData { VirtualResourceHandle sceneInput; VirtualResourceHandle distortionInput; - VirtualResourceHandle depthInput; + VirtualResourceHandle worldPosInput; + VirtualResourceHandle baseColorInput; VirtualResourceHandle output; u32 width; u32 height; @@ -43,7 +44,7 @@ void InitializeDistortionApplyPass(nvrhi::IDevice* device, DistortionApplyPassSt auto& cache = GetPassResourceCache(); state.bindingLayout = cache.GetOrCreateBindingLayoutFromReflection( - "DistortionApply", *vsResult.reflection, *psResult.reflection, device); + "DistortionApply_v8_HudDilate", *vsResult.reflection, *psResult.reflection, device); if (state.bindingLayout) { nvrhi::GraphicsPipelineDesc pipeDesc; @@ -59,7 +60,7 @@ void InitializeDistortionApplyPass(nvrhi::IDevice* device, DistortionApplyPassSt nvrhi::FramebufferInfoEx fbInfo; fbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); - state.pipeline = cache.GetOrCreatePipeline("DistortionApply", pipeDesc, fbInfo, device); + state.pipeline = cache.GetOrCreatePipeline("DistortionApply_v8_HudDilate", pipeDesc, fbInfo, device); } state.initialized = true; } @@ -69,7 +70,8 @@ VirtualResourceHandle setupDistortionApplyPass( fg::RenderDevice* device, VirtualResourceHandle sceneColor, VirtualResourceHandle distortionRT, - VirtualResourceHandle depth, + VirtualResourceHandle worldPos, + VirtualResourceHandle baseColor, u32 width, u32 height, DistortionApplyPassState& passState) @@ -77,6 +79,9 @@ VirtualResourceHandle setupDistortionApplyPass( if (device && device->GetNVRHIDevice()) InitializeDistortionApplyPass(device->GetNVRHIDevice(), passState); + if (!distortionRT.is_valid() || !sceneColor.is_valid()) + return sceneColor; + ResourceDesc outputDesc; outputDesc.type = ResourceDesc::Type::Texture2D; outputDesc.width = width; @@ -91,14 +96,19 @@ VirtualResourceHandle setupDistortionApplyPass( auto& passData = fg.addCallbackPass( "DistortionApply", - [sceneColor, distortionRT, depth, outputHandle, width, height, &passState](FrameGraph& builder, PassHandle passHandle, DistortionApplyData& data) { + [sceneColor, distortionRT, worldPos, baseColor, outputHandle, width, height, &passState](FrameGraph& builder, PassHandle passHandle, DistortionApplyData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.width = width; data.height = height; data.passState = &passState; data.sceneInput = passBuilder.read(sceneColor, ResourceState::ShaderResource); data.distortionInput = passBuilder.read(distortionRT, ResourceState::ShaderResource); - data.depthInput = passBuilder.read(depth, ResourceState::ShaderResource); + data.worldPosInput = worldPos.is_valid() + ? passBuilder.read(worldPos, ResourceState::ShaderResource) + : VirtualResourceHandle{}; + data.baseColorInput = baseColor.is_valid() + ? passBuilder.read(baseColor, ResourceState::ShaderResource) + : VirtualResourceHandle{}; data.output = passBuilder.write(outputHandle, ResourceState::RenderTarget); }, @@ -106,9 +116,10 @@ VirtualResourceHandle setupDistortionApplyPass( nvrhi::ICommandList* cmdList = ctx->GetCommandList(); auto* sceneTex = fg.GetPhysicalTexture(data.sceneInput); auto* distortTex = fg.GetPhysicalTexture(data.distortionInput); - auto* depthTex = fg.GetPhysicalTexture(data.depthInput); + auto* worldPosTex = data.worldPosInput.is_valid() ? fg.GetPhysicalTexture(data.worldPosInput) : nullptr; + auto* baseColorTex = data.baseColorInput.is_valid() ? fg.GetPhysicalTexture(data.baseColorInput) : nullptr; auto* outputTex = fg.GetPhysicalTexture(data.output); - if (!sceneTex || !distortTex || !depthTex || !outputTex) + if (!sceneTex || !distortTex || !outputTex) return; auto* ps = data.passState; @@ -129,7 +140,8 @@ VirtualResourceHandle setupDistortionApplyPass( bsb.ConstantBuffer("static_globals", staticGlobalsCB) .Texture("g_Snapshot", sceneTex) .Texture("g_Distortion", distortTex) - .Texture("g_Depth", depthTex); + .Texture("g_WorldPos", worldPosTex ? worldPosTex : cache.GetDummyContactHistory(device)) + .Texture("g_BaseColor", baseColorTex ? baseColorTex : cache.GetDummyContactHistory(device)); auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->bindingLayout, device); nvrhi::FramebufferDesc fbDesc; diff --git a/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.h index ab4a5b9884e..32d070a9313 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.h @@ -27,7 +27,8 @@ framegraph::VirtualResourceHandle setupDistortionApplyPass( fg::RenderDevice* device, framegraph::VirtualResourceHandle sceneColor, framegraph::VirtualResourceHandle distortionRT, - framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle worldPos, + framegraph::VirtualResourceHandle baseColor, u32 width, u32 height, DistortionApplyPassState& state diff --git a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp index 508f9202e71..ed2fb88b08e 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp @@ -1,6 +1,7 @@ // xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp #include "stdafx.h" #include "ForwardColorPassSetup.h" +#include "IBLPrefilterPassSetup.h" #include "ShaderConstants.h" // CB layout definitions and FillGlobalConstants/FillDynamicTransforms #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/IPass.h" @@ -22,12 +23,16 @@ #include "Layers/xrRender/FrameGraph/PassResourceCache.h" #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" #include "PassCommon.h" +#include "xrCDB/Frustum.h" #include "Layers/xrRender/ClusteredLightManager.h" +#include "Layers/xrRender/r_FrameGraphRenderer.h" +#include "Layers/xrRender/xrRender_console.h" #include "xrCore/FMesh.hpp" namespace xray::render::fg { extern float r__dtex_range; + extern xray::render::FrameGraphRenderer RImplementation; } namespace xray::render::fg::passes { @@ -58,7 +63,7 @@ void InitializeForwardResources(fg::RenderDevice* device, const nvrhi::Framebuff auto& cache = framegraph::GetPassResourceCache(); - state.bindlessLayout = cache.GetOrCreateBindingLayoutFromReflection("ForwardColor", *vsResult.reflection, *psResult.reflection, nvDevice); + state.bindlessLayout = cache.GetOrCreateBindingLayoutFromReflection("ForwardColor_PBR_v4_TranspCB", *vsResult.reflection, *psResult.reflection, nvDevice); u32 attrCount = 0; auto* attrs = GetUnifiedVertexAttributes(attrCount); @@ -87,23 +92,62 @@ void InitializeForwardResources(fg::RenderDevice* device, const nvrhi::Framebuff pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; pipeDesc.renderState.depthStencilState.depthTestEnable = true; pipeDesc.renderState.depthStencilState.depthWriteEnable = true; - pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; pipeDesc.renderState.rasterState.frontCounterClockwise = false; pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; - state.bindlessPipeline = cache.GetOrCreatePipeline("ForwardColor", pipeDesc, fbInfo, nvDevice); + state.bindlessPipeline = cache.GetOrCreatePipeline("ForwardColor_PBR_v4_TranspCB", pipeDesc, fbInfo, nvDevice); if (!state.bindlessPipeline) { Msg("! [BindlessForward] Failed to create pipeline"); return; } + // After DepthPrepass: Equal + no write → Hi-Z rejects overdraw, skips redundant depth stores + { + nvrhi::GraphicsPipelineDesc equalDesc = pipeDesc; + equalDesc.renderState.depthStencilState.depthWriteEnable = false; + equalDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::Equal; + state.bindlessEqualPipeline = cache.GetOrCreatePipeline( + "ForwardColor_PBR_Equal_v1", equalDesc, fbInfo, nvDevice); + } + QueryBindingLayoutFromPipeline(state.bindlessPipeline, state.bindlessLayout); + // Optional tessellation PSO (PatchList). + // Apple/MoltenVK: do not create — Metal tess temp buffers can kernel-panic. +#if !defined(XR_PLATFORM_APPLE) + { + auto hsResult = shaderLoader->LoadHullShader("bindless_tess", "main"); + auto dsResult = shaderLoader->LoadDomainShader("bindless_tess", "main"); + if (hsResult.handle && dsResult.handle) { + state.bindlessHS = hsResult.handle; + state.bindlessDS = dsResult.handle; + + nvrhi::GraphicsPipelineDesc tessDesc = pipeDesc; + tessDesc.HS = state.bindlessHS; + tessDesc.DS = state.bindlessDS; + tessDesc.primType = nvrhi::PrimitiveType::PatchList; + tessDesc.patchControlPoints = 3; + tessDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + state.bindlessTessPipeline = cache.GetOrCreatePipeline( + "ForwardColor_Tess_v5", tessDesc, fbInfo, nvDevice); + if (state.bindlessTessPipeline) + Msg("* [BindlessForward] Tessellation pipeline ready"); + else + Msg("! [BindlessForward] Tessellation pipeline create failed (VkResult logged by NVRHI)"); + } else { + Msg("! [BindlessForward] Tess HS/DS shaders missing — tessellation disabled"); + } + } +#else + Msg("* [BindlessForward] Tessellation disabled on Apple/MoltenVK (IOGPU safety)"); +#endif + auto terrainPsResult = shaderLoader->LoadPixelShader("bindless_terrain", "main"); if (terrainPsResult.handle) { state.terrainPS = terrainPsResult.handle; state.terrainLayout = cache.GetOrCreateBindingLayoutFromReflection( - "ForwardColor_Terrain", *vsResult.reflection, *terrainPsResult.reflection, nvDevice); + "ForwardColor_Terrain_PBR_v3_CSMLadder", *vsResult.reflection, *terrainPsResult.reflection, nvDevice); if (state.terrainLayout) { nvrhi::GraphicsPipelineDesc terrainPipeDesc; @@ -117,12 +161,19 @@ void InitializeForwardResources(fg::RenderDevice* device, const nvrhi::Framebuff terrainPipeDesc.primType = nvrhi::PrimitiveType::TriangleList; terrainPipeDesc.renderState.depthStencilState.depthTestEnable = true; terrainPipeDesc.renderState.depthStencilState.depthWriteEnable = true; - terrainPipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; + terrainPipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; terrainPipeDesc.renderState.rasterState.frontCounterClockwise = false; terrainPipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; - state.terrainPipeline = cache.GetOrCreatePipeline("ForwardColor_Terrain", terrainPipeDesc, fbInfo, nvDevice); + state.terrainPipeline = cache.GetOrCreatePipeline("ForwardColor_Terrain_PBR_v3_CSMLadder", terrainPipeDesc, fbInfo, nvDevice); if (state.terrainPipeline) + { + nvrhi::GraphicsPipelineDesc terrainEqual = terrainPipeDesc; + terrainEqual.renderState.depthStencilState.depthWriteEnable = false; + terrainEqual.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::Equal; + state.terrainEqualPipeline = cache.GetOrCreatePipeline( + "ForwardColor_Terrain_Equal_v2_CSMLadder", terrainEqual, fbInfo, nvDevice); state.terrainInitialized = true; + } } } @@ -137,6 +188,7 @@ static void renderBindlessForward( nvrhi::ITexture* colorRT, nvrhi::ITexture* normalRT, nvrhi::ITexture* baseColorRT, + nvrhi::ITexture* worldPosRT, nvrhi::ITexture* depthRT, const BindlessForwardConfig& config, MaterialCache* materialCache, @@ -171,6 +223,8 @@ static void renderBindlessForward( fbDesc.addColorAttachment(normalRT); if (baseColorRT) fbDesc.addColorAttachment(baseColorRT); + if (worldPosRT) + fbDesc.addColorAttachment(worldPosRT); fbDesc.setDepthAttachment(depthRT); auto& cache = framegraph::GetPassResourceCache(); auto framebuffer = cache.GetOrCreateFramebuffer("ForwardColor", fbDesc, nvDevice); @@ -179,6 +233,17 @@ static void renderBindlessForward( auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), device); auto drawIndexBuffer = GetOrCreateDrawIndexBuffer("ForwardColor", nvDevice); + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + + auto transparentDrawCB = cache.GetOrCreateVolatileCB("Frame", "TransparentDrawCB", 16, device); + { + u32 passMode[4] = { 0, 0, 0, 0 }; + cmdList->writeBuffer(transparentDrawCB, passMode, sizeof(passMode)); + } + auto lightingData = FillLightingConstants(); cmdList->writeBuffer(lightingCB, &lightingData, sizeof(lightingData)); @@ -189,22 +254,98 @@ static void renderBindlessForward( auto* psReflection = shaderLoader->GetCachedReflection("bindless_forward", ".ps"); auto& clm = ClusteredLightManager::Instance(); + auto& passCache = framegraph::GetPassResourceCache(); + + nvrhi::ITexture* shadowTex = config.shadowCascades[0] + ? config.shadowCascades[0] + : (config.shadowMapArray ? config.shadowMapArray : nullptr); + if (!shadowTex) + shadowTex = passCache.GetDummyShadowMap2D(nvDevice); + nvrhi::ITexture* sky0 = config.envSky0 + ? config.envSky0 + : passCache.GetDummyCubeMap(nvDevice); + nvrhi::ITexture* sky1 = config.envSky1 + ? config.envSky1 + : passCache.GetDummyCubeMap(nvDevice); + + auto bindShadowMap = [&](framegraph::BindingSetBuilder& bsb) { + nvrhi::ITexture* dummy2D = passCache.GetDummyShadowMap2D(nvDevice); + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = (config.shadowCascades[i]) ? config.shadowCascades[i] + : (i == 0 && shadowTex ? shadowTex : dummy2D); + if (t) + bsb.Texture(kNames[i], t); + } + // Bind by name — unused g_ContactDepth must not be forced into the set + nvrhi::ITexture* contactHist = config.contactHistory + ? config.contactHistory + : passCache.GetDummyContactHistory(nvDevice); + if (contactHist) + bsb.Texture("g_ContactHistory", contactHist); + nvrhi::ITexture* localAtlas = config.localShadowAtlas + ? config.localShadowAtlas + : passCache.GetDummyShadowMap(nvDevice); + if (localAtlas) + bsb.Texture("g_LocalShadowAtlas", localAtlas); + nvrhi::ITexture* localEsm = config.localShadowESM + ? config.localShadowESM + : passCache.GetDummyLocalShadowESM(nvDevice); + if (localEsm) + bsb.Texture("g_LocalShadowESM", localEsm); + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + nvrhi::ITexture* dummyHzb = passCache.GetDummyContactDepth(nvDevice); + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* hz = config.shadowHZB[i] ? config.shadowHZB[i] : dummyHzb; + if (hz) + bsb.Texture(kHzb[i], hz); + } + }; + + auto bindShadowMask = [&](framegraph::BindingSetBuilder& bsb) { + nvrhi::ITexture* shadowMask = config.shadowMask + ? config.shadowMask + : passCache.GetDummyContactHistory(nvDevice); + if (shadowMask) + bsb.Texture("g_ShadowMask", shadowMask); + }; + + auto bindSkyCubes = [&](framegraph::BindingSetBuilder& bsb) { + if (sky0) + bsb.Texture("s_env0", sky0); + if (sky1) + bsb.Texture("s_env1", sky1); + IBLBindResources ibl{}; + ibl.brdfLut = config.envBrdfLut; + ibl.shBuffer = config.envSkySH; + ibl.probeBuffer = config.envProbes; + ibl.probeCubeArray = config.envProbeCubes; + BindIBLResources(bsb, ibl, nvDevice); + }; - auto buildBindingDescForSet = [&](const BindlessDrawSet& set) -> nvrhi::BindingSetDesc { + auto createBindingSetForSet = [&](const BindlessDrawSet& set) -> nvrhi::BindingSetHandle { framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "ForwardColor"); bsb.ConstantBuffer("static_globals", staticGlobalsCB); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); + bsb.ConstantBuffer("TransparentDrawCB", transparentDrawCB); + BindBindlessMaterialTables(bsb); bsb.BufferSRV("g_InstanceData", set.instanceBuffer); bsb.BufferSRV("g_CompactBatchIndices", set.compactBatchIndicesBuffer); bsb.BufferSRV("g_CompactMaterialIDs", set.compactMaterialIDBuffer); bsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); + if (clm.GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", clm.GetShadowDataBuffer()); bsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); - return bsb.Build(); - }; - - auto createBindingSetForSet = [&](const BindlessDrawSet& set) -> nvrhi::BindingSetHandle { - return framegraph::GetPassResourceCache().GetOrCreateBindingSet(buildBindingDescForSet(set), ps.bindlessLayout, nvDevice); + bindShadowMap(bsb); + bindShadowMask(bsb); + bindSkyCubes(bsb); + + auto bs = passCache.GetOrCreateBindingSet(bsb.Build(), ps.bindlessLayout, nvDevice); + if (!bs) + Msg("! [ForwardColor] Binding set create failed (missing CSM/sky?)"); + return bs; }; // ═══════════════════════════════════════════════════════ @@ -230,8 +371,12 @@ static void renderBindlessForward( return; } + nvrhi::IGraphicsPipeline* opaquePipe = ps.bindlessPipeline.Get(); + if (config.useEqualDepth && ps.bindlessEqualPipeline) + opaquePipe = ps.bindlessEqualPipeline.Get(); + nvrhi::GraphicsState state; - state.pipeline = ps.bindlessPipeline; + state.pipeline = opaquePipe; state.framebuffer = framebuffer; // SM6.6 bindless: Add the descriptor table from D3D12 backend @@ -261,7 +406,11 @@ static void renderBindlessForward( return; auto bindingSet = createBindingSetForSet(set); - R_ASSERT2(bindingSet, "Bindless forward binding set creation failed"); + if (!bindingSet) + { + Msg("! [ForwardColor] Skipping draw — binding set is null"); + return; + } state.bindings = { bindingSet }; if (bindlessTable) { @@ -277,20 +426,43 @@ static void renderBindlessForward( if (config.variantPartition.Enabled()) { auto* backendDev = device->GetBackend(); VariantPartitionDrawConfig vpCfg; - vpCfg.defaultPipeline = ps.bindlessPipeline.Get(); + vpCfg.defaultPipeline = opaquePipe; vpCfg.inputLayout = ps.bindlessInputLayout; vpCfg.passLayout = ps.bindlessLayout; vpCfg.bindlessLayout = backendDev ? backendDev->GetBindlessLayout() : nullptr; vpCfg.bindlessTable = bindlessTable; + vpCfg.sampler = ps.linearSampler; + vpCfg.staticGlobalsCB = staticGlobalsCB; + vpCfg.lightingCB = lightingCB; + vpCfg.materialBuffer = matBuffer.GetBuffer(); + vpCfg.variantTexBuffer = variantTexBuffer.GetBuffer(); + vpCfg.instanceBuffer = config.staticSet.instanceBuffer; vpCfg.megaVertexBuffer = config.megaVertexBuffer; - vpCfg.baseBindings = buildBindingDescForSet(config.staticSet); - vpCfg.objectCount = config.staticSet.totalObjectCount; + vpCfg.shadowMapArray = shadowTex; + for (u32 i = 0; i < 3; ++i) + { + vpCfg.shadowCascades[i] = config.shadowCascades[i]; + vpCfg.shadowHZB[i] = config.shadowHZB[i]; + } + vpCfg.localShadowAtlas = config.localShadowAtlas; + vpCfg.localShadowESM = config.localShadowESM; + vpCfg.contactHistory = config.contactHistory; + vpCfg.shadowMask = config.shadowMask; + vpCfg.envSky0 = sky0; + vpCfg.envSky1 = sky1; + { + auto& clmVp = ClusteredLightManager::Instance(); + vpCfg.lightDataBuffer = clmVp.GetLightDataBuffer(); + vpCfg.shadowDataBuffer = clmVp.GetShadowDataBuffer(); + vpCfg.clusterGridBuffer = clmVp.GetClusterGridBuffer(); + vpCfg.lightIndexListBuffer = clmVp.GetLightIndexListBuffer(); + } vpCfg.partition = config.variantPartition; vpCfg.selectTransparent = false; DrawVariantPartition(cmdList, nvDevice, framebuffer, state, vpCfg); - state.pipeline = ps.bindlessPipeline; + state.pipeline = opaquePipe; state.vertexBuffers = { {config.megaVertexBuffer, 0, 0}, {drawIndexBuffer, 1, 0} @@ -300,6 +472,78 @@ static void renderBindlessForward( } drawSet(config.dynamicSet); + // Tessellation: PatchList PSO + direct drawIndexed (CPU frustum). + // Avoid DrawIndexedIndirect+tess — MoltenVK/Vulkan freezes on some camera angles. + // Apple/MoltenVK: hard-disabled — Metal tess temp buffers can kernel-panic + // (IOGPUGroupMemory::remove_memory_object) regardless of the console flag. +#if defined(XR_PLATFORM_APPLE) + const bool tessEnabled = false; +#else + const bool tessEnabled = ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION); +#endif + if (tessEnabled && ps.bindlessTessPipeline && + config.tessObjectCount > 0 && config.tessDrawArgs && config.tessObjects && + config.tessMaterialIDBuffer && config.tessBatchIndicesBuffer && config.tessInstanceBuffer) + { + framegraph::BindingSetBuilder tessBsb(*vsReflection, *psReflection, nvDevice, "ForwardColor.Tess"); + tessBsb.ConstantBuffer("static_globals", staticGlobalsCB); + tessBsb.ConstantBuffer("TransparentDrawCB", transparentDrawCB); + BindBindlessMaterialTables(tessBsb); + tessBsb.BufferSRV("g_InstanceData", config.tessInstanceBuffer); + tessBsb.BufferSRV("g_CompactBatchIndices", config.tessBatchIndicesBuffer); + tessBsb.BufferSRV("g_CompactMaterialIDs", config.tessMaterialIDBuffer); + tessBsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); + if (clm.GetShadowDataBuffer()) + tessBsb.BufferSRV("g_ShadowData", clm.GetShadowDataBuffer()); + tessBsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); + tessBsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); + bindShadowMap(tessBsb); + bindShadowMask(tessBsb); + bindSkyCubes(tessBsb); + + auto tessBindingSet = passCache.GetOrCreateBindingSet(tessBsb.Build(), ps.bindlessLayout, nvDevice); + if (tessBindingSet) + { + state.pipeline = ps.bindlessTessPipeline; + state.bindings = { tessBindingSet }; + if (bindlessTable) + state.addBindingSet(bindlessTable); + state.indirectParams = nullptr; + state.indirectCountBuffer = nullptr; + cmdList->setGraphicsState(state); + + // Soft frustum cull: PatchList amplification is expensive and has + // caused view-angle freezes; skip batches clearly outside the view. + CFrustum tessFrustum; + tessFrustum.CreateFromMatrix(Device.mFullTransform, FRUSTUM_P_LRTB | FRUSTUM_P_FAR); + + for (u32 i = 0; i < config.tessObjectCount; ++i) + { + const auto& args = config.tessDrawArgs[i]; + if (args.indexCountPerInstance == 0) + continue; + + const auto& obj = config.tessObjects[i]; + if (!tessFrustum.testSphere_dirty(obj.position, obj.radius * 1.15f)) + continue; + + nvrhi::DrawArguments da; + da.vertexCount = args.indexCountPerInstance; + da.instanceCount = 1; + da.startIndexLocation = args.startIndexLocation; + da.startVertexLocation = args.baseVertexLocation; + da.startInstanceLocation = i; + cmdList->drawIndexed(da); + } + + state.pipeline = opaquePipe; + } + else + { + Msg("! [ForwardColor] Tess binding set create failed — skip tess draws"); + } + } + // ═══════════════════════════════════════════════════════ // TERRAIN RENDERING (4-layer detail blending) // ═══════════════════════════════════════════════════════ @@ -318,49 +562,59 @@ static void renderBindlessForward( config.terrainCompactDrawArgsBuffer && config.terrainCompactCountBuffer, "Terrain buffers not ready for compaction rendering"); - // Create terrain binding set (includes TerrainMaterialBuffer at t9) - // NOTE: Terrain uses its own instance/batch buffers, not the regular ones + // Terrain PS includes bindless_common.h → layout needs t8/t9/t10 even if + // only g_TerrainMaterials is sampled (same issue as DetailPass). auto* terrainVsRefl = shaderLoader->GetCachedReflection("bindless_forward", ".vs"); auto* terrainPsRefl = shaderLoader->GetCachedReflection("bindless_terrain", ".ps"); framegraph::BindingSetBuilder terrainBsb(*terrainVsRefl, *terrainPsRefl, nvDevice, "ForwardColor.Terrain"); terrainBsb.ConstantBuffer("static_globals", staticGlobalsCB); - terrainBsb.BufferSRV("g_TerrainMaterials", terrainMatBuffer.GetBuffer()); + BindBindlessMaterialTables(terrainBsb); terrainBsb.BufferSRV("g_InstanceData", config.terrainInstanceBuffer); terrainBsb.BufferSRV("g_CompactBatchIndices", config.terrainCompactBatchIndicesBuffer); terrainBsb.BufferSRV("g_CompactMaterialIDs", config.terrainCompactMaterialIDBuffer); terrainBsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); + if (clm.GetShadowDataBuffer()) + terrainBsb.BufferSRV("g_ShadowData", clm.GetShadowDataBuffer()); terrainBsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); terrainBsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); + bindShadowMap(terrainBsb); + bindSkyCubes(terrainBsb); auto terrainBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(terrainBsb.Build(), ps.terrainLayout, nvDevice); - R_ASSERT2(terrainBindingSet, "Terrain binding set creation failed"); - - // Set up terrain graphics state - nvrhi::GraphicsState terrainState; - terrainState.pipeline = ps.terrainPipeline; - terrainState.framebuffer = framebuffer; - terrainState.bindings = { terrainBindingSet }; - - // Add bindless descriptor table - if (backend) { - auto* bindlessTable = backend->GetBindlessDescriptorTable(); - if (bindlessTable) - terrainState.addBindingSet(bindlessTable); + if (!terrainBindingSet) + { + Msg("! [ForwardColor] Terrain binding set create failed"); + } + else + { + nvrhi::IGraphicsPipeline* terrainPipe = ps.terrainPipeline.Get(); + if (config.useEqualDepth && ps.terrainEqualPipeline) + terrainPipe = ps.terrainEqualPipeline.Get(); + + nvrhi::GraphicsState terrainState; + terrainState.pipeline = terrainPipe; + terrainState.framebuffer = framebuffer; + terrainState.bindings = { terrainBindingSet }; + + if (backend) { + auto* bindlessTable = backend->GetBindlessDescriptorTable(); + if (bindlessTable) + terrainState.addBindingSet(bindlessTable); + } + + terrainState.vertexBuffers = { + {config.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0} + }; + terrainState.indexBuffer = { config.megaIndexBuffer, nvrhi::Format::R32_UINT, 0 }; + terrainState.indirectParams = config.terrainCompactDrawArgsBuffer; + terrainState.indirectCountBuffer = config.terrainCompactCountBuffer; + terrainState.viewport.addViewport(viewport); + terrainState.viewport.addScissorRect(nvrhi::Rect(rtDesc.width, rtDesc.height)); + + cmdList->setGraphicsState(terrainState); + DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, config.terrainObjectCount); } - - terrainState.vertexBuffers = { - {config.megaVertexBuffer, 0, 0}, - {drawIndexBuffer, 1, 0} - }; - terrainState.indexBuffer = { config.megaIndexBuffer, nvrhi::Format::R32_UINT, 0 }; - terrainState.indirectParams = config.terrainCompactDrawArgsBuffer; - terrainState.indirectCountBuffer = config.terrainCompactCountBuffer; - terrainState.viewport.addViewport(viewport); - terrainState.viewport.addScissorRect(nvrhi::Rect(rtDesc.width, rtDesc.height)); - - cmdList->setGraphicsState(terrainState); - DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, config.terrainObjectCount); - } } @@ -375,6 +629,7 @@ framegraph::DefaultOutputLayout setupForwardColorPass( framegraph::VirtualResourceHandle colorInput, framegraph::VirtualResourceHandle normalInput, framegraph::VirtualResourceHandle baseColorInput, + framegraph::VirtualResourceHandle worldPosInput, const GeometryCollector* geometry, MaterialCache* materialCache, u32 width, @@ -401,7 +656,7 @@ framegraph::DefaultOutputLayout setupForwardColorPass( // ═══════════════════════════════════════════════════════ // SETUP LAMBDA (Declares resource usage) // ═══════════════════════════════════════════════════════ - [&, width, height, colorInput, normalInput, baseColorInput, drawArgsInput, bindlessConfig, state](FrameGraph& builder, PassHandle passHandle, ForwardColorPassData& data) { + [&, width, height, colorInput, normalInput, baseColorInput, worldPosInput, drawArgsInput, bindlessConfig, state](FrameGraph& builder, PassHandle passHandle, ForwardColorPassData& data) { data.width = width; data.height = height; data.device = device; @@ -417,14 +672,31 @@ framegraph::DefaultOutputLayout setupForwardColorPass( data.normal = passBuilder.write(normalInput, ResourceState::RenderTarget); if (baseColorInput.is_valid()) data.baseColor = passBuilder.write(baseColorInput, ResourceState::RenderTarget); + if (worldPosInput.is_valid()) + data.worldPos = passBuilder.write(worldPosInput, ResourceState::RenderTarget); if (drawArgsInput.is_valid()) { data.drawArgsBuffer = passBuilder.read(drawArgsInput, ResourceState::IndirectArgument); } + if (bindlessConfig.shadowMapHandle.is_valid()) { + passBuilder.read(bindlessConfig.shadowMapHandle, ResourceState::ShaderResource); + } + if (bindlessConfig.localShadowHandle.is_valid()) { + passBuilder.read(bindlessConfig.localShadowHandle, ResourceState::ShaderResource); + } + for (u32 i = 0; i < 3; ++i) { + if (bindlessConfig.shadowHZBHandles[i].is_valid()) + passBuilder.read(bindlessConfig.shadowHZBHandles[i], ResourceState::ShaderResource); + } + if (bindlessConfig.shadowMaskHandle.is_valid()) { + passBuilder.read(bindlessConfig.shadowMaskHandle, ResourceState::ShaderResource); + } + data.outputs.albedo = data.color; data.outputs.normal = data.normal; data.outputs.baseColor = data.baseColor; + data.outputs.worldPos = data.worldPos; data.outputs.depth = data.depth; }, @@ -439,17 +711,22 @@ framegraph::DefaultOutputLayout setupForwardColorPass( auto* colorRT = fg.GetPhysicalTexture(data.color); auto* normalRT = fg.GetPhysicalTexture(data.normal); auto* baseColorRT = data.baseColor.is_valid() ? fg.GetPhysicalTexture(data.baseColor) : nullptr; + auto* worldPosRT = data.worldPos.is_valid() ? fg.GetPhysicalTexture(data.worldPos) : nullptr; if (!depthRT || !colorRT) return; nvrhi::ICommandList* cmdList = ctx->GetCommandList(); if (cmdList) { - cmdList->clearDepthStencilTexture(depthRT, nvrhi::AllSubresources, true, 0.0f, false, 0); + // DepthPrepass already cleared+filled depth — clearing here would kill early-Z. + if (!ps_r_depth_prepass) + cmdList->clearDepthStencilTexture(depthRT, nvrhi::AllSubresources, true, 1.0f, false, 0); if (normalRT) cmdList->clearTextureFloat(normalRT, nvrhi::AllSubresources, nvrhi::Color(0.0f)); if (baseColorRT) cmdList->clearTextureFloat(baseColorRT, nvrhi::AllSubresources, nvrhi::Color(0.0f)); + if (worldPosRT) + cmdList->clearTextureFloat(worldPosRT, nvrhi::AllSubresources, nvrhi::Color(0.0f)); } // Check if we have geometry to render @@ -475,6 +752,7 @@ framegraph::DefaultOutputLayout setupForwardColorPass( colorRT, normalRT, baseColorRT, + worldPosRT, depthRT, data.bindlessConfig, data.materialCache, @@ -487,6 +765,7 @@ framegraph::DefaultOutputLayout setupForwardColorPass( outputs.albedo = passData.color; outputs.normal = passData.normal; outputs.baseColor = passData.baseColor; + outputs.worldPos = passData.worldPos; outputs.depth = passData.depth; return outputs; } diff --git a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h index da899c126e6..b0f90dc9379 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.h @@ -214,6 +214,7 @@ struct ForwardColorPassData { framegraph::VirtualResourceHandle color; framegraph::VirtualResourceHandle normal; framegraph::VirtualResourceHandle baseColor; + framegraph::VirtualResourceHandle worldPos; framegraph::VirtualResourceHandle drawArgsBuffer; fg::RenderDevice* device; const GeometryCollector* geometry; @@ -237,6 +238,7 @@ framegraph::DefaultOutputLayout setupForwardColorPass( framegraph::VirtualResourceHandle colorInput, framegraph::VirtualResourceHandle normalInput, framegraph::VirtualResourceHandle baseColorInput, + framegraph::VirtualResourceHandle worldPosInput, const GeometryCollector* geometry, MaterialCache* materialCache, u32 width, diff --git a/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp index f2de0d0bebf..29cb9907637 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp @@ -756,8 +756,7 @@ ParticlePassOutput setupParticlePass( BindingSetBuilder lbsb(*vsReflection, *litPsReflection, nvDevice, psName); lbsb.ConstantBuffer("static_globals", staticGlobalsCB) .BufferSRV("g_Materials", matBuffer.GetBuffer()) - .Texture("g_SceneDepth", prevDepthTex) - .Sampler("s_linear", data.passState->sampler); + .Texture("g_SceneDepth", prevDepthTex); auto& clm = ClusteredLightManager::Instance(); if (clm.GetLightDataBuffer()) lbsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); diff --git a/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.h index c130b7a938d..e5eac3a9842 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.h @@ -83,8 +83,9 @@ ReSTIRGIOutput setupReSTIRGIPass( framegraph::VirtualResourceHandle depth, framegraph::VirtualResourceHandle normal, framegraph::VirtualResourceHandle baseColor, + framegraph::VirtualResourceHandle worldPos, framegraph::VirtualResourceHandle prevNormals, - framegraph::VirtualResourceHandle prevDepth, + framegraph::VirtualResourceHandle prevWorldPos, framegraph::VirtualResourceHandle motionVectors, framegraph::VirtualResourceHandle sceneColorIn, const Fmatrix& invViewProj, diff --git a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp index 672da1687b6..1b0cb19a12a 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp @@ -3,6 +3,7 @@ // Uses GPU-driven global bone buffer for efficient skinning #include "stdafx.h" #include "SkinningPassSetup.h" +#include "IBLPrefilterPassSetup.h" #include "ShaderConstants.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/IPass.h" @@ -28,6 +29,8 @@ #include "PassCommon.h" #include "Layers/xrRender/ClusteredLightManager.h" #include "xrCore/FMesh.hpp" +#include "xrEngine/IGame_Persistent.h" +#include "xrEngine/ShadersExternalData.h" extern ENGINE_API float psHUD_FOV; @@ -67,6 +70,9 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf if (state.initialized) return; + // Drop stale variant PSOs so NPC materials remapped to bindless_skinned pick up IBL/CSM. + VariantPSOCache::Instance().Shutdown(); + nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); if (!nvDevice) return; @@ -87,16 +93,25 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf state.ps = skinnedPsResult.handle; auto skinnedVsForReflection = shaderLoader->LoadVertexShader("bindless_skinned", "main"); - state.layout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass", *skinnedVsForReflection.reflection, *skinnedPsResult.reflection, nvDevice); + state.layout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_IBL_CSMLadder_NoMask", *skinnedVsForReflection.reflection, *skinnedPsResult.reflection, nvDevice); auto hudPsResult = shaderLoader->LoadPixelShader("bindless_skinned_hud", "main"); if (hudPsResult.handle) { state.hudPS = hudPsResult.handle; - state.hudLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_HUD", *skinnedVsForReflection.reflection, *hudPsResult.reflection, nvDevice); + state.hudLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_HUD_IBL_NoCSM_v3", *skinnedVsForReflection.reflection, *hudPsResult.reflection, nvDevice); } if (!state.hudLayout) state.hudLayout = state.layout; + auto scopePsResult = shaderLoader->LoadPixelShader("bindless_skinned_scope", "main"); + if (scopePsResult.handle) { + state.hudScopePS = scopePsResult.handle; + state.hudScopeLayout = cache.GetOrCreateBindingLayoutFromReflection( + "SkinningPass_HUDScope", *skinnedVsForReflection.reflection, *scopePsResult.reflection, nvDevice); + } + if (!state.hudScopeLayout) + state.hudScopeLayout = state.hudLayout; + auto buildPipelineDesc = [&](nvrhi::IShader* vs, nvrhi::IInputLayout* il, nvrhi::IShader* psOverride = nullptr) { nvrhi::GraphicsPipelineDesc pipeDesc; pipeDesc.VS = vs; @@ -109,7 +124,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; pipeDesc.renderState.depthStencilState.depthTestEnable = true; pipeDesc.renderState.depthStencilState.depthWriteEnable = true; - pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; pipeDesc.renderState.rasterState.frontCounterClockwise = false; pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; return pipeDesc; @@ -135,14 +150,35 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf hudVariant.inputLayout = worldVariant.inputLayout; auto pipeDesc = buildPipelineDesc(worldVariant.vs, worldVariant.inputLayout, state.hudPS); pipeDesc.bindingLayouts[0] = state.hudLayout; + for (u32 rt = 0; rt < 4; ++rt) + { + pipeDesc.renderState.blendState.targets[rt].setBlendEnable(false); + pipeDesc.renderState.blendState.targets[rt].setColorWriteMask( + nvrhi::ColorMask::Red | nvrhi::ColorMask::Green | + nvrhi::ColorMask::Blue | nvrhi::ColorMask::Alpha); + } hudVariant.pipeline = cache.GetOrCreatePipeline(cacheName, pipeDesc, fbInfo, nvDevice); }; + auto initHudScopeVariant = [&](SkinningPipelineVariant& scopeVariant, const SkinningPipelineVariant& worldVariant, const char* cacheName) { + if (!worldVariant.pipeline || !state.hudScopePS || !state.hudScopeLayout) + return; + scopeVariant.vs = worldVariant.vs; + scopeVariant.inputLayout = worldVariant.inputLayout; + auto pipeDesc = buildPipelineDesc(worldVariant.vs, worldVariant.inputLayout, state.hudScopePS); + pipeDesc.bindingLayouts[0] = state.hudScopeLayout; + pipeDesc.renderState.blendState.targets[0].setBlendEnable(true); + pipeDesc.renderState.blendState.targets[0].setSrcBlend(nvrhi::BlendFactor::SrcAlpha); + pipeDesc.renderState.blendState.targets[0].setDestBlend(nvrhi::BlendFactor::OneMinusSrcAlpha); + pipeDesc.renderState.depthStencilState.depthWriteEnable = false; + scopeVariant.pipeline = cache.GetOrCreatePipeline(cacheName, pipeDesc, fbInfo, nvDevice); + }; + auto mdiPsResult = shaderLoader->LoadPixelShader("bindless_skinned_mdi", "main"); auto mdiVsForReflection = shaderLoader->LoadVertexShader("bindless_skinned_mdi", "main"); if (mdiPsResult.handle && mdiVsForReflection.handle) { state.mdiPS = mdiPsResult.handle; - state.mdiLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_MDI", + state.mdiLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_MDI_IBL_CSMLadder_NoMask", *mdiVsForReflection.reflection, *mdiPsResult.reflection, nvDevice); } @@ -177,8 +213,8 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG16_SNORM).setOffset(20).setElementStride(stride), }; - initVariant(state.nonHQ, "bindless_skinned", "SkinningPass_nonHQ", attribs, 5); - initMDIVariant(state.mdiNonHQ, "bindless_skinned_mdi", "SkinningPass_mdi_nonHQ", attribs, 5); + initVariant(state.nonHQ, "bindless_skinned", "SkinningPass_nonHQ_IBL_CSMLadder_v2", attribs, 5); + initMDIVariant(state.mdiNonHQ, "bindless_skinned_mdi", "SkinningPass_mdi_nonHQ_IBL_CSMLadder_v2", attribs, 5); } { @@ -190,8 +226,8 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), }; - initVariant(state.hq1w, "bindless_skinned_hq", "SkinningPass_hq1w", attribs, 5); - initMDIVariant(state.mdiHQ1w, "bindless_skinned_hq_mdi", "SkinningPass_mdi_hq1w", attribs, 5); + initVariant(state.hq1w, "bindless_skinned_hq", "SkinningPass_hq1w_IBL_CSMLadder_v2", attribs, 5); + initMDIVariant(state.mdiHQ1w, "bindless_skinned_hq_mdi", "SkinningPass_mdi_hq1w_IBL_CSMLadder_v2", attribs, 5); } { @@ -204,8 +240,8 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("BLENDINDICES").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(36).setElementStride(stride), }; - initVariant(state.hq4w, "bindless_skinned_4w", "SkinningPass_hq4w", attribs, 6); - initMDIVariant(state.mdiHQ4w, "bindless_skinned_4w_mdi", "SkinningPass_mdi_hq4w", attribs, 6); + initVariant(state.hq4w, "bindless_skinned_4w", "SkinningPass_hq4w_IBL_CSMLadder_v2", attribs, 6); + initMDIVariant(state.mdiHQ4w, "bindless_skinned_4w_mdi", "SkinningPass_mdi_hq4w_IBL_CSMLadder_v2", attribs, 6); } { @@ -217,8 +253,8 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), }; - initVariant(state.hq2w, "bindless_skinned_2w", "SkinningPass_hq2w", attribs, 5); - initMDIVariant(state.mdiHQ2w, "bindless_skinned_2w_mdi", "SkinningPass_mdi_hq2w", attribs, 5); + initVariant(state.hq2w, "bindless_skinned_2w", "SkinningPass_hq2w_IBL_CSMLadder_v2", attribs, 5); + initMDIVariant(state.mdiHQ2w, "bindless_skinned_2w_mdi", "SkinningPass_mdi_hq2w_IBL_CSMLadder_v2", attribs, 5); } { @@ -230,15 +266,121 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), }; - initVariant(state.hq3w, "bindless_skinned_3w", "SkinningPass_hq3w", attribs, 5); - initMDIVariant(state.mdiHQ3w, "bindless_skinned_3w_mdi", "SkinningPass_mdi_hq3w", attribs, 5); + initVariant(state.hq3w, "bindless_skinned_3w", "SkinningPass_hq3w_IBL_CSMLadder_v2", attribs, 5); + initMDIVariant(state.mdiHQ3w, "bindless_skinned_3w_mdi", "SkinningPass_mdi_hq3w_IBL_CSMLadder_v2", attribs, 5); } - initHudVariant(state.hudNonHQ, state.nonHQ, "SkinningPass_hud_nonHQ"); - initHudVariant(state.hudHQ1w, state.hq1w, "SkinningPass_hud_hq1w"); - initHudVariant(state.hudHQ2w, state.hq2w, "SkinningPass_hud_hq2w"); - initHudVariant(state.hudHQ3w, state.hq3w, "SkinningPass_hud_hq3w"); - initHudVariant(state.hudHQ4w, state.hq4w, "SkinningPass_hud_hq4w"); + initHudVariant(state.hudNonHQ, state.nonHQ, "SkinningPass_hud_nonHQ_Opaque_v3"); + initHudVariant(state.hudHQ1w, state.hq1w, "SkinningPass_hud_hq1w_Opaque_v3"); + initHudVariant(state.hudHQ2w, state.hq2w, "SkinningPass_hud_hq2w_Opaque_v3"); + initHudVariant(state.hudHQ3w, state.hq3w, "SkinningPass_hud_hq3w_Opaque_v3"); + initHudVariant(state.hudHQ4w, state.hq4w, "SkinningPass_hud_hq4w_Opaque_v3"); + + initHudScopeVariant(state.hudScopeNonHQ, state.nonHQ, "SkinningPass_hudScope_nonHQ"); + initHudScopeVariant(state.hudScopeHQ1w, state.hq1w, "SkinningPass_hudScope_hq1w"); + initHudScopeVariant(state.hudScopeHQ2w, state.hq2w, "SkinningPass_hudScope_hq2w"); + initHudScopeVariant(state.hudScopeHQ3w, state.hq3w, "SkinningPass_hudScope_hq3w"); + initHudScopeVariant(state.hudScopeHQ4w, state.hq4w, "SkinningPass_hudScope_hq4w"); + + // Skinned PatchList tessellation is NOT created on Apple/MoltenVK: Metal's + // tessellation temp buffers have caused IOGPU kernel panics + // (IOGPUGroupMemory::remove_memory_object). Keep the variant slots empty so + // SelectSkinnedTessPipeline always returns null. +#if !defined(XR_PLATFORM_APPLE) + { + auto hs = shaderLoader->LoadHullShader("bindless_skinned_tess", "main"); + auto ds = shaderLoader->LoadDomainShader("bindless_skinned_tess", "main"); + if (hs.handle && ds.handle) + { + state.tessHS = hs.handle; + state.tessDS = ds.handle; + + auto initTessVariant = [&](SkinningPipelineVariant& tessVar, + const SkinningPipelineVariant& src, + const char* cacheName) { + if (!src.pipeline || !src.vs || !src.inputLayout) + return; + tessVar.vs = src.vs; + tessVar.inputLayout = src.inputLayout; + auto pipeDesc = buildPipelineDesc(src.vs, src.inputLayout); + pipeDesc.HS = state.tessHS; + pipeDesc.DS = state.tessDS; + pipeDesc.primType = nvrhi::PrimitiveType::PatchList; + pipeDesc.patchControlPoints = 3; + pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + tessVar.pipeline = cache.GetOrCreatePipeline(cacheName, pipeDesc, fbInfo, nvDevice); + }; + + initTessVariant(state.tessNonHQ, state.nonHQ, "SkinningPass_tess_nonHQ_v1"); + initTessVariant(state.tessHQ1w, state.hq1w, "SkinningPass_tess_hq1w_v1"); + initTessVariant(state.tessHQ2w, state.hq2w, "SkinningPass_tess_hq2w_v1"); + initTessVariant(state.tessHQ3w, state.hq3w, "SkinningPass_tess_hq3w_v1"); + initTessVariant(state.tessHQ4w, state.hq4w, "SkinningPass_tess_hq4w_v1"); + + if (state.tessNonHQ.pipeline || state.tessHQ1w.pipeline || state.tessHQ4w.pipeline) + Msg("* [SkinningPass] Skinned tessellation pipelines ready"); + else + Msg("! [SkinningPass] Skinned tess HS/DS loaded but PSO create failed"); + } + else + { + Msg("! [SkinningPass] bindless_skinned_tess HS/DS missing — skinned tess disabled"); + } + } +#else + Msg("* [SkinningPass] Skinned tessellation disabled on Apple/MoltenVK (IOGPU safety)"); +#endif + + { + auto depthPsResult = shaderLoader->LoadPixelShader("bindless_skinned_depth", "main"); + if (depthPsResult.handle && skinnedVsForReflection.handle) + { + state.depthPS = depthPsResult.handle; + state.depthLayout = cache.GetOrCreateBindingLayoutFromReflection( + "SkinningDepthPass", *skinnedVsForReflection.reflection, *depthPsResult.reflection, nvDevice); + + nvrhi::FramebufferInfoEx depthFb; + depthFb.depthFormat = nvrhi::Format::D32; + + auto initDepthVariant = [&](SkinningPipelineVariant& depthVar, + const SkinningPipelineVariant& src, + const char* cacheName) { + if (!state.depthPS || !state.depthLayout || !src.vs || !src.inputLayout) + return; + depthVar.vs = src.vs; + depthVar.inputLayout = src.inputLayout; + nvrhi::GraphicsPipelineDesc pipeDesc; + pipeDesc.VS = src.vs; + pipeDesc.PS = state.depthPS; + pipeDesc.inputLayout = src.inputLayout; + if (bindlessLayout) + pipeDesc.bindingLayouts = {state.depthLayout, bindlessLayout}; + else + pipeDesc.bindingLayouts = {state.depthLayout}; + pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; + pipeDesc.renderState.depthStencilState.depthTestEnable = true; + pipeDesc.renderState.depthStencilState.depthWriteEnable = true; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + pipeDesc.renderState.rasterState.frontCounterClockwise = false; + pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; + depthVar.pipeline = cache.GetOrCreatePipeline(cacheName, pipeDesc, depthFb, nvDevice); + }; + + initDepthVariant(state.depthNonHQ, state.nonHQ, "SkinningDepth_nonHQ_v1"); + initDepthVariant(state.depthHQ1w, state.hq1w, "SkinningDepth_hq1w_v1"); + initDepthVariant(state.depthHQ2w, state.hq2w, "SkinningDepth_hq2w_v1"); + initDepthVariant(state.depthHQ3w, state.hq3w, "SkinningDepth_hq3w_v1"); + initDepthVariant(state.depthHQ4w, state.hq4w, "SkinningDepth_hq4w_v1"); + if (state.depthNonHQ.pipeline || state.depthHQ1w.pipeline || state.depthHQ4w.pipeline) + Msg("* [SkinningPass] Skinned depth pipelines ready"); + else + Msg("! [SkinningPass] Skinned depth PSO create failed"); + } + else + { + Msg("! [SkinningPass] bindless_skinned_depth missing — skinned depth disabled"); + } + } state.initialized = true; Msg("* [SkinningPass] Pipeline initialization complete"); @@ -297,12 +439,40 @@ static nvrhi::IGraphicsPipeline* SelectSkinnedPipeline(const SkinningPassState& return SelectSkinnedPipelineFromVariants(state.nonHQ, state.hq1w, state.hq2w, state.hq3w, state.hq4w, vertexStride, renderMode); } +static nvrhi::IGraphicsPipeline* SelectSkinnedDepthPipeline(const SkinningPassState& state, u32 vertexStride, u16 renderMode) +{ + return SelectSkinnedPipelineFromVariants( + state.depthNonHQ, state.depthHQ1w, state.depthHQ2w, state.depthHQ3w, state.depthHQ4w, + vertexStride, renderMode); +} + +static nvrhi::IGraphicsPipeline* SelectSkinnedTessPipeline(const SkinningPassState& state, u32 vertexStride, u16 renderMode) +{ + return SelectSkinnedPipelineFromVariants(state.tessNonHQ, state.tessHQ1w, state.tessHQ2w, state.tessHQ3w, state.tessHQ4w, vertexStride, renderMode); +} + static nvrhi::IGraphicsPipeline* SelectHUDSkinnedPipeline(const SkinningPassState& state, u32 vertexStride, u16 renderMode) { auto* hudPipe = SelectSkinnedPipelineFromVariants(state.hudNonHQ, state.hudHQ1w, state.hudHQ2w, state.hudHQ3w, state.hudHQ4w, vertexStride, renderMode); return hudPipe ? hudPipe : SelectSkinnedPipeline(state, vertexStride, renderMode); } +static nvrhi::IGraphicsPipeline* SelectHUDScopePipeline(const SkinningPassState& state, u32 vertexStride, u16 renderMode) +{ + auto* pipe = SelectSkinnedPipelineFromVariants( + state.hudScopeNonHQ, state.hudScopeHQ1w, state.hudScopeHQ2w, state.hudScopeHQ3w, state.hudScopeHQ4w, + vertexStride, renderMode); + return pipe ? pipe : SelectHUDSkinnedPipeline(state, vertexStride, renderMode); +} + +static bool IsScopeBatch(const GeometryBatch& batch) +{ + if (!batch.visual || !batch.visual->shaderName.size()) + return false; + const char* sh = batch.visual->shaderName.c_str(); + return strstr(sh, "scope") != nullptr || strstr(sh, "lense") != nullptr || strstr(sh, "lens") != nullptr; +} + static u32 GetSkinnedVertexFormatID(u16 renderMode, u32 vertexStride) { if (renderMode == RM_SKINNING_3B || renderMode == RM_SKINNING_3B_HQ) return VF_SKINNED_HQ3W; @@ -397,6 +567,65 @@ struct SkinnedPhaseContext { bool isHUD = false; }; +static void BindSkinnedLightingTextures( + framegraph::BindingSetBuilder& bsb, + nvrhi::IDevice* nvDevice, + nvrhi::ITexture* const* shadowCascades, + nvrhi::ITexture* hudShadowMap, + nvrhi::ITexture* contactDepth, + nvrhi::ITexture* contactHistory, + nvrhi::ITexture* envSky0, + nvrhi::ITexture* envSky1, + bool isHUD, + nvrhi::ITexture* localShadowAtlas = nullptr, + nvrhi::ITexture* const* shadowHZB = nullptr, + nvrhi::ITexture* shadowMask = nullptr, + nvrhi::ITexture* localShadowESM = nullptr) +{ + auto& cache = framegraph::GetPassResourceCache(); + nvrhi::ITexture* dummy2D = cache.GetDummyShadowMap2D(nvDevice); + nvrhi::ITexture* dummyArray = cache.GetDummyShadowMap(nvDevice); + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = (shadowCascades && shadowCascades[i]) ? shadowCascades[i] : dummy2D; + if (t) + bsb.Texture(kNames[i], t); + } + if (isHUD) + { + nvrhi::ITexture* hudTex = hudShadowMap ? hudShadowMap : dummyArray; + if (hudTex) + bsb.Texture("g_HUDShadowMap", hudTex); + } + (void)contactDepth; + nvrhi::ITexture* hist = contactHistory ? contactHistory : cache.GetDummyContactHistory(nvDevice); + if (hist) + bsb.Texture("g_ContactHistory", hist); + nvrhi::ITexture* localAtlas = localShadowAtlas ? localShadowAtlas : dummyArray; + if (localAtlas) + bsb.Texture("g_LocalShadowAtlas", localAtlas); + nvrhi::ITexture* localEsm = localShadowESM ? localShadowESM : cache.GetDummyLocalShadowESM(nvDevice); + if (localEsm) + bsb.Texture("g_LocalShadowESM", localEsm); + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + nvrhi::ITexture* dummyHzb = cache.GetDummyContactDepth(nvDevice); + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* hz = (shadowHZB && shadowHZB[i]) ? shadowHZB[i] : dummyHzb; + if (hz) + bsb.Texture(kHzb[i], hz); + } + (void)shadowMask; + nvrhi::ITexture* sky0 = envSky0 ? envSky0 : cache.GetDummyCubeMap(nvDevice); + nvrhi::ITexture* sky1 = envSky1 ? envSky1 : cache.GetDummyCubeMap(nvDevice); + if (sky0) + bsb.Texture("s_env0", sky0); + if (sky1) + bsb.Texture("s_env1", sky1); + BindIBLResources(bsb, GetCurrentIBLBindResources(), nvDevice); +} + static SkinnedPhaseContext BuildSkinnedPhaseContext( const SkinningPassState& state, nvrhi::IDevice* nvDevice, @@ -410,7 +639,17 @@ static SkinnedPhaseContext BuildSkinnedPhaseContext( nvrhi::IBuffer* splatBuffer, const nvrhi::Viewport& viewport, const nvrhi::Rect& scissor, - bool isHUD) + bool isHUD, + nvrhi::ITexture* const* shadowCascades, + nvrhi::ITexture* hudShadowMap, + nvrhi::ITexture* contactDepth, + nvrhi::ITexture* contactHistory, + nvrhi::ITexture* envSky0, + nvrhi::ITexture* envSky1, + nvrhi::ITexture* localShadowAtlas = nullptr, + nvrhi::ITexture* const* shadowHZB = nullptr, + nvrhi::ITexture* shadowMask = nullptr, + nvrhi::ITexture* localShadowESM = nullptr) { using namespace fg; using namespace fg::bindless; @@ -442,11 +681,14 @@ static SkinnedPhaseContext BuildSkinnedPhaseContext( bsb.ConstantBuffer("static_globals", staticGlobalsCB); bsb.BufferSRV("g_BoneMatrices", globalBoneBuffer); bsb.ConstantBuffer("SkinnedMaterialCB", materialIdCB); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); - bsb.BufferSRV("g_PaintSplats", splatBuffer); + BindBindlessMaterialTables(bsb); + BindPaintSplatBuffer(bsb, nvDevice, splatBuffer); bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); + if (ClusteredLightManager::Instance().GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", ClusteredLightManager::Instance().GetShadowDataBuffer()); bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); + BindSkinnedLightingTextures(bsb, nvDevice, shadowCascades, hudShadowMap, contactDepth, contactHistory, envSky0, envSky1, isHUD, localShadowAtlas, shadowHZB, shadowMask, localShadowESM); ctx.bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), activeLayout, nvDevice); return ctx; @@ -498,9 +740,37 @@ static void DrawSkinnedBatch( nvDevice, ctx.framebuffer, variantIdx, *variant, p, fmt, GetSkinnedInputLayout(state, fmt), state.layout, ctx.bindlessLayout); } else { - pipeline = ctx.isHUD - ? SelectHUDSkinnedPipeline(state, batch.vertexStride, batch.skinningRenderMode) - : SelectSkinnedPipeline(state, batch.vertexStride, batch.skinningRenderMode); + // Skinned PatchList tess is disabled on Apple (IOGPU kernel panic). + // On other platforms it still requires r4_enable_tessellation + tessMethod. + nvrhi::IGraphicsPipeline* tessPipe = nullptr; +#if !defined(XR_PLATFORM_APPLE) + constexpr u32 kMaxSkinnedTessIndices = 8192; + const bool tessOn = !ctx.isHUD && + ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION) && + batch.indexCount > 0 && batch.indexCount <= kMaxSkinnedTessIndices; + if (tessOn) + { + const auto* mat = MaterialBuffer::Instance().GetMaterial(batch.bindlessMaterialID); + if (mat && mat->tessMethod != 0) + tessPipe = SelectSkinnedTessPipeline(state, batch.vertexStride, batch.skinningRenderMode); + } +#endif + if (tessPipe) + { + pipeline = tessPipe; + indirectArgs = nullptr; + } + else if (ctx.isHUD) + { + if (IsScopeBatch(batch) && state.hudScopePS) + pipeline = SelectHUDScopePipeline(state, batch.vertexStride, batch.skinningRenderMode); + else + pipeline = SelectHUDSkinnedPipeline(state, batch.vertexStride, batch.skinningRenderMode); + } + else + { + pipeline = SelectSkinnedPipeline(state, batch.vertexStride, batch.skinningRenderMode); + } } if (!pipeline) continue; @@ -530,9 +800,189 @@ static void DrawSkinnedBatch( } } -// ═══════════════════════════════════════════════════════════════════════════ -// SKINNING PASS SETUP -// ═══════════════════════════════════════════════════════════════════════════ +framegraph::VirtualResourceHandle setupSkinnedDepthPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle depthInput, + const GeometryCollector* geometry, + MaterialCache* materialCache, + u32 width, + u32 height, + fg::GPUCullingManager* gpuCulling, + SkinningPassState* state, + decals::OverlayManager* overlayMgr) +{ + using namespace framegraph; + + if (!device || !state || !depthInput.is_valid() || !geometry) + return depthInput; + + { + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA8_UNORM); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA32_FLOAT); + fbInfo.depthFormat = nvrhi::Format::D32; + InitializeSkinningResources(device, fbInfo, *state); + } + + if (!state->depthLayout || !state->depthPS) + return depthInput; + + struct DepthPassData + { + VirtualResourceHandle depth; + fg::RenderDevice* device = nullptr; + const GeometryCollector* geometry = nullptr; + MaterialCache* materialCache = nullptr; + fg::GPUCullingManager* gpuCulling = nullptr; + SkinningPassState* passState = nullptr; + decals::OverlayManager* overlayMgr = nullptr; + u32 width = 0, height = 0; + }; + + auto& passData = fg.addCallbackPass( + "SkinnedDepth", + [&, width, height, geometry, materialCache, gpuCulling, state, overlayMgr, depthInput]( + FrameGraph& builder, PassHandle passHandle, DepthPassData& data) { + RenderPassBuilder passBuilder(builder, passHandle); + data.depth = passBuilder.readWrite(depthInput, ResourceState::DepthStencilWrite); + data.device = device; + data.geometry = geometry; + data.materialCache = materialCache; + data.gpuCulling = gpuCulling; + data.passState = state; + data.overlayMgr = overlayMgr; + data.width = width; + data.height = height; + }, + [](const DepthPassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + if (!data.passState || !data.geometry || !data.device) + return; + + u32 worldSkinnedCount = 0; + for (const auto& batch : data.geometry->GetBatches()) + { + if (batch.isSkinned) + ++worldSkinnedCount; + } + if (worldSkinnedCount == 0) + return; + + auto* depthRT = fgGraph.GetPhysicalTexture(data.depth); + if (!depthRT) + return; + + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + if (!nvDevice || !cmdList) + return; + + GPUCullingManager* gpuCullMgr = data.gpuCulling; + if (!gpuCullMgr || !gpuCullMgr->GetGlobalBoneBuffer()) + return; + + if (data.materialCache) + data.materialCache->FinalizePendingMaterials(ctx); + MaterialBuffer::Instance().Upload(ctx); + if (data.overlayMgr) + data.overlayMgr->UploadSplats(cmdList); + + nvrhi::FramebufferDesc fbDesc; + fbDesc.setDepthAttachment(depthRT); + auto& cache = GetPassResourceCache(); + auto framebuffer = cache.GetOrCreateFramebuffer("SkinnedDepth", fbDesc, nvDevice); + if (!framebuffer) + return; + + auto* backend = data.device->GetBackend(); + nvrhi::IDescriptorTable* bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; + + auto dynTransformsCB = cache.GetOrCreateVolatileCB( + "SkinnedDepth", "DynTransforms", sizeof(DynamicTransforms), data.device, 1024 * 8); + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + auto materialIdCB = cache.GetOrCreateVolatileCB( + "SkinnedDepth", "MaterialId", sizeof(SkinnedMaterialCB), data.device, 1024 * 8); + if (!dynTransformsCB || !staticGlobalsCB || !materialIdCB) + return; + + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* vsReflection = shaderLoader->GetCachedReflection("bindless_skinned", ".vs"); + auto* psReflection = shaderLoader->GetCachedReflection("bindless_skinned_depth", ".ps"); + if (!vsReflection || !psReflection || !data.passState->depthLayout) + return; + + nvrhi::IBuffer* splatBuffer = data.overlayMgr ? data.overlayMgr->GetSplatBuffer() : nullptr; + BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "SkinnedDepth"); + bsb.ConstantBuffer("dynamic_transforms", dynTransformsCB); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.BufferSRV("g_BoneMatrices", gpuCullMgr->GetGlobalBoneBuffer()); + bsb.ConstantBuffer("SkinnedMaterialCB", materialIdCB); + BindBindlessMaterialTables(bsb); + BindPaintSplatBuffer(bsb, nvDevice, splatBuffer); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.passState->depthLayout, nvDevice); + if (!bindingSet) + return; + + nvrhi::Viewport viewport( + 0.0f, static_cast(data.width), + 0.0f, static_cast(data.height), + 0.0f, 1.0f); + nvrhi::Rect scissor(data.width, data.height); + + for (const auto& batch : data.geometry->GetBatches()) + { + if (!batch.isSkinned || !batch.vertexBuffer || !batch.indexBuffer) + continue; + + nvrhi::IGraphicsPipeline* pipeline = SelectSkinnedDepthPipeline( + *data.passState, batch.vertexStride, batch.skinningRenderMode); + if (!pipeline) + continue; + + u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); + auto sr = GetSplatRange(batch, data.overlayMgr); + + DynamicTransforms dynTransData = {}; + FillDynamicTransforms(dynTransData, batch.worldMatrix); + cmdList->writeBuffer(dynTransformsCB, &dynTransData, sizeof(dynTransData)); + + SkinnedMaterialCB matIdData = {}; + matIdData.materialID = batch.bindlessMaterialID; + matIdData.skeletonBoneOffset = boneOffset; + matIdData.splatOffset = sr.offset; + matIdData.splatCount = sr.count; + cmdList->writeBuffer(materialIdCB, &matIdData, sizeof(matIdData)); + + nvrhi::GraphicsState gfxState; + gfxState.pipeline = pipeline; + gfxState.framebuffer = framebuffer; + gfxState.bindings = {bindingSet}; + if (bindlessTable) + gfxState.addBindingSet(bindlessTable); + gfxState.vertexBuffers = {{batch.vertexBuffer, 0, 0}}; + gfxState.indexBuffer = {batch.indexBuffer, nvrhi::Format::R16_UINT, 0}; + gfxState.viewport.addViewport(viewport); + gfxState.viewport.addScissorRect(scissor); + + cmdList->setGraphicsState(gfxState); + cmdList->drawIndexed( + nvrhi::DrawArguments() + .setVertexCount(batch.indexCount) + .setStartIndexLocation(batch.startIndex) + .setStartVertexLocation(batch.baseVertex)); + } + }); + + return passData.depth; +} framegraph::DefaultOutputLayout setupSkinningPass( framegraph::FrameGraph& fg, @@ -546,7 +996,19 @@ framegraph::DefaultOutputLayout setupSkinningPass( fg::GPUCullingManager* gpuCulling, framegraph::VirtualResourceHandle skinnedDrawArgs, SkinningPassState* state, - decals::OverlayManager* overlayMgr) + decals::OverlayManager* overlayMgr, + nvrhi::ITexture* shadowMapArray, + framegraph::VirtualResourceHandle shadowMapHandle, + nvrhi::ITexture* contactDepth, + nvrhi::ITexture* contactHistory, + nvrhi::ITexture* envSky0, + nvrhi::ITexture* envSky1, + nvrhi::ITexture* hudShadowMap, + nvrhi::ITexture* const* shadowCascadesIn, + nvrhi::ITexture* localShadowAtlas, + nvrhi::ITexture* const* shadowHZBIn, + nvrhi::ITexture* shadowMask, + nvrhi::ITexture* localShadowESM) { using namespace framegraph; @@ -555,6 +1017,7 @@ framegraph::DefaultOutputLayout setupSkinningPass( fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); fbInfo.colorFormats.push_back(nvrhi::Format::RGBA8_UNORM); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA32_FLOAT); fbInfo.depthFormat = nvrhi::Format::D32; InitializeSkinningResources(device, fbInfo, *state); } @@ -565,7 +1028,8 @@ framegraph::DefaultOutputLayout setupSkinningPass( // ═══════════════════════════════════════════════════════ // SETUP LAMBDA // ═══════════════════════════════════════════════════════ - [&, width, height, gpuCulling, skinnedDrawArgs, state, overlayMgr](FrameGraph& builder, PassHandle passHandle, SkinningPassData& data) { + [&, width, height, gpuCulling, skinnedDrawArgs, state, overlayMgr, + shadowMapArray, shadowMapHandle, contactDepth, contactHistory, envSky0, envSky1, hudShadowMap, shadowCascadesIn, localShadowAtlas, shadowHZBIn, shadowMask, localShadowESM](FrameGraph& builder, PassHandle passHandle, SkinningPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.width = width; @@ -577,19 +1041,38 @@ framegraph::DefaultOutputLayout setupSkinningPass( data.gpuCulling = gpuCulling; data.passState = state; data.overlayMgr = overlayMgr; + data.shadowMapArray = shadowMapArray; + for (int i = 0; i < 3; ++i) + { + data.shadowCascades[i] = (shadowCascadesIn && shadowCascadesIn[i]) ? shadowCascadesIn[i] : shadowMapArray; + data.shadowHZB[i] = (shadowHZBIn && shadowHZBIn[i]) ? shadowHZBIn[i] : nullptr; + } + data.hudShadowMap = hudShadowMap; + data.localShadowAtlas = localShadowAtlas; + data.localShadowESM = localShadowESM; + data.shadowMask = shadowMask; + data.contactDepth = contactDepth; + data.contactHistory = contactHistory; + data.envSky0 = envSky0; + data.envSky1 = envSky1; if (skinnedDrawArgs.is_valid()) data.skinnedDrawArgs = passBuilder.read(skinnedDrawArgs, ResourceState::IndirectArgument); + if (shadowMapHandle.is_valid()) + data.shadowMap = passBuilder.read(shadowMapHandle, ResourceState::ShaderResource); data.color = passBuilder.readWrite(inputs.albedo, ResourceState::RenderTarget); data.normal = passBuilder.readWrite(inputs.normal, ResourceState::RenderTarget); if (inputs.baseColor.is_valid()) data.baseColor = passBuilder.readWrite(inputs.baseColor, ResourceState::RenderTarget); + if (inputs.worldPos.is_valid()) + data.worldPos = passBuilder.readWrite(inputs.worldPos, ResourceState::RenderTarget); data.depth = passBuilder.readWrite(inputs.depth, ResourceState::DepthStencilWrite); data.outputs.albedo = data.color; data.outputs.normal = data.normal; data.outputs.baseColor = data.baseColor; + data.outputs.worldPos = data.worldPos; data.outputs.depth = data.depth; }, @@ -622,6 +1105,7 @@ framegraph::DefaultOutputLayout setupSkinningPass( auto* colorRT = fg.GetPhysicalTexture(data.color); auto* normalRT = fg.GetPhysicalTexture(data.normal); auto* baseColorRT = data.baseColor.is_valid() ? fg.GetPhysicalTexture(data.baseColor) : nullptr; + auto* worldPosRT = data.worldPos.is_valid() ? fg.GetPhysicalTexture(data.worldPos) : nullptr; auto* depthRT = fg.GetPhysicalTexture(data.depth); if (!colorRT || !depthRT) return; @@ -637,6 +1121,8 @@ framegraph::DefaultOutputLayout setupSkinningPass( fbDesc.addColorAttachment(normalRT); if (baseColorRT) fbDesc.addColorAttachment(baseColorRT); + if (worldPosRT) + fbDesc.addColorAttachment(worldPosRT); fbDesc.setDepthAttachment(depthRT); auto framebuffer = framegraph::GetPassResourceCache().GetOrCreateFramebuffer("SkinningPass", fbDesc, nvDevice); if (!framebuffer) @@ -709,7 +1195,9 @@ framegraph::DefaultOutputLayout setupSkinningPass( *data.passState, nvDevice, framebuffer, dynTransformsCB, staticGlobalsCB, materialIdCB, globalBoneBuffer, bindlessTable, bindlessLayout, splatBuffer, - worldViewport, scissor, false); + worldViewport, scissor, false, + data.shadowCascades, data.hudShadowMap, data.contactDepth, data.contactHistory, data.envSky0, data.envSky1, data.localShadowAtlas, + data.shadowHZB, data.shadowMask, data.localShadowESM); const bool cullActive = data.skinnedDrawArgs.is_valid() && gpuCullMgr->IsSkinnedCullingEnabled() @@ -741,14 +1229,17 @@ framegraph::DefaultOutputLayout setupSkinningPass( framegraph::BindingSetBuilder bsb(*mdiVsRefl, *mdiPsRefl, nvDevice, "Skinning.MDI"); bsb.ConstantBuffer("static_globals", staticGlobalsCB); bsb.BufferSRV("g_BoneMatrices", globalBoneBuffer); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); - bsb.BufferSRV("g_PaintSplats", splatBuffer); + BindBindlessMaterialTables(bsb); + BindPaintSplatBuffer(bsb, nvDevice, splatBuffer); bsb.BufferSRV("g_SkinnedRecords", bucket.recordsBuffer); bsb.BufferSRV("g_SkinnedCompactIndices", bucket.compactBatchIndicesBuffer); bsb.BufferSRV("g_SkinnedCompactMaterialIDs", bucket.compactMaterialIDBuffer); bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); + if (ClusteredLightManager::Instance().GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", ClusteredLightManager::Instance().GetShadowDataBuffer()); bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); + BindSkinnedLightingTextures(bsb, nvDevice, data.shadowCascades, data.hudShadowMap, data.contactDepth, data.contactHistory, data.envSky0, data.envSky1, false, data.localShadowAtlas, data.shadowHZB, data.shadowMask, data.localShadowESM); auto& cache = framegraph::GetPassResourceCache(); nvrhi::BindingSetHandle mdiBindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.passState->mdiLayout, nvDevice); @@ -797,40 +1288,272 @@ framegraph::DefaultOutputLayout setupSkinningPass( } } - // ═══════════════════════════════════════════════════════ - // PHASE 2: HUD SKINNED MESHES (depth [0.9, 1.0]) - // ═══════════════════════════════════════════════════════ - if (hasHUDSkinned) { - nvrhi::Viewport hudViewport( - 0.0f, static_cast(rtDesc.width), - 0.0f, static_cast(rtDesc.height), - 0.9f, 1.0f - ); - - SkinnedPhaseContext hudCtx = BuildSkinnedPhaseContext( - *data.passState, nvDevice, framebuffer, - dynTransformsCB, staticGlobalsCB, materialIdCB, - globalBoneBuffer, bindlessTable, bindlessLayout, splatBuffer, - hudViewport, scissor, true); - - for (const auto& batch : *data.hudBatches) { - Fmatrix adjustedWorldMatrix = ApplyHUDFOVAdjustment(batch.worldMatrix); - u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); - - DrawSkinnedBatch(*data.passState, cmdList, nvDevice, hudCtx, - batch, adjustedWorldMatrix, boneOffset, {0, 0}); - } - } } ); - // Return outputs DefaultOutputLayout outputs; outputs.albedo = passData.color; outputs.normal = passData.normal; outputs.baseColor = passData.baseColor; + outputs.worldPos = passData.worldPos; outputs.depth = passData.depth; return outputs; } +framegraph::VirtualResourceHandle setupHudOverlayPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle normal, + framegraph::VirtualResourceHandle baseColor, + framegraph::VirtualResourceHandle worldPos, + const xr_vector* hudBatches, + MaterialCache* materialCache, + u32 width, + u32 height, + fg::GPUCullingManager* gpuCulling, + SkinningPassState* state, + nvrhi::ITexture* shadowMapArray, + nvrhi::ITexture* contactDepth, + nvrhi::ITexture* contactHistory, + nvrhi::ITexture* envSky0, + nvrhi::ITexture* envSky1, + nvrhi::ITexture* hudShadowMap, + nvrhi::ITexture* const* shadowCascadesIn, + nvrhi::ITexture* localShadowAtlas, + nvrhi::ITexture* const* shadowHZBIn, + nvrhi::ITexture* shadowMask, + nvrhi::ITexture* localShadowESM, + bool rtgiGuidePass) +{ + using namespace framegraph; + + if (!device || !state || !hudBatches || hudBatches->empty() || !sceneColor.is_valid() || !depth.is_valid()) + return sceneColor; + + { + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA8_UNORM); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA32_FLOAT); + fbInfo.depthFormat = nvrhi::Format::D32; + InitializeSkinningResources(device, fbInfo, *state); + } + + struct HudOverlayPassData { + VirtualResourceHandle color; + VirtualResourceHandle normal; + VirtualResourceHandle baseColor; + VirtualResourceHandle worldPos; + VirtualResourceHandle depth; + fg::RenderDevice* device = nullptr; + const xr_vector* hudBatches = nullptr; + MaterialCache* materialCache = nullptr; + fg::GPUCullingManager* gpuCulling = nullptr; + SkinningPassState* passState = nullptr; + u32 width = 0; + u32 height = 0; + nvrhi::ITexture* shadowCascades[3] = {}; + nvrhi::ITexture* hudShadowMap = nullptr; + nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* localShadowESM = nullptr; + nvrhi::ITexture* shadowHZB[3] = {}; + nvrhi::ITexture* shadowMask = nullptr; + nvrhi::ITexture* contactDepth = nullptr; + nvrhi::ITexture* contactHistory = nullptr; + nvrhi::ITexture* envSky0 = nullptr; + nvrhi::ITexture* envSky1 = nullptr; + bool rtgiGuidePass = false; + }; + + auto& passData = fg.addCallbackPass( + rtgiGuidePass ? "HUD RTGI Guide" : "HUD Overlay", + [&, width, height, gpuCulling, state, materialCache, shadowMapArray, contactDepth, contactHistory, + envSky0, envSky1, hudShadowMap, shadowCascadesIn, localShadowAtlas, shadowHZBIn, shadowMask, localShadowESM, rtgiGuidePass]( + FrameGraph& builder, PassHandle passHandle, HudOverlayPassData& data) { + RenderPassBuilder passBuilder(builder, passHandle); + data.width = width; + data.height = height; + data.device = device; + data.hudBatches = hudBatches; + data.materialCache = materialCache; + data.gpuCulling = gpuCulling; + data.passState = state; + data.rtgiGuidePass = rtgiGuidePass; + for (int i = 0; i < 3; ++i) + { + data.shadowCascades[i] = (shadowCascadesIn && shadowCascadesIn[i]) ? shadowCascadesIn[i] : shadowMapArray; + data.shadowHZB[i] = (shadowHZBIn && shadowHZBIn[i]) ? shadowHZBIn[i] : nullptr; + } + data.hudShadowMap = hudShadowMap; + data.localShadowAtlas = localShadowAtlas; + data.localShadowESM = localShadowESM; + data.shadowMask = shadowMask; + data.contactDepth = contactDepth; + data.contactHistory = contactHistory; + data.envSky0 = envSky0; + data.envSky1 = envSky1; + + data.color = passBuilder.readWrite(sceneColor, ResourceState::RenderTarget); + data.depth = passBuilder.readWrite(depth, ResourceState::DepthStencilWrite); + if (normal.is_valid()) + data.normal = passBuilder.readWrite(normal, ResourceState::RenderTarget); + if (baseColor.is_valid()) + data.baseColor = passBuilder.readWrite(baseColor, ResourceState::RenderTarget); + if (worldPos.is_valid()) + data.worldPos = passBuilder.readWrite(worldPos, ResourceState::RenderTarget); + }, + [](const HudOverlayPassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + if (!data.passState || !data.hudBatches || data.hudBatches->empty()) + return; + if (!data.gpuCulling || !data.gpuCulling->GetGlobalBoneBuffer()) + return; + if (!TerrainMaterialBuffer::Instance().GetBuffer()) + return; + + auto* colorRT = fgGraph.GetPhysicalTexture(data.color); + auto* depthRT = fgGraph.GetPhysicalTexture(data.depth); + auto* normalRT = data.normal.is_valid() ? fgGraph.GetPhysicalTexture(data.normal) : nullptr; + auto* baseColorRT = data.baseColor.is_valid() ? fgGraph.GetPhysicalTexture(data.baseColor) : nullptr; + auto* worldPosRT = data.worldPos.is_valid() ? fgGraph.GetPhysicalTexture(data.worldPos) : nullptr; + if (!colorRT || !depthRT || !normalRT || !baseColorRT || !worldPosRT) + return; + + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + if (!nvDevice || !cmdList || !data.passState->initialized) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(colorRT); + if (normalRT) + fbDesc.addColorAttachment(normalRT); + if (baseColorRT) + fbDesc.addColorAttachment(baseColorRT); + if (worldPosRT) + fbDesc.addColorAttachment(worldPosRT); + fbDesc.setDepthAttachment(depthRT); + auto framebuffer = framegraph::GetPassResourceCache().GetOrCreateFramebuffer( + data.rtgiGuidePass ? "HudRtgiGuidePass" : "HudOverlayPass", fbDesc, nvDevice); + if (!framebuffer) + return; + + GPUCullingManager* gpuCullMgr = data.gpuCulling; + nvrhi::IBuffer* globalBoneBuffer = gpuCullMgr->GetGlobalBoneBuffer(); + auto& cache = framegraph::GetPassResourceCache(); + auto dynTransformsCB = cache.GetOrCreateVolatileCB("HudOverlay", "DynTransforms", sizeof(DynamicTransforms), data.device, 1024 * 8); + auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + auto materialIdCB = cache.GetOrCreateVolatileCB("HudOverlay", "MaterialId", sizeof(SkinnedMaterialCB), data.device, 1024 * 8); + + { + StaticGlobals sg = BuildStaticGlobals(2.0f, data.rtgiGuidePass); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + + if (data.materialCache) + data.materialCache->FinalizePendingMaterials(ctx); + MaterialBuffer::Instance().Upload(ctx); + + auto* backend = data.device->GetBackend(); + nvrhi::IDescriptorTable* bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; + nvrhi::IBindingLayout* bindlessLayout = backend ? backend->GetBindlessLayout() : nullptr; + + const auto& rtDesc = colorRT->getDesc(); + nvrhi::Viewport hudViewport( + 0.0f, static_cast(rtDesc.width), + 0.0f, static_cast(rtDesc.height), + 0.0f, 0.1f); + nvrhi::Rect scissor(rtDesc.width, rtDesc.height); + + bool hasScopeBatch = false; + for (const auto& batch : *data.hudBatches) { + if (IsScopeBatch(batch)) { hasScopeBatch = true; break; } + } + + if (hasScopeBatch && data.passState->hudScopePS) + { + auto& st = *data.passState; + const u32 w = rtDesc.width; + const u32 h = rtDesc.height; + if (!st.secondVP || st.secondVP->getDesc().width != w || st.secondVP->getDesc().height != h) + { + nvrhi::TextureDesc td = colorRT->getDesc(); + td.width = w; + td.height = h; + td.debugName = "rt_SecondVP"; + td.isRenderTarget = false; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + st.secondVP = nvDevice->createTexture(td); + } + if (st.secondVP) + { + cmdList->copyTexture( + st.secondVP, nvrhi::TextureSlice(), + colorRT, nvrhi::TextureSlice()); + } + } + + SkinnedPhaseContext hudCtx = BuildSkinnedPhaseContext( + *data.passState, nvDevice, framebuffer, + dynTransformsCB, staticGlobalsCB, materialIdCB, + globalBoneBuffer, bindlessTable, bindlessLayout, nullptr, + hudViewport, scissor, true, + data.shadowCascades, data.hudShadowMap, data.contactDepth, data.contactHistory, data.envSky0, data.envSky1, data.localShadowAtlas, + data.shadowHZB, data.shadowMask, data.localShadowESM); + + SkinnedPhaseContext scopeCtx = hudCtx; + if (hasScopeBatch && data.passState->hudScopePS && data.passState->hudScopeLayout && data.passState->secondVP) + { + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* vsReflection = shaderLoader->GetCachedReflection("bindless_skinned", ".vs"); + auto* psReflection = shaderLoader->GetCachedReflection("bindless_skinned_scope", ".ps"); + if (vsReflection && psReflection) + { + struct alignas(16) ScopeCB { Fvector4 hud; Fvector4 zoom; }; + ScopeCB scb{}; + if (g_pGamePersistent && g_pGamePersistent->m_pGShaderConstants) + { + scb.hud = g_pGamePersistent->m_pGShaderConstants->hud_params; + scb.zoom.set(0.f, 0.f, 1.f, 0.f); + } + else + scb.hud.set(0.f, 0.f, 0.015f, 1.f); + + auto* scopeParamsCB = cache.GetOrCreateVolatileCB( + "HudOverlay", "ScopeParams", sizeof(ScopeCB), data.device); + if (scopeParamsCB) + cmdList->writeBuffer(scopeParamsCB, &scb, sizeof(scb)); + + framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "HudOverlay.Scope"); + bsb.ConstantBuffer("dynamic_transforms", dynTransformsCB); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.BufferSRV("g_BoneMatrices", globalBoneBuffer); + bsb.ConstantBuffer("SkinnedMaterialCB", materialIdCB); + BindBindlessMaterialTables(bsb); + if (scopeParamsCB) + bsb.ConstantBuffer("ScopeParams", scopeParamsCB); + bsb.Texture("s_vp2", data.passState->secondVP); + scopeCtx.bindingSet = cache.GetOrCreateBindingSet( + bsb.Build(), data.passState->hudScopeLayout, nvDevice); + } + } + + for (const auto& batch : *data.hudBatches) { + Fmatrix adjustedWorldMatrix = ApplyHUDFOVAdjustment(batch.worldMatrix); + u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); + const SkinnedPhaseContext& drawCtx = + (IsScopeBatch(batch) && scopeCtx.bindingSet) ? scopeCtx : hudCtx; + + DrawSkinnedBatch(*data.passState, cmdList, nvDevice, drawCtx, + batch, adjustedWorldMatrix, boneOffset, {0, 0}); + } + } + ); + + return passData.color; +} + } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h index cb015077795..e4258b217e2 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h @@ -30,13 +30,6 @@ namespace xray::render::fg::passes { // ═══════════════════════════════════════════════════════════════════════════ // SKINNING PASS - Consolidated skinned mesh rendering // ═══════════════════════════════════════════════════════════════════════════ -// Renders all skinned meshes in two phases: -// 1. World Phase - NPCs, monsters, etc. with normal depth [0.0, 1.0] -// 2. HUD Phase - First-person weapons/hands with compressed depth [0.9, 1.0] -// -// This pass consolidates all skinned mesh rendering that was previously split -// between ForwardColorPass (world skinned) and HUDPass (HUD skinned). - struct SkinningPipelineVariant { nvrhi::GraphicsPipelineHandle pipeline; nvrhi::InputLayoutHandle inputLayout; @@ -49,10 +42,20 @@ struct SkinningPassState { SkinningPipelineVariant hq2w; SkinningPipelineVariant hq3w; SkinningPipelineVariant hq4w; + // World-only PatchList variants (same VS/PS, + bindless_skinned_tess HS/DS). + SkinningPipelineVariant tessNonHQ; + SkinningPipelineVariant tessHQ1w; + SkinningPipelineVariant tessHQ2w; + SkinningPipelineVariant tessHQ3w; + SkinningPipelineVariant tessHQ4w; nvrhi::BindingLayoutHandle layout; nvrhi::BindingLayoutHandle hudLayout; + nvrhi::BindingLayoutHandle hudScopeLayout; nvrhi::ShaderHandle ps; nvrhi::ShaderHandle hudPS; + nvrhi::ShaderHandle hudScopePS; + nvrhi::ShaderHandle tessHS; + nvrhi::ShaderHandle tessDS; SkinningPipelineVariant mdiNonHQ; SkinningPipelineVariant mdiHQ1w; SkinningPipelineVariant mdiHQ2w; @@ -65,6 +68,19 @@ struct SkinningPassState { SkinningPipelineVariant hudHQ2w; SkinningPipelineVariant hudHQ3w; SkinningPipelineVariant hudHQ4w; + SkinningPipelineVariant hudScopeNonHQ; + SkinningPipelineVariant hudScopeHQ1w; + SkinningPipelineVariant hudScopeHQ2w; + SkinningPipelineVariant hudScopeHQ3w; + SkinningPipelineVariant hudScopeHQ4w; + SkinningPipelineVariant depthNonHQ; + SkinningPipelineVariant depthHQ1w; + SkinningPipelineVariant depthHQ2w; + SkinningPipelineVariant depthHQ3w; + SkinningPipelineVariant depthHQ4w; + nvrhi::BindingLayoutHandle depthLayout; + nvrhi::ShaderHandle depthPS; + nvrhi::TextureHandle secondVP; nvrhi::SamplerHandle linearSampler; bool initialized = false; }; @@ -75,8 +91,10 @@ struct SkinningPassData { framegraph::VirtualResourceHandle color; framegraph::VirtualResourceHandle normal; framegraph::VirtualResourceHandle baseColor; + framegraph::VirtualResourceHandle worldPos; framegraph::VirtualResourceHandle depth; framegraph::VirtualResourceHandle skinnedDrawArgs; + framegraph::VirtualResourceHandle shadowMap; fg::RenderDevice* device; const GeometryCollector* geometry; const xr_vector* hudBatches; @@ -86,23 +104,85 @@ struct SkinningPassData { framegraph::DefaultOutputLayout outputs; SkinningPassState* passState; decals::OverlayManager* overlayMgr; + nvrhi::ITexture* shadowMapArray = nullptr; + nvrhi::ITexture* shadowCascades[3] = {}; + nvrhi::ITexture* hudShadowMap = nullptr; + nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* localShadowESM = nullptr; + nvrhi::ITexture* shadowHZB[3] = {}; + nvrhi::ITexture* shadowMask = nullptr; + nvrhi::ITexture* contactDepth = nullptr; + nvrhi::ITexture* contactHistory = nullptr; + nvrhi::ITexture* envSky0 = nullptr; + nvrhi::ITexture* envSky1 = nullptr; }; -// Main skinning pass setup function -// Renders world skinned meshes followed by HUD skinned meshes +framegraph::VirtualResourceHandle setupSkinnedDepthPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle depthInput, + const GeometryCollector* geometry, + MaterialCache* materialCache, + u32 width, + u32 height, + fg::GPUCullingManager* gpuCulling, + SkinningPassState* state, + decals::OverlayManager* overlayMgr = nullptr +); + framegraph::DefaultOutputLayout setupSkinningPass( framegraph::FrameGraph& fg, fg::RenderDevice* device, const framegraph::DefaultOutputLayout& inputs, const GeometryCollector* geometry, // Contains world skinned batches - const xr_vector* hudBatches, // HUD skinned batches (weapons, hands) + const xr_vector* hudBatches, // unused when HUD is deferred; pass nullptr MaterialCache* materialCache, u32 width, u32 height, fg::GPUCullingManager* gpuCulling = nullptr, framegraph::VirtualResourceHandle skinnedDrawArgs = {}, SkinningPassState* state = nullptr, - decals::OverlayManager* overlayMgr = nullptr + decals::OverlayManager* overlayMgr = nullptr, + nvrhi::ITexture* shadowMapArray = nullptr, + framegraph::VirtualResourceHandle shadowMapHandle = {}, + nvrhi::ITexture* contactDepth = nullptr, + nvrhi::ITexture* contactHistory = nullptr, + nvrhi::ITexture* envSky0 = nullptr, + nvrhi::ITexture* envSky1 = nullptr, + nvrhi::ITexture* hudShadowMap = nullptr, + nvrhi::ITexture* const* shadowCascades = nullptr, + nvrhi::ITexture* localShadowAtlas = nullptr, + nvrhi::ITexture* const* shadowHZB = nullptr, + nvrhi::ITexture* shadowMask = nullptr, + nvrhi::ITexture* localShadowESM = nullptr +); + +framegraph::VirtualResourceHandle setupHudOverlayPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle sceneColor, + framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle normal, + framegraph::VirtualResourceHandle baseColor, + framegraph::VirtualResourceHandle worldPos, + const xr_vector* hudBatches, + MaterialCache* materialCache, + u32 width, + u32 height, + fg::GPUCullingManager* gpuCulling, + SkinningPassState* state, + nvrhi::ITexture* shadowMapArray = nullptr, + nvrhi::ITexture* contactDepth = nullptr, + nvrhi::ITexture* contactHistory = nullptr, + nvrhi::ITexture* envSky0 = nullptr, + nvrhi::ITexture* envSky1 = nullptr, + nvrhi::ITexture* hudShadowMap = nullptr, + nvrhi::ITexture* const* shadowCascades = nullptr, + nvrhi::ITexture* localShadowAtlas = nullptr, + nvrhi::ITexture* const* shadowHZB = nullptr, + nvrhi::ITexture* shadowMask = nullptr, + nvrhi::ITexture* localShadowESM = nullptr, + bool rtgiGuidePass = false ); } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp index 182de8a4439..86b9529a5a5 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "TransparentPassSetup.h" +#include "IBLPrefilterPassSetup.h" #include "ShaderConstants.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/IPass.h" @@ -9,12 +10,18 @@ #include "Layers/xrRender/RenderContext/RenderDevice.h" #include "Layers/xrRender/Backend/D3D12Backend.h" #include "Layers/xrRender/Bindless/MaterialBuffer.h" +#include "Layers/xrRender/Bindless/TerrainMaterialBuffer.h" #include "Layers/xrRender/Bindless/VariantTextureBuffer.h" #include "Layers/xrRender/ShaderVariant/VariantPSOCache.h" #include "Layers/xrRender/FrameGraph/PassResourceCache.h" #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" #include "PassCommon.h" #include "Layers/xrRender/ClusteredLightManager.h" +#include "Layers/xrRender/ResourceManager/FGResourceManager.h" +#include "Layers/xrRender/ResourceManager/TextureManager.h" +#include "xrEngine/Environment.h" +#include "xrEngine/IGame_Persistent.h" +#include "Layers/xrRender/xrRender_console.h" namespace xray::render::fg::passes { @@ -40,10 +47,12 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame state.ps = psResult.handle; auto& cache = framegraph::GetPassResourceCache(); - state.layout = cache.GetOrCreateBindingLayoutFromReflection("TransparentPass", *vsResult.reflection, *psResult.reflection, nvDevice); + state.layout = cache.GetOrCreateBindingLayoutFromReflection("TransparentPass_v2_TranspCB", *vsResult.reflection, *psResult.reflection, nvDevice); if (!state.layout) return; + state.sampler = cache.GetLinearWrapSampler(nvDevice); + u32 attrCount = 0; auto* attrs = GetUnifiedVertexAttributes(attrCount); state.inputLayout = nvDevice->createInputLayout(attrs, attrCount, state.vs); @@ -67,9 +76,11 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; pipeDesc.renderState.depthStencilState.depthTestEnable = true; pipeDesc.renderState.depthStencilState.depthWriteEnable = false; - pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; pipeDesc.renderState.rasterState.frontCounterClockwise = false; pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; + pipeDesc.renderState.rasterState.depthBias = -16; + pipeDesc.renderState.rasterState.slopeScaledDepthBias = -2.f; auto& rt0 = pipeDesc.renderState.blendState.targets[0]; rt0.blendEnable = true; @@ -79,15 +90,125 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame rt0.srcBlendAlpha = nvrhi::BlendFactor::One; rt0.destBlendAlpha = nvrhi::BlendFactor::InvSrcAlpha; rt0.blendOpAlpha = nvrhi::BlendOp::Add; + for (u32 rt = 1; rt < 4; ++rt) + pipeDesc.renderState.blendState.targets[rt].setColorWriteMask(nvrhi::ColorMask(0)); - state.pipeline = cache.GetOrCreatePipeline("TransparentPass", pipeDesc, fbInfo, nvDevice); + state.pipeline = cache.GetOrCreatePipeline("TransparentPass_v3_AlphaBias", pipeDesc, fbInfo, nvDevice); if (!state.pipeline) return; + { + nvrhi::GraphicsPipelineDesc mulDesc = pipeDesc; + auto& mulRt = mulDesc.renderState.blendState.targets[0]; + mulRt.blendEnable = true; + mulRt.srcBlend = nvrhi::BlendFactor::DstColor; + mulRt.destBlend = nvrhi::BlendFactor::SrcColor; + mulRt.blendOp = nvrhi::BlendOp::Add; + mulRt.srcBlendAlpha = nvrhi::BlendFactor::Zero; + mulRt.destBlendAlpha = nvrhi::BlendFactor::One; + mulRt.blendOpAlpha = nvrhi::BlendOp::Add; + state.multiplyPipeline = cache.GetOrCreatePipeline("TransparentPass_v3_MultiplyBias", mulDesc, fbInfo, nvDevice); + } + QueryBindingLayoutFromPipeline(state.pipeline, state.layout); + // Dedicated water PSO (soft + foam + SSR) + { + auto waterVs = shaderLoader->LoadVertexShader("water", "main"); + auto waterPs = shaderLoader->LoadPixelShader("water", "main"); + if (waterVs.handle && waterPs.handle && waterVs.reflection && waterPs.reflection) + { + state.waterVs = waterVs.handle; + state.waterPs = waterPs.handle; + state.waterLayout = cache.GetOrCreateBindingLayoutFromReflection( + "TransparentWater_v18_Murk", *waterVs.reflection, *waterPs.reflection, nvDevice); + if (state.waterLayout) + { + nvrhi::GraphicsPipelineDesc waterDesc = pipeDesc; + waterDesc.VS = state.waterVs; + waterDesc.PS = state.waterPs; + waterDesc.inputLayout = nvDevice->createInputLayout(attrs, attrCount, state.waterVs); + if (bindlessLayout) + waterDesc.bindingLayouts = {state.waterLayout, bindlessLayout}; + else + waterDesc.bindingLayouts = {state.waterLayout}; + waterDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + for (u32 rt = 1; rt < 4; ++rt) + { + waterDesc.renderState.blendState.targets[rt].blendEnable = false; + waterDesc.renderState.blendState.targets[rt].setColorWriteMask( + nvrhi::ColorMask::Red | nvrhi::ColorMask::Green | + nvrhi::ColorMask::Blue | nvrhi::ColorMask::Alpha); + } + state.waterPipeline = cache.GetOrCreatePipeline("TransparentWater_v18_Murk", waterDesc, fbInfo, nvDevice); + } + waterVs.reflection = nullptr; + waterPs.reflection = nullptr; + } + if (!state.waterPipeline) + Msg("! [TransparentPass] Water PSO unavailable — water clipped from forward"); + } + + { + auto waterdVs = shaderLoader->LoadVertexShader("waterd", "main"); + auto waterdPs = shaderLoader->LoadPixelShader("waterd", "main"); + if (waterdVs.handle && waterdPs.handle && waterdVs.reflection && waterdPs.reflection) + { + state.waterDistortVs = waterdVs.handle; + state.waterDistortPs = waterdPs.handle; + state.waterDistortLayout = cache.GetOrCreateBindingLayoutFromReflection( + "TransparentWaterDistort_v6_Amp", *waterdVs.reflection, *waterdPs.reflection, nvDevice); + if (state.waterDistortLayout) + { + state.waterDistortInputLayout = + nvDevice->createInputLayout(attrs, attrCount, state.waterDistortVs); + nvrhi::GraphicsPipelineDesc distortDesc; + distortDesc.VS = state.waterDistortVs; + distortDesc.PS = state.waterDistortPs; + distortDesc.inputLayout = state.waterDistortInputLayout; + if (bindlessLayout) + distortDesc.bindingLayouts = {state.waterDistortLayout, bindlessLayout}; + else + distortDesc.bindingLayouts = {state.waterDistortLayout}; + distortDesc.primType = nvrhi::PrimitiveType::TriangleList; + distortDesc.renderState.depthStencilState.depthTestEnable = true; + distortDesc.renderState.depthStencilState.depthWriteEnable = false; + distortDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + distortDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + auto& drt = distortDesc.renderState.blendState.targets[0]; + drt.blendEnable = true; + drt.srcBlend = nvrhi::BlendFactor::One; + drt.destBlend = nvrhi::BlendFactor::One; + drt.blendOp = nvrhi::BlendOp::Add; + drt.srcBlendAlpha = nvrhi::BlendFactor::One; + drt.destBlendAlpha = nvrhi::BlendFactor::One; + drt.blendOpAlpha = nvrhi::BlendOp::Add; + + nvrhi::FramebufferInfoEx distortFb; + distortFb.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); + distortFb.depthFormat = nvrhi::Format::D32; + state.waterDistortPipeline = cache.GetOrCreatePipeline( + "TransparentWaterDistort_v6_Amp", distortDesc, distortFb, nvDevice); + } + waterdVs.reflection = nullptr; + waterdPs.reflection = nullptr; + } + if (!state.waterDistortPipeline) + Msg("! [TransparentPass] Water distort PSO unavailable"); + } + + if (auto* resMgr = device->GetFGResourceManager()) + { + if (auto* texMgr = resMgr->GetTextureManager()) + { + auto foam = texMgr->LoadTexture("water" DELIMITER "water_foam"); + state.foamTexture = texMgr->GetNVRHITexture(foam); + } + } + state.initialized = true; - Msg("* [TransparentPass] Pipeline initialized"); + Msg("* [TransparentPass] Pipeline initialized (water=%d waterd=%d)", + state.waterPipeline ? 1 : 0, state.waterDistortPipeline ? 1 : 0); } framegraph::DefaultOutputLayout setupTransparentPass( @@ -108,6 +229,7 @@ framegraph::DefaultOutputLayout setupTransparentPass( fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); fbInfo.colorFormats.push_back(nvrhi::Format::RGBA8_UNORM); + fbInfo.colorFormats.push_back(nvrhi::Format::RGBA32_FLOAT); fbInfo.depthFormat = nvrhi::Format::D32; InitializeTransparentResources(device, fbInfo, state); @@ -127,6 +249,27 @@ framegraph::DefaultOutputLayout setupTransparentPass( data.depth = passBuilder.read(inputs.depth, ResourceState::DepthStencilRead); if (inputs.baseColor.is_valid()) data.baseColor = passBuilder.readWrite(inputs.baseColor, ResourceState::RenderTarget); + if (inputs.worldPos.is_valid()) + data.worldPos = passBuilder.readWrite(inputs.worldPos, ResourceState::RenderTarget); + for (u32 i = 0; i < 3; ++i) { + if (config.shadowHZBHandles[i].is_valid()) + passBuilder.read(config.shadowHZBHandles[i], ResourceState::ShaderResource); + } + if (config.shadowMaskHandle.is_valid()) + passBuilder.read(config.shadowMaskHandle, ResourceState::ShaderResource); + + if (state.waterDistortPipeline) + { + framegraph::ResourceDesc distDesc; + distDesc.type = framegraph::ResourceDesc::Type::Texture2D; + distDesc.width = width; + distDesc.height = height; + distDesc.format = nvrhi::Format::RGBA16_FLOAT; + distDesc.isRenderTarget = true; + distDesc.isTransient = true; + distDesc.debugName = "rt_Distortion"; + data.distortion = passBuilder.createTexture("rt_Distortion", distDesc); + } }, [](const TransparentPassData& data, @@ -145,6 +288,7 @@ framegraph::DefaultOutputLayout setupTransparentPass( return; auto* baseColorRT = data.baseColor.is_valid() ? fg.GetPhysicalTexture(data.baseColor) : nullptr; + auto* worldPosRT = data.worldPos.is_valid() ? fg.GetPhysicalTexture(data.worldPos) : nullptr; nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(colorRT); @@ -152,6 +296,8 @@ framegraph::DefaultOutputLayout setupTransparentPass( fbDesc.addColorAttachment(normalRT); if (baseColorRT) fbDesc.addColorAttachment(baseColorRT); + if (worldPosRT) + fbDesc.addColorAttachment(worldPosRT); fbDesc.setDepthAttachment(depthRT); auto& cache = framegraph::GetPassResourceCache(); auto framebuffer = cache.GetOrCreateFramebuffer("TransparentPass", fbDesc, nvDevice); @@ -161,82 +307,409 @@ framegraph::DefaultOutputLayout setupTransparentPass( if (!data.passState->initialized || !data.passState->pipeline) return; + if (data.distortion.is_valid()) + { + if (auto* distortRT = fg.GetPhysicalTexture(data.distortion)) + cmdList->clearTextureFloat( + distortRT, nvrhi::AllSubresources, nvrhi::Color(0.f, 0.f, 0.f, 0.f)); + } + using namespace fg::bindless; auto& matBuffer = MaterialBuffer::Instance(); auto lightingCB = cache.GetOrCreateVolatileCB("TransparentPass", "LightingCB", sizeof(LightingConstants), data.device); auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + auto transparentDrawCB = cache.GetOrCreateVolatileCB("Frame", "TransparentDrawCB", 16, data.device); auto drawIndexBuffer = GetOrCreateDrawIndexBuffer("TransparentPass", nvDevice); + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + auto lightingData = FillLightingConstants(); cmdList->writeBuffer(lightingCB, &lightingData, sizeof(lightingData)); const auto& cfg = data.config; auto& variantTexBuffer = bindless::VariantTextureBuffer::Instance(); + auto& clm = ClusteredLightManager::Instance(); + auto& passCache = framegraph::GetPassResourceCache(); + + nvrhi::ITexture* dummy2D = passCache.GetDummyShadowMap2D(nvDevice); + nvrhi::ITexture* sky0 = cfg.envSky0 + ? cfg.envSky0 + : passCache.GetDummyCubeMap(nvDevice); + nvrhi::ITexture* sky1 = cfg.envSky1 + ? cfg.envSky1 + : passCache.GetDummyCubeMap(nvDevice); auto* shaderLoader = GEnv.Render->GetShaderLoader(); auto* vsReflection = shaderLoader->GetCachedReflection("bindless_forward", ".vs"); auto* psReflection = shaderLoader->GetCachedReflection("bindless_forward", ".ps"); - - framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Transparent"); - bsb.ConstantBuffer("static_globals", staticGlobalsCB); - bsb.BufferSRV("g_Materials", matBuffer.GetBuffer()); - bsb.BufferSRV("g_InstanceData", cfg.instanceBuffer); - bsb.BufferSRV("g_CompactBatchIndices", cfg.compactBatchIndicesBuffer); - bsb.BufferSRV("g_CompactMaterialIDs", cfg.compactMaterialIDBuffer); - bsb.BufferSRV("g_LightData", ClusteredLightManager::Instance().GetLightDataBuffer()); - bsb.BufferSRV("g_ClusterGrid", ClusteredLightManager::Instance().GetClusterGridBuffer()); - bsb.BufferSRV("g_LightIndexList", ClusteredLightManager::Instance().GetLightIndexListBuffer()); - - auto transparentBindDesc = bsb.Build(); - auto bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(transparentBindDesc, data.passState->layout, nvDevice); - R_ASSERT2(bindingSet, "Transparent binding set creation failed"); - - nvrhi::GraphicsState state; - state.pipeline = data.passState->pipeline; - state.framebuffer = framebuffer; - state.bindings = { bindingSet }; + if (!vsReflection || !psReflection) + return; auto* backend = data.device->GetBackend(); - if (backend) { - auto* bindlessTable = backend->GetBindlessDescriptorTable(); - if (bindlessTable) - state.addBindingSet(bindlessTable); - } - - state.vertexBuffers = { - {cfg.megaVertexBuffer, 0, 0}, - {drawIndexBuffer, 1, 0} - }; - state.indexBuffer = { cfg.megaIndexBuffer, nvrhi::Format::R32_UINT, 0 }; - state.indirectParams = cfg.compactDrawArgsBuffer; - state.indirectCountBuffer = cfg.compactCountBuffer; + nvrhi::IBindingSet* bindlessTable = nullptr; + if (backend) + bindlessTable = backend->GetBindlessDescriptorTable(); const auto& rtDesc = colorRT->getDesc(); nvrhi::Viewport viewport(0.0f, static_cast(rtDesc.width), 0.0f, static_cast(rtDesc.height), 0.0f, 1.0f); - state.viewport.addViewport(viewport); - state.viewport.addScissorRect(nvrhi::Rect(rtDesc.width, rtDesc.height)); - - if (cfg.variantPartition.Enabled()) { - auto* backendDev = data.device->GetBackend(); - - VariantPartitionDrawConfig vpCfg; - vpCfg.defaultPipeline = data.passState->pipeline.Get(); - vpCfg.inputLayout = data.passState->inputLayout; - vpCfg.passLayout = data.passState->layout; - vpCfg.bindlessLayout = backendDev ? backendDev->GetBindlessLayout() : nullptr; - vpCfg.bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; - vpCfg.megaVertexBuffer = cfg.megaVertexBuffer; - vpCfg.baseBindings = transparentBindDesc; - vpCfg.objectCount = cfg.objectCount; - vpCfg.partition = cfg.variantPartition; - vpCfg.selectTransparent = true; - - DrawVariantPartition(cmdList, nvDevice, framebuffer, state, vpCfg); - } else { - cmdList->setGraphicsState(state); - DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, cfg.objectCount); + + auto drawTransparentBatch = [&](nvrhi::IGraphicsPipeline* pipeline, u32 blendPass) { + if (!pipeline) + return; + + u32 passMode[4] = { blendPass, 0, 0, 0 }; + cmdList->writeBuffer(transparentDrawCB, passMode, sizeof(passMode)); + + framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Transparent"); + bsb.ConstantBuffer("static_globals", staticGlobalsCB); + bsb.ConstantBuffer("TransparentDrawCB", transparentDrawCB); + BindBindlessMaterialTables(bsb); + bsb.BufferSRV("g_InstanceData", cfg.instanceBuffer); + bsb.BufferSRV("g_CompactBatchIndices", cfg.compactBatchIndicesBuffer); + bsb.BufferSRV("g_CompactMaterialIDs", cfg.compactMaterialIDBuffer); + bsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); + if (clm.GetShadowDataBuffer()) + bsb.BufferSRV("g_ShadowData", clm.GetShadowDataBuffer()); + bsb.BufferSRV("g_ClusterGrid", clm.GetClusterGridBuffer()); + bsb.BufferSRV("g_LightIndexList", clm.GetLightIndexListBuffer()); + { + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = cfg.shadowCascades[i] ? cfg.shadowCascades[i] + : (i == 0 && cfg.shadowMapArray ? cfg.shadowMapArray : dummy2D); + if (t) + bsb.Texture(kNames[i], t); + } + } + { + nvrhi::ITexture* contactHist = cfg.contactHistory + ? cfg.contactHistory + : passCache.GetDummyContactHistory(nvDevice); + if (contactHist) + bsb.Texture("g_ContactHistory", contactHist); + } + { + nvrhi::ITexture* localAtlas = cfg.localShadowAtlas + ? cfg.localShadowAtlas + : passCache.GetDummyShadowMap(nvDevice); + if (localAtlas) + bsb.Texture("g_LocalShadowAtlas", localAtlas); + nvrhi::ITexture* localEsm = cfg.localShadowESM + ? cfg.localShadowESM + : passCache.GetDummyLocalShadowESM(nvDevice); + if (localEsm) + bsb.Texture("g_LocalShadowESM", localEsm); + } + { + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + nvrhi::ITexture* dummyHzb = passCache.GetDummyContactDepth(nvDevice); + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* hz = cfg.shadowHZB[i] ? cfg.shadowHZB[i] : dummyHzb; + if (hz) + bsb.Texture(kHzb[i], hz); + } + } + { + nvrhi::ITexture* shadowMask = cfg.shadowMask + ? cfg.shadowMask + : passCache.GetDummyContactHistory(nvDevice); + if (shadowMask) + bsb.Texture("g_ShadowMask", shadowMask); + } + if (sky0) + bsb.Texture("s_env0", sky0); + if (sky1) + bsb.Texture("s_env1", sky1); + BindIBLResources(bsb, GetCurrentIBLBindResources(), nvDevice); + + auto bindingSet = passCache.GetOrCreateBindingSet(bsb.Build(), data.passState->layout, nvDevice); + if (!bindingSet) + return; + + nvrhi::GraphicsState gfxState; + gfxState.pipeline = pipeline; + gfxState.framebuffer = framebuffer; + gfxState.bindings = { bindingSet }; + if (bindlessTable) + gfxState.addBindingSet(bindlessTable); + gfxState.vertexBuffers = { + {cfg.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0} + }; + gfxState.indexBuffer = { cfg.megaIndexBuffer, nvrhi::Format::R32_UINT, 0 }; + gfxState.indirectParams = cfg.compactDrawArgsBuffer; + gfxState.indirectCountBuffer = cfg.compactCountBuffer; + gfxState.viewport.addViewport(viewport); + gfxState.viewport.addScissorRect(nvrhi::Rect(rtDesc.width, rtDesc.height)); + + if (cfg.variantPartition.Enabled()) { + auto* backendDev = data.device->GetBackend(); + + VariantPartitionDrawConfig vpCfg; + vpCfg.defaultPipeline = pipeline; + vpCfg.inputLayout = data.passState->inputLayout; + vpCfg.passLayout = data.passState->layout; + vpCfg.bindlessLayout = backendDev ? backendDev->GetBindlessLayout() : nullptr; + vpCfg.bindlessTable = bindlessTable; + vpCfg.sampler = data.passState->sampler + ? data.passState->sampler.Get() + : passCache.GetLinearWrapSampler(nvDevice); + vpCfg.staticGlobalsCB = staticGlobalsCB; + vpCfg.lightingCB = lightingCB; + vpCfg.materialBuffer = matBuffer.GetBuffer(); + vpCfg.variantTexBuffer = variantTexBuffer.GetBuffer(); + vpCfg.instanceBuffer = cfg.instanceBuffer; + vpCfg.megaVertexBuffer = cfg.megaVertexBuffer; + vpCfg.shadowMapArray = cfg.shadowCascades[0] ? cfg.shadowCascades[0] : cfg.shadowMapArray; + for (u32 i = 0; i < 3; ++i) + { + vpCfg.shadowCascades[i] = cfg.shadowCascades[i]; + vpCfg.shadowHZB[i] = cfg.shadowHZB[i]; + } + vpCfg.localShadowAtlas = cfg.localShadowAtlas; + vpCfg.localShadowESM = cfg.localShadowESM; + vpCfg.contactHistory = cfg.contactHistory; + vpCfg.shadowMask = cfg.shadowMask; + vpCfg.envSky0 = sky0; + vpCfg.envSky1 = sky1; + vpCfg.lightDataBuffer = clm.GetLightDataBuffer(); + vpCfg.shadowDataBuffer = clm.GetShadowDataBuffer(); + vpCfg.clusterGridBuffer = clm.GetClusterGridBuffer(); + vpCfg.lightIndexListBuffer = clm.GetLightIndexListBuffer(); + vpCfg.partition = cfg.variantPartition; + vpCfg.selectTransparent = true; + + DrawVariantPartition(cmdList, nvDevice, framebuffer, gfxState, vpCfg); + } else { + cmdList->setGraphicsState(gfxState); + DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, cfg.objectCount); + } + }; + + drawTransparentBatch(data.passState->pipeline.Get(), 1); + drawTransparentBatch(data.passState->multiplyPipeline.Get(), 2); + + // Second draw: dedicated water PSO (soft depth + foam + SSR) + if (data.passState->waterPipeline && data.passState->waterLayout) + { + auto* waterVsRefl = shaderLoader->GetCachedReflection("water", ".vs"); + auto* waterPsRefl = shaderLoader->GetCachedReflection("water", ".ps"); + if (waterVsRefl && waterPsRefl) + { + // Snapshot HDR + depth for SSR/soft water (cannot sample RTs we write / DSV) + const auto& cdesc = colorRT->getDesc(); + if (!data.passState->waterSsrColor || + data.passState->waterSsrColor->getDesc().width != cdesc.width || + data.passState->waterSsrColor->getDesc().height != cdesc.height) + { + nvrhi::TextureDesc td = cdesc; + td.debugName = "WaterSSR_Color"; + td.isRenderTarget = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + data.passState->waterSsrColor = nvDevice->createTexture(td); + } + nvrhi::ITexture* ssrColor = data.passState->waterSsrColor; + nvrhi::ITexture* sceneDepthSrv = nullptr; + // Single ping-pong: unbind → copy color+depth → restore for water draw + if (ssrColor) + { + cmdList->setTextureState(colorRT, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmdList->setTextureState(ssrColor, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmdList->copyTexture(ssrColor, nvrhi::TextureSlice(), colorRT, nvrhi::TextureSlice()); + } + + const auto& ddesc = depthRT->getDesc(); + if (!data.passState->waterSceneDepth || + data.passState->waterSceneDepth->getDesc().width != ddesc.width || + data.passState->waterSceneDepth->getDesc().height != ddesc.height) + { + nvrhi::TextureDesc td{}; + td.width = ddesc.width; + td.height = ddesc.height; + td.format = nvrhi::Format::D32; + td.debugName = "WaterSSR_Depth"; + td.isShaderResource = true; + td.isTypeless = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + data.passState->waterSceneDepth = nvDevice->createTexture(td); + } + sceneDepthSrv = data.passState->waterSceneDepth; + if (sceneDepthSrv) + { + cmdList->setTextureState(depthRT, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmdList->setTextureState(sceneDepthSrv, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmdList->copyTexture(sceneDepthSrv, nvrhi::TextureSlice(), depthRT, nvrhi::TextureSlice()); + cmdList->setTextureState(sceneDepthSrv, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + + auto* worldPosRT = data.worldPos.is_valid() ? fg.GetPhysicalTexture(data.worldPos) : nullptr; + if (worldPosRT) + { + const auto& wpDesc = worldPosRT->getDesc(); + if (!data.passState->waterSceneWorldPos || + data.passState->waterSceneWorldPos->getDesc().width != wpDesc.width || + data.passState->waterSceneWorldPos->getDesc().height != wpDesc.height) + { + nvrhi::TextureDesc td = wpDesc; + td.debugName = "WaterSoft_WorldPos"; + td.isRenderTarget = true; + td.isShaderResource = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + data.passState->waterSceneWorldPos = nvDevice->createTexture(td); + } + if (data.passState->waterSceneWorldPos) + { + nvrhi::ITexture* wpCopy = data.passState->waterSceneWorldPos; + cmdList->setTextureState( + worldPosRT, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmdList->setTextureState( + wpCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmdList->copyTexture( + wpCopy, nvrhi::TextureSlice(), worldPosRT, nvrhi::TextureSlice()); + cmdList->setTextureState( + wpCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + } + + if (ssrColor) + cmdList->setTextureState(ssrColor, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmdList->setTextureState(colorRT, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); + cmdList->setTextureState(depthRT, nvrhi::AllSubresources, nvrhi::ResourceStates::DepthRead); + if (worldPosRT) + cmdList->setTextureState( + worldPosRT, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); + + auto waterCB = cache.GetOrCreateVolatileCB( + "TransparentWater", "WaterParams", sizeof(Fvector4), data.device); + Fvector4 wi{}; + float intens = 1.f; + if (g_pGamePersistent) + intens = g_pGamePersistent->Environment().CurrentEnv.m_fWaterIntensity; + intens = std::clamp(intens, 0.f, 1.f); + const float softOn = ps_r2_ls_flags.test(R2FLAG_SOFT_WATER) ? 1.f : 0.f; + wi.set(intens, intens, intens, softOn); + cmdList->writeBuffer(waterCB, &wi, sizeof(wi)); + + nvrhi::ITexture* foam = data.passState->foamTexture; + if (!foam) + foam = passCache.GetDummyShadowMap2D(nvDevice); + + framegraph::BindingSetBuilder wbsb(*waterVsRefl, *waterPsRefl, nvDevice, "Transparent.Water"); + wbsb.ConstantBuffer("static_globals", staticGlobalsCB) + .ConstantBuffer("WaterParams", waterCB); + BindBindlessMaterialTables(wbsb); + wbsb.BufferSRV("g_InstanceData", cfg.instanceBuffer) + .BufferSRV("g_CompactBatchIndices", cfg.compactBatchIndicesBuffer) + .BufferSRV("g_CompactMaterialIDs", cfg.compactMaterialIDBuffer); + if (sky0) wbsb.Texture("s_env0", sky0); + if (sky1) wbsb.Texture("s_env1", sky1); + if (foam) wbsb.Texture("s_leaves", foam); + if (ssrColor) wbsb.Texture("g_SceneColor", ssrColor); + if (sceneDepthSrv) wbsb.Texture("g_SceneDepth", sceneDepthSrv); + else wbsb.Texture("g_SceneDepth", passCache.GetDummyContactDepth(nvDevice)); + if (data.passState->waterSceneWorldPos) + wbsb.Texture("g_UnderWorldPos", data.passState->waterSceneWorldPos); + else + wbsb.Texture("g_UnderWorldPos", passCache.GetDummyContactHistory(nvDevice)); + + auto waterSet = passCache.GetOrCreateBindingSet( + wbsb.Build(), data.passState->waterLayout, nvDevice); + if (waterSet) + { + nvrhi::GraphicsState waterGfx; + waterGfx.pipeline = data.passState->waterPipeline; + waterGfx.framebuffer = framebuffer; + waterGfx.bindings = {waterSet}; + if (bindlessTable) + waterGfx.addBindingSet(bindlessTable); + waterGfx.vertexBuffers = { + {cfg.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0} + }; + waterGfx.indexBuffer = { cfg.megaIndexBuffer, nvrhi::Format::R32_UINT, 0 }; + waterGfx.indirectParams = cfg.compactDrawArgsBuffer; + waterGfx.indirectCountBuffer = cfg.compactCountBuffer; + waterGfx.viewport.addViewport(viewport); + waterGfx.viewport.addScissorRect(nvrhi::Rect(rtDesc.width, rtDesc.height)); + cmdList->setGraphicsState(waterGfx); + DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, cfg.objectCount); + } + + if (data.passState->waterDistortPipeline && data.passState->waterDistortLayout && + data.distortion.is_valid()) + { + auto* distortRT = fg.GetPhysicalTexture(data.distortion); + auto* waterdVsRefl = shaderLoader->GetCachedReflection("waterd", ".vs"); + auto* waterdPsRefl = shaderLoader->GetCachedReflection("waterd", ".ps"); + if (distortRT && waterdVsRefl && waterdPsRefl) + { + nvrhi::FramebufferDesc distortFbDesc; + distortFbDesc.addColorAttachment(distortRT); + distortFbDesc.setDepthAttachment(depthRT); + auto distortFB = passCache.GetOrCreateFramebuffer( + "TransparentWaterDistort", distortFbDesc, nvDevice); + if (distortFB) + { + framegraph::BindingSetBuilder dbsb( + *waterdVsRefl, *waterdPsRefl, nvDevice, "Transparent.WaterDistort"); + dbsb.ConstantBuffer("static_globals", staticGlobalsCB) + .ConstantBuffer("WaterParams", waterCB); + BindBindlessMaterialTables(dbsb); + dbsb.BufferSRV("g_InstanceData", cfg.instanceBuffer) + .BufferSRV("g_CompactBatchIndices", cfg.compactBatchIndicesBuffer) + .BufferSRV("g_CompactMaterialIDs", cfg.compactMaterialIDBuffer); + if (sceneDepthSrv) + dbsb.Texture("g_SceneDepth", sceneDepthSrv); + else + dbsb.Texture("g_SceneDepth", passCache.GetDummyContactDepth(nvDevice)); + dbsb.Texture("g_SceneColor", ssrColor + ? ssrColor + : passCache.GetDummyContactHistory(nvDevice)); + if (data.passState->waterSceneWorldPos) + dbsb.Texture("g_UnderWorldPos", data.passState->waterSceneWorldPos); + else + dbsb.Texture("g_UnderWorldPos", passCache.GetDummyContactHistory(nvDevice)); + + auto distortSet = passCache.GetOrCreateBindingSet( + dbsb.Build(), data.passState->waterDistortLayout, nvDevice); + if (distortSet) + { + nvrhi::GraphicsState dgfx; + dgfx.pipeline = data.passState->waterDistortPipeline; + dgfx.framebuffer = distortFB; + dgfx.bindings = {distortSet}; + if (bindlessTable) + dgfx.addBindingSet(bindlessTable); + dgfx.vertexBuffers = { + {cfg.megaVertexBuffer, 0, 0}, + {drawIndexBuffer, 1, 0} + }; + dgfx.indexBuffer = { + cfg.megaIndexBuffer, nvrhi::Format::R32_UINT, 0}; + dgfx.indirectParams = cfg.compactDrawArgsBuffer; + dgfx.indirectCountBuffer = cfg.compactCountBuffer; + dgfx.viewport.addViewport(viewport); + dgfx.viewport.addScissorRect( + nvrhi::Rect(rtDesc.width, rtDesc.height)); + cmdList->setGraphicsState(dgfx); + DrawIndexedIndirectCountOrFallback( + cmdList, 0, 0, cfg.objectCount); + } + } + } + } + } } } ); @@ -245,7 +718,9 @@ framegraph::DefaultOutputLayout setupTransparentPass( outputs.albedo = passData.color; outputs.normal = passData.normal; outputs.baseColor = passData.baseColor; + outputs.worldPos = passData.worldPos; outputs.depth = passData.depth; + outputs.distortion = passData.distortion; return outputs; } diff --git a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.h index 81501646c49..8b2e64356b8 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.h @@ -30,6 +30,20 @@ struct TransparentPassConfig { VariantPartitionConfig variantPartition; + // Cascaded shadow maps + sky cubes for water reflections + nvrhi::ITexture* shadowMapArray = nullptr; + nvrhi::ITexture* shadowCascades[3] = {}; + nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* localShadowESM = nullptr; + nvrhi::ITexture* envSky0 = nullptr; + nvrhi::ITexture* envSky1 = nullptr; + nvrhi::ITexture* contactDepth = nullptr; // unused (history-only contact) + nvrhi::ITexture* contactHistory = nullptr; // g_ContactHistory @ t28 + nvrhi::ITexture* shadowHZB[3] = {}; + framegraph::VirtualResourceHandle shadowHZBHandles[3]; + nvrhi::ITexture* shadowMask = nullptr; + framegraph::VirtualResourceHandle shadowMaskHandle; + bool IsValid() const { return objectCount > 0 && compactDrawArgsBuffer && megaVertexBuffer && megaIndexBuffer; } @@ -37,11 +51,25 @@ struct TransparentPassConfig { struct TransparentPassState { nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::GraphicsPipelineHandle multiplyPipeline; nvrhi::BindingLayoutHandle layout; + nvrhi::GraphicsPipelineHandle waterPipeline; + nvrhi::BindingLayoutHandle waterLayout; + nvrhi::GraphicsPipelineHandle waterDistortPipeline; + nvrhi::BindingLayoutHandle waterDistortLayout; nvrhi::InputLayoutHandle inputLayout; + nvrhi::InputLayoutHandle waterDistortInputLayout; nvrhi::SamplerHandle sampler; nvrhi::ShaderHandle vs; nvrhi::ShaderHandle ps; + nvrhi::ShaderHandle waterVs; + nvrhi::ShaderHandle waterPs; + nvrhi::ShaderHandle waterDistortVs; + nvrhi::ShaderHandle waterDistortPs; + nvrhi::ITexture* foamTexture = nullptr; + nvrhi::TextureHandle waterSsrColor; + nvrhi::TextureHandle waterSceneDepth; + nvrhi::TextureHandle waterSceneWorldPos; bool initialized = false; }; @@ -50,6 +78,8 @@ struct TransparentPassData { framegraph::VirtualResourceHandle color; framegraph::VirtualResourceHandle normal; framegraph::VirtualResourceHandle baseColor; + framegraph::VirtualResourceHandle worldPos; + framegraph::VirtualResourceHandle distortion; fg::RenderDevice* device; TransparentPassConfig config; TransparentPassState* passState; diff --git a/src/Layers/xrRender/GPUCullingManager.cpp b/src/Layers/xrRender/GPUCullingManager.cpp index 5c2c46984a7..eeb7d511167 100644 --- a/src/Layers/xrRender/GPUCullingManager.cpp +++ b/src/Layers/xrRender/GPUCullingManager.cpp @@ -1,6 +1,7 @@ // xrRender/GPUCullingManager.cpp #include "stdafx.h" #include "GPUCullingManager.h" +#include "ClusteredLightManager.h" #include "xrCore/Profiler/Profiler.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/RenderPassBuilder.h" @@ -53,7 +54,8 @@ struct CullParamsCB { u32 hizHeight; // Hi-Z pyramid height u32 hizMipLevels; // Hi-Z mip levels u32 frameId; // Frame stamp for visibility - u32 padding[3]; + u32 hizEnable; // 0=skip Hi-Z occlusion test + u32 padding[2]; }; // ═══════════════════════════════════════════════════════ @@ -62,7 +64,6 @@ struct CullParamsCB { struct CullDebugParamsCB { Fmatrix viewProj; - Fmatrix prevViewProj; Fvector cameraPos; float maxDistanceSq; Fvector4 frustumPlanes[6]; @@ -99,7 +100,9 @@ GPUCullingManager::GPUCullingManager() , m_maxObjects(MAX_CULLING_OBJECTS) , m_initialized(false) , m_computeEnabled(false) + , m_particleCount(0) , m_maxParticles(MAX_CULLING_PARTICLES) + , m_particleCullEnabled(false) { m_staticObjectData.reserve(MAX_CULLING_OBJECTS); m_staticDrawArgsData.reserve(MAX_CULLING_OBJECTS); @@ -154,6 +157,29 @@ void GPUCullingManager::Initialize(fg::RenderDevice* device) CreateDebugResources(device); CreateParticleResources(device); + { + nvrhi::TextureDesc td; + td.width = 1; + td.height = 1; + td.format = nvrhi::Format::R32_FLOAT; + td.mipLevels = 1; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "GPUCull_DummyHiZ"; + m_dummyHizTexture = nvDevice->createTexture(td); + // Far depth (1.0) so HiZTestSphere always passes — all-visible path for + // DetailCull / frame-0 / forceDisableHiz when no real pyramid is bound. + if (m_dummyHizTexture) + { + nvrhi::CommandListHandle cmd = nvDevice->createCommandList(); + cmd->open(); + const float farD = 1.0f; + cmd->writeTexture(m_dummyHizTexture, 0, 0, &farD, sizeof(farD)); + cmd->close(); + nvDevice->executeCommandList(cmd); + } + } + m_initialized = true; Msg("* [GPUCulling] Initialized (max objects: %d, max particles: %d, compact: %s)", m_maxObjects, m_maxParticles, m_compactEnabled ? "yes" : "no"); @@ -455,6 +481,86 @@ void GPUCullingManager::CreateBuffers(fg::RenderDevice* device) Msg("* [GPUCulling] Transparent buffers created (max: %u objects)", maxTransparent); } + // ─────────────────────────────────────────────────────── + // TESSELLATION CULLING SET (opaque bump# / PN+HM) + // ─────────────────────────────────────────────────────── + { + const u32 maxTess = m_maxObjects; + m_tessSet.maxObjects = maxTess; + + nvrhi::BufferDesc desc; + desc.debugName = "GPUCull_Tess_Objects"; + desc.byteSize = maxTess * sizeof(GPUObjectData); + desc.structStride = sizeof(GPUObjectData); + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + m_tessSet.objectBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_VisibleIndices"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.visibleIndexBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_VisibleCount"; + desc.byteSize = sizeof(u32); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.visibleCountBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_DrawArgs"; + desc.byteSize = maxTess * sizeof(IndirectDrawArgs); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.drawArgsBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_Visibility"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.visibilityBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_MaterialIDs"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + m_tessSet.materialIDBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_InstanceData"; + desc.byteSize = maxTess * sizeof(GPUInstanceData); + desc.structStride = sizeof(GPUInstanceData); + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + m_tessSet.instanceBuffer = nvDevice->createBuffer(desc); + + // Identity batch map for direct drawIndexed (DRAWINDEX → batch index) + desc = {}; + desc.debugName = "GPUCull_Tess_BatchIndices"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + m_tessBatchIndicesBuffer = nvDevice->createBuffer(desc); + + Msg("* [GPUCulling] Tessellation buffers created (max: %u objects)", maxTess); + } + // ─────────────────────────────────────────────────────── // SKINNED MESH CULLING BUFFERS // ─────────────────────────────────────────────────────── @@ -1214,6 +1320,89 @@ void GPUCullingManager::CreateCompactionResources(fg::RenderDevice* device) m_transparentSet.compactGroupOffsetsBuffer = nvDevice->createBuffer(desc); } + // Tessellation compaction buffers + { + const u32 maxTess = m_tessSet.maxObjects; + u32 maxTessGroups = (maxTess + COMPACT_THREAD_GROUP_SIZE - 1) / COMPACT_THREAD_GROUP_SIZE; + R_ASSERT2(maxTessGroups > 0 && maxTessGroups <= COMPACT_THREAD_GROUP_SIZE, + "Tess compaction group count invalid"); + + nvrhi::BufferDesc desc; + desc.debugName = "GPUCull_Tess_CompactDrawArgs"; + desc.byteSize = maxTess * sizeof(IndirectDrawArgs); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactDrawArgsBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactBatchIndices"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactBatchIndicesBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactMaterialIDs"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactMaterialIDBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactCount"; + desc.byteSize = sizeof(u32); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactCountBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactDispatchArgs"; + desc.byteSize = 3 * sizeof(u32); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactDispatchArgsBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactLocalPrefix"; + desc.byteSize = maxTess * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactLocalPrefixBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactGroupCounts"; + desc.byteSize = maxTessGroups * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactGroupCountsBuffer = nvDevice->createBuffer(desc); + + desc = {}; + desc.debugName = "GPUCull_Tess_CompactGroupOffsets"; + desc.byteSize = maxTessGroups * sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + m_tessSet.compactGroupOffsetsBuffer = nvDevice->createBuffer(desc); + } + { fg::RenderDevice::BufferDesc desc; desc.debugName = "GPUCull_CompactParams"; @@ -1449,6 +1638,7 @@ void GPUCullingManager::DispatchVariantPartition( cmdList->setBufferState(set.compactBatchIndicesBuffer, nvrhi::ResourceStates::ShaderResource); cmdList->setBufferState(set.compactMaterialIDBuffer, nvrhi::ResourceStates::ShaderResource); cmdList->setBufferState(set.compactCountBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.compactDispatchArgsBuffer, nvrhi::ResourceStates::IndirectArgument); cmdList->setBufferState(materialBuffer, nvrhi::ResourceStates::ShaderResource); cmdList->setBufferState(partition.variantCountBuffer, nvrhi::ResourceStates::UnorderedAccess); cmdList->setBufferState(partition.reorderedDrawArgsBuffer, nvrhi::ResourceStates::UnorderedAccess); @@ -1486,8 +1676,9 @@ void GPUCullingManager::DispatchVariantPartition( nvrhi::ComputeState state; state.pipeline = m_variantPartitionPipeline; state.bindings = { bindingSet }; + state.indirectParams = set.compactDispatchArgsBuffer; cmdList->setComputeState(state); - cmdList->dispatch(1, 1, 1); + cmdList->dispatchIndirect(0); cmdList->setBufferState(partition.variantCountBuffer, nvrhi::ResourceStates::IndirectArgument); cmdList->setBufferState(partition.reorderedDrawArgsBuffer, nvrhi::ResourceStates::IndirectArgument); @@ -1497,10 +1688,6 @@ void GPUCullingManager::DispatchVariantPartition( void GPUCullingManager::Shutdown() { - m_skinnedPools.Reset(); - for (u32 f = SkinnedGeometryPools::FIRST_FORMAT; f < SkinnedGeometryPools::FORMAT_COUNT; ++f) - m_skinnedBuckets[f] = SkinnedBucket{}; - m_staticSet.objectBuffer = nullptr; m_staticSet.visibleIndexBuffer = nullptr; m_staticSet.visibleCountBuffer = nullptr; @@ -1516,6 +1703,9 @@ void GPUCullingManager::Shutdown() m_staticSet.compactGroupCountsBuffer = nullptr; m_staticSet.compactGroupOffsetsBuffer = nullptr; m_staticSet.instanceBuffer = nullptr; + m_staticSet.shadowIndicesBuffer = nullptr; + m_staticSet.shadowCountBuffer = nullptr; + m_staticSet.shadowBuffersCapacity = 0; m_staticSet.objectCount = 0; m_staticSet.maxObjects = 0; m_staticSet.drawArgsUploaded = false; @@ -1536,16 +1726,31 @@ void GPUCullingManager::Shutdown() m_dynamicSet.compactGroupCountsBuffer = nullptr; m_dynamicSet.compactGroupOffsetsBuffer = nullptr; m_dynamicSet.instanceBuffer = nullptr; + m_dynamicSet.shadowIndicesBuffer = nullptr; + m_dynamicSet.shadowCountBuffer = nullptr; + m_dynamicSet.shadowBuffersCapacity = 0; m_dynamicSet.objectCount = 0; m_dynamicSet.maxObjects = 0; m_dynamicSet.drawArgsUploaded = false; m_dynamicSet.objectsUploaded = false; m_transparentSet = {}; + m_tessSet = {}; + m_tessBatchIndicesBuffer = nullptr; + m_tessObjectData.clear(); + m_tessDrawArgsData.clear(); + m_tessMaterialIDData.clear(); + m_tessInstanceData.clear(); m_cullParamsCB = fg::BufferHandle(); m_cullPipeline = nullptr; m_cullLayout = nullptr; + m_lightShadowCullPipeline = nullptr; + m_lightShadowCullLayout = nullptr; + m_lightShadowCullParamsCB = nullptr; + m_lightShadowCullReady = false; + m_localShadowCullSlots.clear(); + m_localShadowCullSlotCount = 0; m_pointSampler = nullptr; m_compactParamsCB = fg::BufferHandle(); @@ -1574,6 +1779,11 @@ void GPUCullingManager::Shutdown() m_debugInputLayout = nullptr; m_particleBuffer = nullptr; + m_particleDrawArgsBuffer = nullptr; + m_particleVisibleCountBuffer = nullptr; + m_particleCullParamsCB = fg::BufferHandle(); + m_particleCullPipeline = nullptr; + m_particleCullLayout = nullptr; // Mega-buffer resources m_megaVertexBuffer = nullptr; @@ -1623,12 +1833,16 @@ void GPUCullingManager::Shutdown() // Visibility buffer m_staticTerrainDrawArgsUploaded = false; + m_terrainDataCached = false; + m_lastMaterialCountForStatic = 0; m_staticDataCached = false; m_initialized = false; m_computeEnabled = false; m_compactEnabled = false; + m_particleCullEnabled = false; m_objectCount = 0; + m_particleCount = 0; // Stats readback for (u32 i = 0; i < STATS_READBACK_SLOTS; ++i) @@ -1763,22 +1977,108 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr const auto& batches = geometry->GetBatches(); u32 totalBatches = static_cast(batches.size()); + const bool haveStaticSource = m_staticBatchSource && !m_staticBatchSource->empty(); - if (totalBatches == 0) { - m_staticSet.objectCount = 0; + if (totalBatches == 0 && !(haveStaticSource && !m_staticDataCached)) { m_dynamicSet.objectCount = 0; - m_objectCount = 0; + m_tessSet.objectCount = 0; + m_transparentSet.objectCount = 0; + m_transientDataCached = false; + m_transientUploadFingerprint = 0; + if (!m_staticDataCached) + { + m_staticSet.objectCount = 0; + m_objectCount = 0; + } + else + { + m_objectCount = m_staticSet.objectCount; + } return; } +#if defined(XR_PLATFORM_APPLE) + const bool tessFlagOn = false; +#else + const bool tessFlagOn = ps_r2_ls_flags_ext.test(R2FLAGEXT_ENABLE_TESSELLATION); +#endif + { + const u32 matCount = bindless::MaterialBuffer::Instance().GetMaterialCount(); + if (matCount != m_lastMaterialCountForStatic) + { + m_staticDataCached = false; + m_staticSet.objectsUploaded = false; + m_terrainDataCached = false; + m_staticTerrainDrawArgsUploaded = false; + m_transientDataCached = false; + m_lastMaterialCountForStatic = matCount; + } + } + if (tessFlagOn != m_lastTessFlagOn) + { + m_staticDataCached = false; + m_staticSet.objectsUploaded = false; + m_transientDataCached = false; + m_lastTessFlagOn = tessFlagOn; + } + + auto hashMix = [](u64 h, u64 v) -> u64 { + return h ^ (v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2)); + }; + auto hashFloat = [&](u64 h, float f) -> u64 { + u32 bits = 0; + memcpy(&bits, &f, sizeof(bits)); + return hashMix(h, bits); + }; + + u64 transientFp = 0; + transientFp = hashMix(transientFp, tessFlagOn ? 1u : 0u); + transientFp = hashMix(transientFp, bindless::MaterialBuffer::Instance().GetMaterialCount()); + { + constexpr u32 kMaxTessIndexCount = 8192; + u32 transientCount = 0; + for (u32 i = 0; i < totalBatches; ++i) + { + const auto& batch = batches[i]; + if (batch.isSkinned || batch.isTerrain) + continue; + + bool routeTess = false; + if (tessFlagOn && !batch.IsStrictB2F()) + { + const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(batch.bindlessMaterialID); + routeTess = mat && mat->tessMethod != 0 && batch.indexCount > 0 && + batch.indexCount <= kMaxTessIndexCount; + } + const bool isTransient = batch.IsStrictB2F() || routeTess || !batch.isStatic; + if (!isTransient) + continue; + + ++transientCount; + transientFp = hashMix(transientFp, reinterpret_cast(batch.visual)); + transientFp = hashMix(transientFp, batch.bindlessMaterialID); + transientFp = hashMix(transientFp, batch.indexCount); + transientFp = hashMix(transientFp, batch.isStatic ? 1u : 0u); + transientFp = hashMix(transientFp, batch.IsStrictB2F() ? 1u : 0u); + transientFp = hashMix(transientFp, routeTess ? 1u : 0u); + transientFp = hashFloat(transientFp, batch.worldMatrix._11); + transientFp = hashFloat(transientFp, batch.worldMatrix._22); + transientFp = hashFloat(transientFp, batch.worldMatrix._33); + transientFp = hashFloat(transientFp, batch.worldMatrix._41); + transientFp = hashFloat(transientFp, batch.worldMatrix._42); + transientFp = hashFloat(transientFp, batch.worldMatrix._43); + transientFp = hashFloat(transientFp, batch.worldBoundsRadius); + } + transientFp = hashMix(transientFp, transientCount); + } + const bool skipTransientRebuild = + m_transientDataCached && transientFp == m_transientUploadFingerprint; + R_ASSERT2(m_staticSet.objectBuffer && m_staticSet.drawArgsBuffer && m_staticSet.visibilityBuffer, "Static GPU culling buffers not initialized"); R_ASSERT2(m_dynamicSet.objectBuffer && m_dynamicSet.drawArgsBuffer && m_dynamicSet.visibilityBuffer, "Dynamic GPU culling buffers not initialized"); - // Build object data, draw args, and material ID arrays - // NOTE: Skip skinned batches - they use separate per-draw rendering with bone matrices - // NOTE: Terrain batches are tracked separately for terrain shader rendering if (!m_staticDataCached) { m_staticObjectData.clear(); m_staticObjectData.reserve(totalBatches); @@ -1792,36 +2092,47 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr m_staticBatchVertexCounts.reserve(totalBatches); } - m_dynamicObjectData.clear(); - m_dynamicObjectData.reserve(totalBatches); - m_dynamicDrawArgsData.clear(); - m_dynamicDrawArgsData.reserve(totalBatches); - m_dynamicMaterialIDData.clear(); - m_dynamicMaterialIDData.reserve(totalBatches); - m_dynamicInstanceData.clear(); - m_dynamicInstanceData.reserve(totalBatches); + if (!skipTransientRebuild) { + m_dynamicObjectData.clear(); + m_dynamicObjectData.reserve(totalBatches); + m_dynamicDrawArgsData.clear(); + m_dynamicDrawArgsData.reserve(totalBatches); + m_dynamicMaterialIDData.clear(); + m_dynamicMaterialIDData.reserve(totalBatches); + m_dynamicInstanceData.clear(); + m_dynamicInstanceData.reserve(totalBatches); - // Terrain-specific arrays - m_terrainObjectData.clear(); - m_terrainObjectData.reserve(totalBatches / 4); - m_terrainDrawArgsData.clear(); - m_terrainDrawArgsData.reserve(totalBatches / 4); - m_terrainMaterialIDData.clear(); - m_terrainMaterialIDData.reserve(totalBatches / 4); - m_terrainInstanceData.clear(); - m_terrainInstanceData.reserve(totalBatches / 4); + m_transparentObjectData.clear(); + m_transparentDrawArgsData.clear(); + m_transparentMaterialIDData.clear(); + m_transparentInstanceData.clear(); + + m_tessObjectData.clear(); + m_tessDrawArgsData.clear(); + m_tessMaterialIDData.clear(); + m_tessInstanceData.clear(); + } - // Transparent-specific arrays - m_transparentObjectData.clear(); - m_transparentDrawArgsData.clear(); - m_transparentMaterialIDData.clear(); - m_transparentInstanceData.clear(); + if (!m_terrainDataCached) { + m_terrainObjectData.clear(); + m_terrainObjectData.reserve(totalBatches / 4); + m_terrainDrawArgsData.clear(); + m_terrainDrawArgsData.reserve(totalBatches / 4); + m_terrainMaterialIDData.clear(); + m_terrainMaterialIDData.reserve(totalBatches / 4); + m_terrainInstanceData.clear(); + m_terrainInstanceData.reserve(totalBatches / 4); + } auto appendBatch = [&](const GeometryBatch& batch, xr_vector& objectData, xr_vector& drawArgsData, xr_vector& materialIDData, - xr_vector& instanceData) { + xr_vector& instanceData) -> bool { + const u32 matCount = bindless::MaterialBuffer::Instance().GetMaterialCount(); + if (batch.bindlessMaterialID == UINT32_MAX || batch.bindlessMaterialID >= matCount) + return false; + // ───────────────────────────────────────────────────── // BUILD OBJECT DATA (for culling) // ───────────────────────────────────────────────────── @@ -1856,7 +2167,12 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr args.startIndexLocation = batch.startIndex; args.baseVertexLocation = batch.baseVertex; } - args.startInstanceLocation = 0; + // startInstanceLocation carries the per-draw DRAWINDEX for the view-independent + // "cast all" shadow path, which draws these raw args directly (no compaction). + // The forward path ignores this: batch_compact.cs overwrites it with the compacted + // output index. With it left at 0, every cast-all draw read instance 0, collapsing + // all static/transparent shadow casters (trees included) onto the first object. + args.startInstanceLocation = static_cast(drawArgsData.size()); drawArgsData.push_back(args); // ───────────────────────────────────────────────────── @@ -1874,76 +2190,204 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr inst.pad0 = 0.0f; inst.pad1 = 0.0f; instanceData.push_back(inst); + return true; }; + // [ShadowDbg] one-shot: classify tree/skinned batches to find why they never cast. + { + static bool s_treeDbgDone = false; + if (!s_treeDbgDone && totalBatches > 0) { + u32 treeTotal = 0, treeStatic = 0, treeTrans = 0, treeTess = 0, treeSkinnedOrOther = 0; + u32 treeAllocValid = 0, treeAllocInvalid = 0, skinnedTotal = 0; + const bool tessOn = tessFlagOn; + for (u32 i = 0; i < totalBatches; i++) { + const auto& b = batches[i]; + const u32 vt = b.visual ? b.visual->getType() : 0; + if (b.isSkinned) { skinnedTotal++; continue; } + if (vt != MT_TREE_ST && vt != MT_TREE_PM) + continue; + treeTotal++; + if (b.megaBufferAlloc.valid) treeAllocValid++; else treeAllocInvalid++; + bool routedTess = false; + if (tessOn) { + const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(b.bindlessMaterialID); + routedTess = mat && mat->tessMethod != 0 && b.indexCount > 0 && b.indexCount <= 8192; + } + if (b.IsStrictB2F()) treeTrans++; + else if (routedTess) treeTess++; + else if (b.isStatic) treeStatic++; + else treeSkinnedOrOther++; + + if (treeTotal <= 5) { + const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(b.bindlessMaterialID); + Msg("* [ShadowDbg] tree#%u type=%u matID=%u flags=0x%X alphaRef=%.3f diffIdx=%u alphaTest=%d strictB2F=%d shader='%s' tex='%s'", + treeTotal, vt, b.bindlessMaterialID, + mat ? mat->flags : 0u, mat ? mat->alphaRef : -1.f, + mat ? mat->diffuseIndex : 0xFFFFFFFFu, + b.IsAlphaTested() ? 1 : 0, b.IsStrictB2F() ? 1 : 0, + (b.visual && b.visual->shaderName.size()) ? b.visual->shaderName.c_str() : "", + (b.visual && b.visual->textureName.size()) ? b.visual->textureName.c_str() : ""); + } + } + Msg("* [ShadowDbg] batches=%u trees=%u (static=%u trans=%u tess=%u dyn=%u) allocValid=%u allocInvalid=%u | skinned=%u", + totalBatches, treeTotal, treeStatic, treeTrans, treeTess, treeSkinnedOrOther, + treeAllocValid, treeAllocInvalid, skinnedTotal); + + u32 tessRouted = 0, tessSkippedBig = 0, tessMats = 0, skinnedTessMats = 0; + for (u32 i = 0; i < totalBatches; i++) { + const auto& b = batches[i]; + const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(b.bindlessMaterialID); + if (!mat || mat->tessMethod == 0) + continue; + if (b.isSkinned) { + skinnedTessMats++; + continue; + } + const u32 vt = b.visual ? b.visual->getType() : 0; + if (vt == MT_TREE_ST || vt == MT_TREE_PM) + continue; + tessMats++; + if (!tessOn) + continue; + if (b.indexCount > 8192) + tessSkippedBig++; + else if (b.indexCount > 0) + tessRouted++; + } + Msg("* [TessDbg] tessOn=%d staticRouted=%u skippedBig=%u staticMats=%u skinnedMatsWithTess=%u", + tessOn ? 1 : 0, tessRouted, tessSkippedBig, tessMats, skinnedTessMats); + if (treeTotal > 0 || skinnedTotal > 0) + s_treeDbgDone = true; + } + } + { ZoneScopedN("Upload::Rebuild"); + u32 badMatSkipped = 0; + const u32 terrainMatCount = bindless::TerrainMaterialBuffer::Instance().GetMaterialCount(); + constexpr u32 kMaxTessIndexCount = 8192; + + auto appendTerrain = [&](const GeometryBatch& batch) { + if (batch.terrainMaterialID == UINT32_MAX) + { + badMatSkipped++; + return; + } + GPUObjectData obj; + obj.position = batch.worldBoundsCenter; + obj.radius = batch.worldBoundsRadius; + obj.batchIndex = static_cast(m_terrainObjectData.size()); + obj.flags = GPU_OBJECT_OPAQUE; + obj.pad0 = 0.0f; + obj.pad1 = 0.0f; + m_terrainObjectData.push_back(obj); + + IndirectDrawArgs args; + args.indexCountPerInstance = batch.indexCount; + args.instanceCount = 0; + if (batch.megaBufferAlloc.valid) { + args.startIndexLocation = batch.megaBufferAlloc.indexOffset; + args.baseVertexLocation = static_cast(batch.megaBufferAlloc.vertexOffset); + } else { + args.startIndexLocation = batch.startIndex; + args.baseVertexLocation = batch.baseVertex; + } + args.startInstanceLocation = static_cast(m_terrainDrawArgsData.size()); + m_terrainDrawArgsData.push_back(args); + + m_terrainMaterialIDData.push_back(batch.terrainMaterialID); + + GPUInstanceData inst; + inst.world = batch.worldMatrix; + inst.materialID = batch.terrainMaterialID; + inst.flags = GPU_OBJECT_OPAQUE; + inst.pad0 = 0.0f; + inst.pad1 = 0.0f; + m_terrainInstanceData.push_back(inst); + }; + + auto isTessBatch = [&](const GeometryBatch& batch) -> bool { + if (!tessFlagOn || batch.IsStrictB2F()) + return false; + const auto* mat = bindless::MaterialBuffer::Instance().GetMaterial(batch.bindlessMaterialID); + return mat && mat->tessMethod != 0 && batch.indexCount > 0 && + batch.indexCount <= kMaxTessIndexCount; + }; + + if (haveStaticSource && (!m_staticDataCached || !m_terrainDataCached)) + { + ZoneScopedN("Upload::StaticFromCache"); + for (const auto& batch : *m_staticBatchSource) + { + if (batch.isSkinned) + continue; + if (batch.isTerrain) + { + if (!m_terrainDataCached) + appendTerrain(batch); + continue; + } + if (m_staticDataCached || batch.IsStrictB2F() || isTessBatch(batch)) + continue; + if (appendBatch(batch, m_staticObjectData, m_staticDrawArgsData, m_staticMaterialIDData, m_staticInstanceData)) + m_staticBatchVertexCounts.push_back(batch.megaBufferAlloc.valid ? batch.megaBufferAlloc.vertexCount : 0); + else + badMatSkipped++; + } + } + for (u32 i = 0; i < totalBatches; i++) { const auto& batch = batches[i]; - // Skip skinned batches - they use a separate per-draw rendering path - // with bone matrices and cannot use the GPU-driven multi-draw system if (batch.isSkinned) continue; - // Route terrain batches to separate arrays for terrain shader rendering if (batch.isTerrain) { - // ───────────────────────────────────────────────────── - // TERRAIN BATCH - Goes to terrain arrays - // ───────────────────────────────────────────────────── - GPUObjectData obj; - obj.position = batch.worldBoundsCenter; - obj.radius = batch.worldBoundsRadius; - obj.batchIndex = static_cast(m_terrainObjectData.size()); - obj.flags = GPU_OBJECT_OPAQUE; // Terrain is always opaque - obj.pad0 = 0.0f; - obj.pad1 = 0.0f; - m_terrainObjectData.push_back(obj); - - // Terrain draw args - IndirectDrawArgs args; - args.indexCountPerInstance = batch.indexCount; - args.instanceCount = 0; // Set by culling shader if visible - if (batch.megaBufferAlloc.valid) { - args.startIndexLocation = batch.megaBufferAlloc.indexOffset; - args.baseVertexLocation = static_cast(batch.megaBufferAlloc.vertexOffset); - } else { - args.startIndexLocation = batch.startIndex; - args.baseVertexLocation = batch.baseVertex; - } - // CRITICAL: StartInstanceLocation provides draw index to shader - // This indexes into terrain material/instance buffers - args.startInstanceLocation = static_cast(m_terrainDrawArgsData.size()); - m_terrainDrawArgsData.push_back(args); - - // Terrain material ID (index into TerrainMaterialBuffer, not regular MaterialBuffer) - m_terrainMaterialIDData.push_back(batch.terrainMaterialID); - - // Terrain instance data (world transform) - GPUInstanceData inst; - inst.world = batch.worldMatrix; - inst.materialID = batch.terrainMaterialID; // Terrain material ID - inst.flags = GPU_OBJECT_OPAQUE; // Terrain is always opaque - inst.pad0 = 0.0f; - inst.pad1 = 0.0f; - m_terrainInstanceData.push_back(inst); + if (m_terrainDataCached || haveStaticSource) + continue; + appendTerrain(batch); continue; } if (batch.IsStrictB2F()) { - appendBatch(batch, m_transparentObjectData, m_transparentDrawArgsData, m_transparentMaterialIDData, m_transparentInstanceData); + if (!skipTransientRebuild && + !appendBatch(batch, m_transparentObjectData, m_transparentDrawArgsData, m_transparentMaterialIDData, m_transparentInstanceData)) + badMatSkipped++; continue; } + if (isTessBatch(batch)) + { + if (!skipTransientRebuild && + m_tessObjectData.size() < m_tessSet.maxObjects && + appendBatch(batch, m_tessObjectData, m_tessDrawArgsData, m_tessMaterialIDData, m_tessInstanceData)) + { + continue; + } + if (skipTransientRebuild) + continue; + } + if (batch.isStatic) { - if (!m_staticDataCached) { - appendBatch(batch, m_staticObjectData, m_staticDrawArgsData, m_staticMaterialIDData, m_staticInstanceData); - m_staticBatchVertexCounts.push_back(batch.megaBufferAlloc.valid ? batch.megaBufferAlloc.vertexCount : 0); + if (!m_staticDataCached && !haveStaticSource) { + if (appendBatch(batch, m_staticObjectData, m_staticDrawArgsData, m_staticMaterialIDData, m_staticInstanceData)) + m_staticBatchVertexCounts.push_back(batch.megaBufferAlloc.valid ? batch.megaBufferAlloc.vertexCount : 0); + else + badMatSkipped++; } - } else { - appendBatch(batch, m_dynamicObjectData, m_dynamicDrawArgsData, m_dynamicMaterialIDData, m_dynamicInstanceData); + } else if (!skipTransientRebuild) { + if (!appendBatch(batch, m_dynamicObjectData, m_dynamicDrawArgsData, m_dynamicMaterialIDData, m_dynamicInstanceData)) + badMatSkipped++; } } + static bool s_badMatLogged = false; + if (!s_badMatLogged && totalBatches > 0) + { + s_badMatLogged = true; + Msg("* [GPUCull] badMat skipped=%u / batches=%u (matCount=%u terrainMatCount=%u)", + badMatSkipped, totalBatches, + bindless::MaterialBuffer::Instance().GetMaterialCount(), terrainMatCount); + } } // Set object counts with total cap @@ -2031,7 +2475,7 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr Msg("* [GPUCulling] Static object data uploaded: %u objects", m_staticSet.objectCount); } - if (m_dynamicSet.objectCount > 0) { + if (!skipTransientRebuild && m_dynamicSet.objectCount > 0) { ZoneScopedN("Upload::DynamicWrite"); R_ASSERT2(m_dynamicObjectData.size() >= m_dynamicSet.objectCount, "Dynamic object data smaller than count"); R_ASSERT2(m_dynamicDrawArgsData.size() >= m_dynamicSet.objectCount, "Dynamic draw args data smaller than count"); @@ -2058,6 +2502,15 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr m_dynamicSet.objectCount * sizeof(GPUInstanceData)); } + // View-independent shadow-caster buffers (identity drawID->batch map + count). + // Lets the shadow pass draw every batch regardless of camera-frustum culling, so + // casters that leave the camera view still cast into the CSM cascades. + { + nvrhi::IDevice* shadowNvDevice = m_device->GetNVRHIDevice(); + EnsureShadowCasterBuffers(cmdList, shadowNvDevice, m_staticSet, "Static"); + EnsureShadowCasterBuffers(cmdList, shadowNvDevice, m_dynamicSet, "Dynamic"); + } + // ───────────────────────────────────────────────────── // TERRAIN BUFFER UPLOADS // ───────────────────────────────────────────────────── @@ -2070,15 +2523,11 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr R_ASSERT2(m_terrainMaterialIDData.size() >= m_terrainObjectCount, "Terrain material ID data smaller than object count"); R_ASSERT2(m_terrainInstanceData.size() >= m_terrainObjectCount, "Terrain instance data smaller than object count"); - // Terrain object data (bounding spheres) - still uploaded every frame - // TODO: Terrain is static, could cache this too with dirty flag - cmdList->writeBuffer(m_terrainObjectBuffer, - m_terrainObjectData.data(), - m_terrainObjectCount * sizeof(GPUObjectData)); - cmdList->setBufferState(m_terrainObjectBuffer, nvrhi::ResourceStates::ShaderResource); - - // Terrain draw args, material IDs, instance data - uploaded ONCE (visibility buffer handles culling) if (!m_staticTerrainDrawArgsUploaded) { + cmdList->writeBuffer(m_terrainObjectBuffer, + m_terrainObjectData.data(), + m_terrainObjectCount * sizeof(GPUObjectData)); + cmdList->setBufferState(m_terrainObjectBuffer, nvrhi::ResourceStates::ShaderResource); // Set instanceCount=1 for all terrain (apply visibility pass will set actual visibility) for (auto& args : m_terrainDrawArgsData) { args.instanceCount = 1; @@ -2130,6 +2579,7 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr } m_staticTerrainDrawArgsUploaded = true; + m_terrainDataCached = true; Msg("* [GPUCulling] Static terrain draw args uploaded: %u objects (visibility buffer mode)", m_terrainObjectCount); } } @@ -2141,7 +2591,8 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr // ───────────────────────────────────────────────────── m_transparentSet.objectCount = std::min(static_cast(m_transparentObjectData.size()), m_transparentSet.maxObjects); - if (m_transparentSet.objectCount > 0 && m_transparentSet.objectBuffer && m_transparentSet.drawArgsBuffer) { + if (!skipTransientRebuild && m_transparentSet.objectCount > 0 && + m_transparentSet.objectBuffer && m_transparentSet.drawArgsBuffer) { ZoneScopedN("Upload::TransparentWrite"); for (auto& args : m_transparentDrawArgsData) args.instanceCount = 1; @@ -2165,21 +2616,81 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr m_transparentInstanceData.data(), m_transparentSet.objectCount * sizeof(GPUInstanceData)); cmdList->setBufferState(m_transparentSet.instanceBuffer, nvrhi::ResourceStates::ShaderResource); + + EnsureShadowCasterBuffers(cmdList, m_device->GetNVRHIDevice(), m_transparentSet, "Transparent"); + } + + m_tessSet.objectCount = std::min(static_cast(m_tessObjectData.size()), m_tessSet.maxObjects); + + if (!skipTransientRebuild && m_tessSet.objectCount > 0 && + m_tessSet.objectBuffer && m_tessSet.drawArgsBuffer) { + ZoneScopedN("Upload::TessWrite"); + for (u32 i = 0; i < m_tessSet.objectCount; ++i) + { + m_tessDrawArgsData[i].instanceCount = 1; + m_tessDrawArgsData[i].startInstanceLocation = i; + } + + cmdList->writeBuffer(m_tessSet.objectBuffer, + m_tessObjectData.data(), + m_tessSet.objectCount * sizeof(GPUObjectData)); + cmdList->setBufferState(m_tessSet.objectBuffer, nvrhi::ResourceStates::ShaderResource); + + cmdList->writeBuffer(m_tessSet.drawArgsBuffer, + m_tessDrawArgsData.data(), + m_tessSet.objectCount * sizeof(IndirectDrawArgs)); + cmdList->setBufferState(m_tessSet.drawArgsBuffer, nvrhi::ResourceStates::ShaderResource); + + cmdList->writeBuffer(m_tessSet.materialIDBuffer, + m_tessMaterialIDData.data(), + m_tessSet.objectCount * sizeof(u32)); + cmdList->setBufferState(m_tessSet.materialIDBuffer, nvrhi::ResourceStates::ShaderResource); + + cmdList->writeBuffer(m_tessSet.instanceBuffer, + m_tessInstanceData.data(), + m_tessSet.objectCount * sizeof(GPUInstanceData)); + cmdList->setBufferState(m_tessSet.instanceBuffer, nvrhi::ResourceStates::ShaderResource); + + if (m_tessBatchIndicesBuffer) + { + xr_vector identity(m_tessSet.objectCount); + for (u32 i = 0; i < m_tessSet.objectCount; ++i) + identity[i] = i; + cmdList->writeBuffer(m_tessBatchIndicesBuffer, identity.data(), + m_tessSet.objectCount * sizeof(u32)); + cmdList->setBufferState(m_tessBatchIndicesBuffer, nvrhi::ResourceStates::ShaderResource); + } + } + + if (!skipTransientRebuild) + { + m_transientUploadFingerprint = transientFp; + m_transientDataCached = true; } } void GPUCullingManager::InvalidateStaticCullingData() { m_staticDataCached = false; + m_terrainDataCached = false; + m_staticTerrainDrawArgsUploaded = false; + m_lastMaterialCountForStatic = 0; m_staticSet.objectsUploaded = false; m_staticSet.drawArgsUploaded = false; m_staticSet.objectCount = 0; + m_transientDataCached = false; + m_transientUploadFingerprint = 0; m_staticObjectData.clear(); m_staticDrawArgsData.clear(); m_staticMaterialIDData.clear(); m_staticInstanceData.clear(); m_staticBatchVertexCounts.clear(); + m_terrainObjectData.clear(); + m_terrainDrawArgsData.clear(); + m_terrainMaterialIDData.clear(); + m_terrainInstanceData.clear(); + m_terrainObjectCount = 0; Msg("* [GPUCulling] Static culling data invalidated"); } @@ -2210,16 +2721,22 @@ void GPUCullingManager::InvalidateShadersAndPipelines() m_debugGraphicsLayout = nullptr; m_debugInputLayout = nullptr; + m_particleCullPipeline = nullptr; + m_particleCullLayout = nullptr; + m_pointSampler = nullptr; m_initialized = false; m_computeEnabled = false; m_compactEnabled = false; m_variantPartitionEnabled = false; + m_particleCullEnabled = false; m_skinnedCullEnabled = false; m_staticDataCached = false; m_staticTerrainDrawArgsUploaded = false; + m_terrainDataCached = false; + m_lastMaterialCountForStatic = 0; m_staticSet.objectsUploaded = false; m_staticSet.drawArgsUploaded = false; m_dynamicSet.objectsUploaded = false; @@ -2266,7 +2783,8 @@ void GPUCullingManager::UploadSkinnedObjects(fg::RenderContext* ctx, const Geome GPUObjectData obj; obj.position = batch.worldBoundsCenter; - obj.radius = batch.worldBoundsRadius; + // Animated poses exceed rest-pose spheres → inflate to avoid pop-in/out + obj.radius = batch.worldBoundsRadius * 1.75f; obj.flags = 0; // Skinned meshes use their own alpha handling obj.pad0 = 0.0f; obj.pad1 = 0.0f; @@ -2494,22 +3012,737 @@ void GPUCullingManager::ExtractFrustumPlanes(Fmatrix& M, Fvector4* outPlanes) } } -// ═══════════════════════════════════════════════════════ -// SETUP CULLING PASS -// ═══════════════════════════════════════════════════════ - -GPUCullOutput GPUCullingManager::SetupCullingPass( - framegraph::FrameGraph& fg, - framegraph::VirtualResourceHandle hizPyramid, - u32 hizWidth, - u32 hizHeight, - u32 hizMipLevels, - const GeometryCollector* geometry, - const Fmatrix& prevViewProj) +void GPUCullingManager::EnsureShadowCasterBuffers(nvrhi::ICommandList* cmdList, + nvrhi::IDevice* nvDevice, CullSetBuffers& set, const char* name) { - using namespace framegraph; + if (!cmdList || !nvDevice || set.objectCount == 0) + return; - GPUCullOutput output; + // (Re)create the identity map when it is missing or too small. Identity content is a + // prefix, so an oversized buffer already contains a valid [0..objectCount-1] range. + if (!set.shadowIndicesBuffer || set.shadowBuffersCapacity < set.objectCount) + { + const u32 capacity = set.objectCount; + set.shadowCountBuffer = nullptr; + + char nameBuf[128]; + nvrhi::BufferDesc idxDesc; + snprintf(nameBuf, sizeof(nameBuf), "GPUCull_%s_ShadowIndices", name); + idxDesc.debugName = nameBuf; + idxDesc.byteSize = u64(capacity) * sizeof(u32); + idxDesc.structStride = sizeof(u32); + idxDesc.canHaveUAVs = true; + idxDesc.initialState = nvrhi::ResourceStates::UnorderedAccess; + idxDesc.keepInitialState = true; + set.shadowIndicesBuffer = nvDevice->createBuffer(idxDesc); + + if (set.shadowIndicesBuffer) + { + xr_vector ids(capacity); + for (u32 i = 0; i < capacity; ++i) + ids[i] = i; + cmdList->writeBuffer(set.shadowIndicesBuffer, ids.data(), u64(capacity) * sizeof(u32)); + } + + if (!set.shadowCountBuffer) + { + nvrhi::BufferDesc cntDesc; + snprintf(nameBuf, sizeof(nameBuf), "GPUCull_%s_ShadowCount", name); + cntDesc.debugName = nameBuf; + cntDesc.byteSize = sizeof(u32); + cntDesc.canHaveRawViews = true; + cntDesc.canHaveUAVs = true; + cntDesc.isDrawIndirectArgs = true; + cntDesc.initialState = nvrhi::ResourceStates::UnorderedAccess; + cntDesc.keepInitialState = true; + set.shadowCountBuffer = nvDevice->createBuffer(cntDesc); + } + + { + nvrhi::BufferDesc argsDesc; + snprintf(nameBuf, sizeof(nameBuf), "GPUCull_%s_ShadowCompactDrawArgs", name); + argsDesc.debugName = nameBuf; + argsDesc.byteSize = u64(capacity) * sizeof(IndirectDrawArgs); + argsDesc.canHaveRawViews = true; + argsDesc.canHaveUAVs = true; + argsDesc.isDrawIndirectArgs = true; + argsDesc.structStride = sizeof(IndirectDrawArgs); + argsDesc.initialState = nvrhi::ResourceStates::UnorderedAccess; + argsDesc.keepInitialState = true; + set.shadowCompactDrawArgsBuffer = nvDevice->createBuffer(argsDesc); + } + { + nvrhi::BufferDesc matDesc; + snprintf(nameBuf, sizeof(nameBuf), "GPUCull_%s_ShadowCompactMats", name); + matDesc.debugName = nameBuf; + matDesc.byteSize = u64(capacity) * sizeof(u32); + matDesc.structStride = sizeof(u32); + matDesc.canHaveUAVs = true; + matDesc.initialState = nvrhi::ResourceStates::UnorderedAccess; + matDesc.keepInitialState = true; + set.shadowCompactMaterialIDBuffer = nvDevice->createBuffer(matDesc); + } + + set.shadowBuffersCapacity = capacity; + } + + // Count changes per frame (dynamic set), keep it in sync for cast-all fallback. + if (set.shadowCountBuffer) + cmdList->writeBuffer(set.shadowCountBuffer, &set.objectCount, sizeof(u32)); +} + +void GPUCullingManager::BuildLightFrustumCasters(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const Fmatrix& lightVP, float radiusInflate, float skipNearCameraClearance) +{ + if (!cmdList || !nvDevice) + return; + + ZoneScopedN("BuildLightFrustumCasters"); + + CFrustum frustum; + Fmatrix vp = lightVP; + frustum.CreateFromMatrix(vp, FRUSTUM_P_LRTB | FRUSTUM_P_FAR); + const Fvector camPos = Device.vCameraPosition; + + auto filterSet = [&](CullSetBuffers& set, + const xr_vector& objects, + const xr_vector& drawArgs, + const xr_vector& materialIDs, + const char* name) + { + if (set.objectCount == 0) + return; + EnsureShadowCasterBuffers(cmdList, nvDevice, set, name); + if (!set.shadowIndicesBuffer || !set.shadowCompactDrawArgsBuffer || + !set.shadowCompactMaterialIDBuffer || !set.shadowCountBuffer) + return; + + static thread_local xr_vector ids; + static thread_local xr_vector args; + static thread_local xr_vector mats; + ids.clear(); + args.clear(); + mats.clear(); + ids.reserve(set.objectCount); + args.reserve(set.objectCount); + mats.reserve(set.objectCount); + + const u32 n = std::min(set.objectCount, static_cast(objects.size())); + for (u32 i = 0; i < n; ++i) + { + const GPUObjectData& o = objects[i]; + const float r = o.radius * radiusInflate; + if (!frustum.testSphere_dirty(o.position, r)) + continue; + // Local atlas: drop large casters that reach into the camera bubble. + // Those walls fill sideways OMNIPART faces → hard "shadows in your face". + // CSM/rain keep skipNearCameraClearance=0. + if (skipNearCameraClearance > 0.f && r > 0.45f) + { + const float camDist = camPos.distance_to(o.position); + if (camDist - r < skipNearCameraClearance) + continue; + } + ids.push_back(i); + if (i < drawArgs.size()) + { + IndirectDrawArgs a = drawArgs[i]; + a.instanceCount = 1; + a.startInstanceLocation = static_cast(args.size()); + args.push_back(a); + } + mats.push_back(i < materialIDs.size() ? materialIDs[i] : 0u); + } + + const u32 count = static_cast(ids.size()); + if (count > 0) + { + cmdList->writeBuffer(set.shadowIndicesBuffer, ids.data(), u64(count) * sizeof(u32)); + cmdList->writeBuffer(set.shadowCompactDrawArgsBuffer, args.data(), u64(count) * sizeof(IndirectDrawArgs)); + cmdList->writeBuffer(set.shadowCompactMaterialIDBuffer, mats.data(), u64(count) * sizeof(u32)); + } + cmdList->writeBuffer(set.shadowCountBuffer, &count, sizeof(u32)); + }; + + filterSet(m_staticSet, m_staticObjectData, m_staticDrawArgsData, m_staticMaterialIDData, "Static"); + filterSet(m_dynamicSet, m_dynamicObjectData, m_dynamicDrawArgsData, m_dynamicMaterialIDData, "Dynamic"); + filterSet(m_transparentSet, m_transparentObjectData, m_transparentDrawArgsData, m_transparentMaterialIDData, "Transparent"); +} + +bool GPUCullingManager::EnsureLightShadowCullPipeline(nvrhi::IDevice* nvDevice) +{ + if (m_lightShadowCullReady) + return m_lightShadowCullPipeline != nullptr && m_lightShadowCullParamsCB != nullptr; + if (!nvDevice || !GEnv.Render || !GEnv.Render->GetShaderLoader()) + return false; + + auto csShader = GEnv.Render->GetShaderLoader()->LoadComputeShader("light_shadow_cull"); + auto* csRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("light_shadow_cull", ".cs"); + if (!csRefl || !csShader.handle) + { + static bool s_once = false; + if (!s_once) + { + Msg("! [GPUCulling] light_shadow_cull.cs missing — falling back to CPU light frustum cull"); + s_once = true; + } + m_lightShadowCullReady = true; + return false; + } + + auto& cache = framegraph::GetPassResourceCache(); + m_lightShadowCullLayout = cache.GetOrCreateBindingLayoutFromReflection( + "GPUCull_LightShadowCull", *csRefl, nvDevice); + if (!m_lightShadowCullLayout) + { + m_lightShadowCullReady = true; + return false; + } + + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = csShader.handle; + pipeDesc.bindingLayouts = {m_lightShadowCullLayout}; + m_lightShadowCullPipeline = nvDevice->createComputePipeline(pipeDesc); + if (!m_lightShadowCullPipeline) + { + m_lightShadowCullReady = true; + return false; + } + + if (!m_lightShadowCullParamsCB) + { + nvrhi::BufferDesc cbDesc; + cbDesc.byteSize = sizeof(Fvector4) * 6 + sizeof(u32) * 4; + cbDesc.isConstantBuffer = true; + cbDesc.isVolatile = true; + cbDesc.maxVersions = 8192; + cbDesc.debugName = "GPUCull_LightShadowCullParams"; + m_lightShadowCullParamsCB = nvDevice->createBuffer(cbDesc); + } + m_lightShadowCullReady = true; + if (m_lightShadowCullPipeline && m_lightShadowCullParamsCB) + Msg("* [GPUCulling] light_shadow_cull pipeline ready"); + return m_lightShadowCullPipeline != nullptr && m_lightShadowCullParamsCB != nullptr; +} + +bool GPUCullingManager::EnsureLocalShadowCullSlots(nvrhi::IDevice* nvDevice, u32 /*slotCount*/) +{ + if (!nvDevice) + return false; + if (m_localShadowCullSlotCount >= kLocalShadowCullMaxSlots && !m_localShadowCullSlots.empty()) + return true; + + m_localShadowCullSlots.clear(); + m_localShadowCullSlots.resize(kLocalShadowCullMaxSlots); + m_localShadowCullSlotCount = 0; + + const u32 slotCount = kLocalShadowCullMaxSlots; + const u32 cap = kLocalShadowCullSlotCap; + static const char* kSetNames[3] = {"Static", "Dynamic", "Transparent"}; + for (u32 s = 0; s < slotCount; ++s) + { + for (u32 si = 0; si < 3; ++si) + { + auto& set = m_localShadowCullSlots[s].sets[si]; + char nameBuf[128]; + { + nvrhi::BufferDesc d; + snprintf(nameBuf, sizeof(nameBuf), "LocalShadowCull_s%u_%s_Idx", s, kSetNames[si]); + d.debugName = nameBuf; + d.byteSize = u64(cap) * sizeof(u32); + d.structStride = sizeof(u32); + d.canHaveUAVs = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + d.keepInitialState = true; + set.indices = nvDevice->createBuffer(d); + } + { + nvrhi::BufferDesc d; + snprintf(nameBuf, sizeof(nameBuf), "LocalShadowCull_s%u_%s_Args", s, kSetNames[si]); + d.debugName = nameBuf; + d.byteSize = u64(cap) * sizeof(IndirectDrawArgs); + d.structStride = sizeof(IndirectDrawArgs); + d.canHaveUAVs = true; + d.canHaveRawViews = true; + d.isDrawIndirectArgs = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + d.keepInitialState = true; + set.drawArgs = nvDevice->createBuffer(d); + } + { + nvrhi::BufferDesc d; + snprintf(nameBuf, sizeof(nameBuf), "LocalShadowCull_s%u_%s_Mats", s, kSetNames[si]); + d.debugName = nameBuf; + d.byteSize = u64(cap) * sizeof(u32); + d.structStride = sizeof(u32); + d.canHaveUAVs = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + d.keepInitialState = true; + set.mats = nvDevice->createBuffer(d); + } + { + nvrhi::BufferDesc d; + snprintf(nameBuf, sizeof(nameBuf), "LocalShadowCull_s%u_%s_Cnt", s, kSetNames[si]); + d.debugName = nameBuf; + d.byteSize = sizeof(u32); + d.canHaveUAVs = true; + d.canHaveRawViews = true; + d.isDrawIndirectArgs = true; + d.initialState = nvrhi::ResourceStates::UnorderedAccess; + d.keepInitialState = true; + set.count = nvDevice->createBuffer(d); + } + if (!set.indices || !set.drawArgs || !set.mats || !set.count) + { + m_localShadowCullSlots.clear(); + m_localShadowCullSlotCount = 0; + Msg("! [GPUCulling] LocalShadowCull slot alloc failed at %u", s); + return false; + } + } + } + m_localShadowCullSlotCount = slotCount; + Msg("* [GPUCulling] LocalShadowCull slots ready (%u x %u casters)", slotCount, cap); + return true; +} + +nvrhi::BindingSetHandle* GPUCullingManager::MutLocalShadowSlotDrawBinding(u32 slot, u32 setIdx) +{ + if (slot >= m_localShadowCullSlotCount || setIdx >= 3) + return nullptr; + return std::addressof(m_localShadowCullSlots[slot].sets[setIdx].drawBinding); +} + +nvrhi::IBuffer** GPUCullingManager::MutLocalShadowSlotDrawSrcInstance(u32 slot, u32 setIdx) +{ + if (slot >= m_localShadowCullSlotCount || setIdx >= 3) + return nullptr; + return std::addressof(m_localShadowCullSlots[slot].sets[setIdx].drawSrcInstance); +} + +nvrhi::IBuffer* GPUCullingManager::GetLocalShadowSlotIndices(u32 slot, u32 setIdx) const +{ + if (slot >= m_localShadowCullSlotCount || setIdx >= 3) + return nullptr; + return m_localShadowCullSlots[slot].sets[setIdx].indices.Get(); +} +nvrhi::IBuffer* GPUCullingManager::GetLocalShadowSlotDrawArgs(u32 slot, u32 setIdx) const +{ + if (slot >= m_localShadowCullSlotCount || setIdx >= 3) + return nullptr; + return m_localShadowCullSlots[slot].sets[setIdx].drawArgs.Get(); +} +nvrhi::IBuffer* GPUCullingManager::GetLocalShadowSlotMats(u32 slot, u32 setIdx) const +{ + if (slot >= m_localShadowCullSlotCount || setIdx >= 3) + return nullptr; + return m_localShadowCullSlots[slot].sets[setIdx].mats.Get(); +} +nvrhi::IBuffer* GPUCullingManager::GetLocalShadowSlotCount(u32 slot, u32 setIdx) const +{ + if (slot >= m_localShadowCullSlotCount || setIdx >= 3) + return nullptr; + return m_localShadowCullSlots[slot].sets[setIdx].count.Get(); +} + +bool GPUCullingManager::DispatchLightFrustumCasters(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const Fmatrix& lightVP, float radiusInflate) +{ + if (!cmdList || !nvDevice) + return false; + + ZoneScopedN("DispatchLightFrustumCasters"); + if (!EnsureLightShadowCullPipeline(nvDevice)) + return false; + + struct LightShadowCullParamsCB + { + Fvector4 frustumPlanes[6]; + u32 objectCount; + float radiusInflate; + u32 maxOut; + u32 pad; + }; + + Fmatrix vp = lightVP; + Fvector4 planes[6]; + ExtractFrustumPlanes(vp, planes); + + auto dispatchSet = [&](CullSetBuffers& set, const char* name) -> bool { + if (set.objectCount == 0) + return true; + if (!set.objectBuffer || !set.drawArgsBuffer || !set.materialIDBuffer) + return false; + + EnsureShadowCasterBuffers(cmdList, nvDevice, set, name); + if (!set.shadowIndicesBuffer || !set.shadowCompactDrawArgsBuffer || + !set.shadowCompactMaterialIDBuffer || !set.shadowCountBuffer) + return false; + + LightShadowCullParamsCB cb{}; + for (u32 i = 0; i < 6; ++i) + cb.frustumPlanes[i] = planes[i]; + cb.objectCount = set.objectCount; + cb.radiusInflate = radiusInflate; + cb.maxOut = set.objectCount; + cmdList->writeBuffer(m_lightShadowCullParamsCB, &cb, sizeof(cb)); + + const u32 zero = 0; + cmdList->writeBuffer(set.shadowCountBuffer, &zero, sizeof(u32)); + + cmdList->setBufferState(set.objectBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.drawArgsBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.materialIDBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.shadowIndicesBuffer, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(set.shadowCompactDrawArgsBuffer, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(set.shadowCompactMaterialIDBuffer, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(set.shadowCountBuffer, nvrhi::ResourceStates::UnorderedAccess); + + auto* csRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("light_shadow_cull", ".cs"); + framegraph::BindingSetBuilder bsb(*csRefl, nvDevice, "LightShadowCull"); + bsb.ConstantBuffer("LightShadowCullParams", m_lightShadowCullParamsCB); + bsb.BufferSRV("g_Objects", set.objectBuffer); + bsb.BufferSRV("g_InputDrawArgs", set.drawArgsBuffer); + bsb.BufferSRV("g_InputMaterialIDs", set.materialIDBuffer); + bsb.BufferUAV("g_OutIndices", set.shadowIndicesBuffer); + bsb.BufferUAV("g_OutDrawArgs", set.shadowCompactDrawArgsBuffer); + bsb.BufferUAV("g_OutMaterialIDs", set.shadowCompactMaterialIDBuffer); + bsb.BufferUAV("g_OutCount", set.shadowCountBuffer); + + auto bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet( + bsb.Build(), m_lightShadowCullLayout, nvDevice); + if (!bindingSet) + return false; + + nvrhi::ComputeState state; + state.pipeline = m_lightShadowCullPipeline; + state.bindings = {bindingSet}; + cmdList->setComputeState(state); + cmdList->dispatch((set.objectCount + 63) / 64, 1, 1); + + cmdList->setBufferState(set.shadowIndicesBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.shadowCompactDrawArgsBuffer, nvrhi::ResourceStates::IndirectArgument); + cmdList->setBufferState(set.shadowCompactMaterialIDBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.shadowCountBuffer, nvrhi::ResourceStates::IndirectArgument); + return true; + }; + + if (!dispatchSet(m_staticSet, "Static")) + return false; + if (!dispatchSet(m_dynamicSet, "Dynamic")) + return false; + if (!dispatchSet(m_transparentSet, "Transparent")) + return false; + return true; +} + +bool GPUCullingManager::DispatchLightFrustumCastersToSlot(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const Fmatrix& lightVP, float radiusInflate, u32 slot) +{ + if (!cmdList || !nvDevice || slot >= m_localShadowCullSlotCount) + return false; + if (!EnsureLightShadowCullPipeline(nvDevice)) + return false; + + auto* csRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("light_shadow_cull", ".cs"); + if (!csRefl) + return false; + + struct LightShadowCullParamsCB + { + Fvector4 frustumPlanes[6]; + u32 objectCount; + float radiusInflate; + u32 maxOut; + u32 pad; + }; + + Fmatrix vp = lightVP; + Fvector4 planes[6]; + ExtractFrustumPlanes(vp, planes); + + CullSetBuffers* srcSets[3] = {&m_staticSet, &m_dynamicSet, &m_transparentSet}; + auto& slotSets = m_localShadowCullSlots[slot].sets; + auto& cache = framegraph::GetPassResourceCache(); + + for (u32 si = 0; si < 3; ++si) + { + CullSetBuffers& set = *srcSets[si]; + auto& out = slotSets[si]; + if (set.objectCount == 0) + { + const u32 zero = 0; + if (out.count) + cmdList->writeBuffer(out.count, &zero, sizeof(u32)); + continue; + } + if (!set.objectBuffer || !set.drawArgsBuffer || !set.materialIDBuffer) + return false; + if (!out.indices || !out.drawArgs || !out.mats || !out.count) + return false; + + LightShadowCullParamsCB cb{}; + for (u32 i = 0; i < 6; ++i) + cb.frustumPlanes[i] = planes[i]; + cb.objectCount = set.objectCount; + cb.radiusInflate = radiusInflate; + cb.maxOut = kLocalShadowCullSlotCap; + cmdList->writeBuffer(m_lightShadowCullParamsCB, &cb, sizeof(cb)); + + const u32 zero = 0; + cmdList->writeBuffer(out.count, &zero, sizeof(u32)); + + cmdList->setBufferState(out.indices, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(out.drawArgs, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(out.mats, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(out.count, nvrhi::ResourceStates::UnorderedAccess); + + nvrhi::IBuffer* srcObj = set.objectBuffer.Get(); + if (!out.cullBinding || out.cullSrcObject != srcObj) + { + framegraph::BindingSetBuilder bsb(*csRefl, nvDevice, "LightShadowCullSlot"); + bsb.ConstantBuffer("LightShadowCullParams", m_lightShadowCullParamsCB); + bsb.BufferSRV("g_Objects", set.objectBuffer); + bsb.BufferSRV("g_InputDrawArgs", set.drawArgsBuffer); + bsb.BufferSRV("g_InputMaterialIDs", set.materialIDBuffer); + bsb.BufferUAV("g_OutIndices", out.indices); + bsb.BufferUAV("g_OutDrawArgs", out.drawArgs); + bsb.BufferUAV("g_OutMaterialIDs", out.mats); + bsb.BufferUAV("g_OutCount", out.count); + out.cullBinding = cache.GetOrCreateBindingSet(bsb.Build(), m_lightShadowCullLayout, nvDevice); + out.cullSrcObject = out.cullBinding ? srcObj : nullptr; + if (!out.cullBinding) + return false; + } + + nvrhi::ComputeState state; + state.pipeline = m_lightShadowCullPipeline; + state.bindings = {out.cullBinding}; + cmdList->setComputeState(state); + cmdList->dispatch((set.objectCount + 63) / 64, 1, 1); + + cmdList->setBufferState(out.indices, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(out.drawArgs, nvrhi::ResourceStates::IndirectArgument); + cmdList->setBufferState(out.mats, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(out.count, nvrhi::ResourceStates::IndirectArgument); + } + return true; +} + +bool GPUCullingManager::DispatchAllLocalShadowTileCulls( + nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const xr_vector& tiles, float radiusInflate) +{ + ZoneScopedN("DispatchAllLocalShadowTileCulls"); + if (!cmdList || !nvDevice || tiles.empty()) + return false; + if (!EnsureLightShadowCullPipeline(nvDevice)) + return false; + + auto* csRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("light_shadow_cull", ".cs"); + if (!csRefl) + return false; + + CullSetBuffers* srcSets[3] = {&m_staticSet, &m_dynamicSet, &m_transparentSet}; + bool anySrc = false; + for (u32 si = 0; si < 3; ++si) + { + CullSetBuffers& set = *srcSets[si]; + if (set.objectCount == 0 || !set.objectBuffer || !set.drawArgsBuffer || !set.materialIDBuffer) + continue; + cmdList->setBufferState(set.objectBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.drawArgsBuffer, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(set.materialIDBuffer, nvrhi::ResourceStates::ShaderResource); + anySrc = true; + } + if (!anySrc) + return false; + + struct LightShadowCullParamsCB + { + Fvector4 frustumPlanes[6]; + u32 objectCount; + float radiusInflate; + u32 maxOut; + u32 pad; + }; + + struct CullJob + { + u32 slot = 0; + u32 setIdx = 0; + u32 objectCount = 0; + Fvector4 planes[6]{}; + }; + static thread_local xr_vector s_jobs; + static thread_local xr_vector s_outs; + static thread_local xr_vector s_zeroCounts; + auto& jobs = s_jobs; + auto& outs = s_outs; + auto& zeroCounts = s_zeroCounts; + jobs.clear(); + outs.clear(); + zeroCounts.clear(); + + const u32 maxSlots = GetLocalShadowCullSlotCount(); + const u32 zero = 0; + bool anyTile = false; + for (u32 i = 0; i < tiles.size() && i < maxSlots; ++i) + { + const LocalShadowTile& tile = tiles[i]; + if (tile.size < 8 || !tile.needsRedraw) + continue; + anyTile = true; + + Fmatrix vp = tile.clipVP; + Fvector4 planes[6]; + ExtractFrustumPlanes(vp, planes); + + auto& slotSets = m_localShadowCullSlots[i].sets; + for (u32 si = 0; si < 3; ++si) + { + CullSetBuffers& set = *srcSets[si]; + auto& out = slotSets[si]; + if (!out.indices || !out.drawArgs || !out.mats || !out.count) + return false; + cmdList->writeBuffer(out.count, &zero, sizeof(u32)); + if (set.objectCount == 0) + { + zeroCounts.push_back(out.count.Get()); + continue; + } + CullJob job{}; + job.slot = i; + job.setIdx = si; + job.objectCount = set.objectCount; + for (u32 p = 0; p < 6; ++p) + job.planes[p] = planes[p]; + jobs.push_back(job); + outs.push_back(&out); + } + } + if (!anyTile) + return false; + if (jobs.empty()) + { + for (nvrhi::IBuffer* countBuf : zeroCounts) + { + if (countBuf) + cmdList->setBufferState(countBuf, nvrhi::ResourceStates::IndirectArgument); + } + return true; + } + + auto& cache = framegraph::GetPassResourceCache(); + for (u32 ji = 0; ji < jobs.size(); ++ji) + { + LocalShadowCullSlotSet& out = *outs[ji]; + cmdList->setBufferState(out.indices, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(out.drawArgs, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(out.mats, nvrhi::ResourceStates::UnorderedAccess); + cmdList->setBufferState(out.count, nvrhi::ResourceStates::UnorderedAccess); + } + + for (u32 ji = 0; ji < jobs.size(); ++ji) + { + const CullJob& job = jobs[ji]; + CullSetBuffers& set = *srcSets[job.setIdx]; + LocalShadowCullSlotSet& out = *outs[ji]; + + LightShadowCullParamsCB cb{}; + for (u32 p = 0; p < 6; ++p) + cb.frustumPlanes[p] = job.planes[p]; + cb.objectCount = job.objectCount; + cb.radiusInflate = radiusInflate; + cb.maxOut = kLocalShadowCullSlotCap; + cmdList->writeBuffer(m_lightShadowCullParamsCB, &cb, sizeof(cb)); + + nvrhi::IBuffer* srcObj = set.objectBuffer.Get(); + if (!out.cullBinding || out.cullSrcObject != srcObj) + { + framegraph::BindingSetBuilder bsb(*csRefl, nvDevice, "LightShadowCullSlot"); + bsb.ConstantBuffer("LightShadowCullParams", m_lightShadowCullParamsCB); + bsb.BufferSRV("g_Objects", set.objectBuffer); + bsb.BufferSRV("g_InputDrawArgs", set.drawArgsBuffer); + bsb.BufferSRV("g_InputMaterialIDs", set.materialIDBuffer); + bsb.BufferUAV("g_OutIndices", out.indices); + bsb.BufferUAV("g_OutDrawArgs", out.drawArgs); + bsb.BufferUAV("g_OutMaterialIDs", out.mats); + bsb.BufferUAV("g_OutCount", out.count); + out.cullBinding = cache.GetOrCreateBindingSet(bsb.Build(), m_lightShadowCullLayout, nvDevice); + out.cullSrcObject = out.cullBinding ? srcObj : nullptr; + if (!out.cullBinding) + return false; + } + + nvrhi::ComputeState state; + state.pipeline = m_lightShadowCullPipeline; + state.bindings = {out.cullBinding}; + cmdList->setComputeState(state); + cmdList->dispatch((job.objectCount + 63) / 64, 1, 1); + } + + for (u32 ji = 0; ji < jobs.size(); ++ji) + { + LocalShadowCullSlotSet& out = *outs[ji]; + cmdList->setBufferState(out.indices, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(out.drawArgs, nvrhi::ResourceStates::IndirectArgument); + cmdList->setBufferState(out.mats, nvrhi::ResourceStates::ShaderResource); + cmdList->setBufferState(out.count, nvrhi::ResourceStates::IndirectArgument); + } + for (nvrhi::IBuffer* countBuf : zeroCounts) + { + if (countBuf) + cmdList->setBufferState(countBuf, nvrhi::ResourceStates::IndirectArgument); + } + return true; +} + +void GPUCullingManager::EnsureShadowCasterIdentityBuffers( + nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice) +{ + if (!cmdList || !nvDevice) + return; + + auto restoreIdentity = [&](CullSetBuffers& set, const char* name) { + EnsureShadowCasterBuffers(cmdList, nvDevice, set, name); + if (!set.shadowIndicesBuffer || set.objectCount == 0) + return; + + // CSM light-frustum cull overwrites indices with a compact subset. Restore a + // full identity map so cast-all local shadows see every caster again. + xr_vector ids(set.objectCount); + for (u32 i = 0; i < set.objectCount; ++i) + ids[i] = i; + cmdList->writeBuffer(set.shadowIndicesBuffer, ids.data(), u64(set.objectCount) * sizeof(u32)); + if (set.shadowCountBuffer) + cmdList->writeBuffer(set.shadowCountBuffer, &set.objectCount, sizeof(u32)); + }; + + restoreIdentity(m_staticSet, "Static"); + restoreIdentity(m_dynamicSet, "Dynamic"); + restoreIdentity(m_transparentSet, "Transparent"); +} + +// ═══════════════════════════════════════════════════════ +// SETUP CULLING PASS +// ═══════════════════════════════════════════════════════ + +GPUCullOutput GPUCullingManager::SetupCullingPass( + framegraph::FrameGraph& fg, + framegraph::VirtualResourceHandle hizPyramid, + u32 hizWidth, + u32 hizHeight, + u32 hizMipLevels, + const GeometryCollector* geometry, + const Fmatrix& prevViewProj, + bool forceDisableHiz, + bool skipUpload, + framegraph::VirtualResourceHandle waitFor) +{ + using namespace framegraph; + + GPUCullOutput output; output.maxObjects = m_maxObjects; output.visibleIndices = VirtualResourceHandle(); output.visibleCount = VirtualResourceHandle(); @@ -2546,15 +3779,19 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( struct GPUCullPassData { VirtualResourceHandle hizPyramid; - VirtualResourceHandle staticDrawArgsBuffer; // For framegraph tracking - VirtualResourceHandle dynamicDrawArgsBuffer; // For framegraph tracking + VirtualResourceHandle staticDrawArgsBuffer; + VirtualResourceHandle dynamicDrawArgsBuffer; + VirtualResourceHandle staticCompactDrawArgsBuffer; + VirtualResourceHandle dynamicCompactDrawArgsBuffer; GPUCullingManager* manager; - const GeometryCollector* geometry; // For uploading during execute - Fmatrix prevViewProj; // Previous frame's viewProj for temporal Hi-Z + const GeometryCollector* geometry; + Fmatrix prevViewProj; u32 hizWidth; u32 hizHeight; u32 hizMipLevels; + bool forceDisableHiz = false; + bool skipUpload = false; }; // Import draw args buffers into framegraph for proper state tracking @@ -2567,30 +3804,82 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( drawArgsDesc.isUAV = true; drawArgsDesc.isTransient = false; // Persistent - forward pass needs it - VirtualResourceHandle staticDrawArgsHandle = fg.ImportBuffer("gpu_cull_static_drawargs", m_staticSet.drawArgsBuffer, drawArgsDesc); - VirtualResourceHandle dynamicDrawArgsHandle = fg.ImportBuffer("gpu_cull_dynamic_drawargs", m_dynamicSet.drawArgsBuffer, drawArgsDesc); + VirtualResourceHandle staticDrawArgsHandle = fg.ImportBuffer( + skipUpload ? "gpu_cull_static_drawargs_hiz" : "gpu_cull_static_drawargs", + m_staticSet.drawArgsBuffer, drawArgsDesc); + VirtualResourceHandle dynamicDrawArgsHandle = fg.ImportBuffer( + skipUpload ? "gpu_cull_dynamic_drawargs_hiz" : "gpu_cull_dynamic_drawargs", + m_dynamicSet.drawArgsBuffer, drawArgsDesc); + + VirtualResourceHandle staticCompactDrawArgsHandle; + VirtualResourceHandle dynamicCompactDrawArgsHandle; + if (m_compactEnabled && m_staticSet.compactDrawArgsBuffer && m_dynamicSet.compactDrawArgsBuffer) + { + ResourceDesc compactArgsDesc; + compactArgsDesc.type = ResourceDesc::Type::Buffer; + compactArgsDesc.debugName = "GPUCull_CompactDrawArgs"; + compactArgsDesc.bufferSize = m_maxObjects * sizeof(IndirectDrawArgs); + compactArgsDesc.isUAV = true; + compactArgsDesc.isTransient = false; + + staticCompactDrawArgsHandle = fg.ImportBuffer( + skipUpload ? "gpu_cull_static_compact_drawargs_hiz" : "gpu_cull_static_compact_drawargs", + m_staticSet.compactDrawArgsBuffer, compactArgsDesc); + dynamicCompactDrawArgsHandle = fg.ImportBuffer( + skipUpload ? "gpu_cull_dynamic_compact_drawargs_hiz" : "gpu_cull_dynamic_compact_drawargs", + m_dynamicSet.compactDrawArgsBuffer, compactArgsDesc); + } + + // Ensure Hi-Z handle is valid for binding (dummy 1x1 when pyramid missing) + VirtualResourceHandle hizHandle = hizPyramid; + u32 effHizW = hizWidth; + u32 effHizH = hizHeight; + u32 effHizMips = hizMipLevels; + if (!hizHandle.is_valid() && m_dummyHizTexture) + { + ResourceDesc dummyDesc; + dummyDesc.type = ResourceDesc::Type::Texture2D; + dummyDesc.debugName = "rt_DummyHiZ"; + dummyDesc.width = 1; + dummyDesc.height = 1; + dummyDesc.format = nvrhi::Format::R32_FLOAT; + dummyDesc.isImported = true; + dummyDesc.isTransient = false; + hizHandle = fg.ImportTexture("rt_DummyHiZ", m_dummyHizTexture, dummyDesc); + effHizW = 1; + effHizH = 1; + effHizMips = 1; + forceDisableHiz = true; + } auto& passData = fg.addCallbackPass( - "GPU Culling", + skipUpload ? "GPU Culling HiZ" : "GPU Culling", // Setup lambda - [&, hizWidth, hizHeight, hizMipLevels, staticDrawArgsHandle, dynamicDrawArgsHandle, geometry, prevViewProj](FrameGraph& builder, PassHandle passHandle, GPUCullPassData& data) { + [&, effHizW, effHizH, effHizMips, staticDrawArgsHandle, dynamicDrawArgsHandle, + staticCompactDrawArgsHandle, dynamicCompactDrawArgsHandle, + geometry, prevViewProj, forceDisableHiz, skipUpload, hizHandle, waitFor](FrameGraph& builder, PassHandle passHandle, GPUCullPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); - passBuilder.asyncCompute(); + passBuilder.sideEffects(); data.manager = this; - data.geometry = geometry; // Capture for upload during execute - data.prevViewProj = prevViewProj; // Previous frame's viewProj for temporal Hi-Z - data.hizWidth = hizWidth; - data.hizHeight = hizHeight; - data.hizMipLevels = hizMipLevels; - - // Read Hi-Z pyramid - data.hizPyramid = passBuilder.read(hizPyramid, ResourceState::ShaderResource); - - // Write draw args buffers (for dependency tracking) + data.geometry = geometry; + data.prevViewProj = prevViewProj; + data.hizWidth = effHizW; + data.hizHeight = effHizH; + data.hizMipLevels = effHizMips; + data.forceDisableHiz = forceDisableHiz; + data.skipUpload = skipUpload; + + if (waitFor.is_valid()) + passBuilder.read(waitFor, ResourceState::IndirectArgument); + data.hizPyramid = passBuilder.read(hizHandle, ResourceState::ShaderResource); data.staticDrawArgsBuffer = passBuilder.write(staticDrawArgsHandle, ResourceState::UnorderedAccess); data.dynamicDrawArgsBuffer = passBuilder.write(dynamicDrawArgsHandle, ResourceState::UnorderedAccess); + if (staticCompactDrawArgsHandle.is_valid()) + data.staticCompactDrawArgsBuffer = passBuilder.write(staticCompactDrawArgsHandle, ResourceState::UnorderedAccess); + if (dynamicCompactDrawArgsHandle.is_valid()) + data.dynamicCompactDrawArgsBuffer = passBuilder.write(dynamicCompactDrawArgsHandle, ResourceState::UnorderedAccess); }, // Execute lambda @@ -2612,18 +3901,20 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( // ───────────────────────────────────────────────────── // UPLOAD SCENE OBJECTS (must happen during execute, not setup) // ───────────────────────────────────────────────────── - // This ensures we use the correct command list - mgr->UploadSceneObjects(ctx, data.geometry); - - bindless::MaterialBuffer::Instance().Upload(ctx); - - if (mgr->m_rtAccelMgr) { - mgr->m_rtAccelMgr->BuildIfNeeded(cmdList, mgr); - if (mgr->m_rtAccelMgr->IsReady()) { - if (!mgr->m_rtAccelMgr->GetMaterialBuffer()) - mgr->m_rtAccelMgr->SetMaterialBuffer(bindless::MaterialBuffer::Instance().GetBuffer()); - if (!mgr->m_rtAccelMgr->GetTerrainMaterialBuffer()) - mgr->m_rtAccelMgr->SetTerrainMaterialBuffer(bindless::TerrainMaterialBuffer::Instance().GetBuffer()); + if (!data.skipUpload) + { + mgr->UploadSceneObjects(ctx, data.geometry); + + bindless::MaterialBuffer::Instance().Upload(ctx); + + if (mgr->m_rtAccelMgr) { + mgr->m_rtAccelMgr->BuildIfNeeded(cmdList, mgr); + if (mgr->m_rtAccelMgr->IsReady()) { + if (!mgr->m_rtAccelMgr->GetMaterialBuffer()) + mgr->m_rtAccelMgr->SetMaterialBuffer(bindless::MaterialBuffer::Instance().GetBuffer()); + if (!mgr->m_rtAccelMgr->GetTerrainMaterialBuffer()) + mgr->m_rtAccelMgr->SetTerrainMaterialBuffer(bindless::TerrainMaterialBuffer::Instance().GetBuffer()); + } } } @@ -2634,21 +3925,21 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( return; } - auto dispatchCullSet = [&](CullSetBuffers& set, VariantPartitionBuffers* partition = nullptr) { + auto dispatchCullSet = [&](CullSetBuffers& set, VariantPartitionBuffers* partition = nullptr, bool allowHiz = true) { if (set.objectCount == 0) return; R_ASSERT2(set.objectBuffer && set.visibleIndexBuffer && set.visibleCountBuffer && set.visibilityBuffer, "Cull set buffers not initialized"); - // Clear visible count to 0 u32 zero = 0; cmdList->writeBuffer(set.visibleCountBuffer, &zero, sizeof(u32)); + cmdList->clearBufferUInt(set.visibilityBuffer, 0); // Fill constant buffer CullParamsCB cb; cb.viewProj = Device.mFullTransform; - cb.prevViewProj = data.prevViewProj; // Previous frame's viewProj for temporal Hi-Z + cb.prevViewProj = data.prevViewProj; cb.cameraPos = Device.vCameraPosition; float farPlane = g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.far_plane : 300.0f; cb.maxDistanceSq = farPlane * farPlane; @@ -2657,7 +3948,10 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( cb.hizHeight = data.hizHeight; cb.hizMipLevels = data.hizMipLevels; cb.frameId = frameId; - cb.padding[0] = cb.padding[1] = cb.padding[2] = 0; + // Hi-Z occlusion only for static world geometry (stable, doesn't move). + // Dynamic props flicker under temporal Hi-Z → frustum/distance only. + cb.hizEnable = (data.forceDisableHiz || ps_r_hiz_occlusion == 0 || !allowHiz) ? 0u : 1u; + cb.padding[0] = cb.padding[1] = 0; mgr->ExtractFrustumPlanes(Device.mFullTransform, cb.frustumPlanes); @@ -2672,7 +3966,7 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( .BufferUAV("g_VisibleCount", set.visibleCountBuffer) .BufferUAV("g_Visibility", set.visibilityBuffer); - nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_cullLayout); + nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_cullLayout, nvDevice); R_ASSERT2(bindingSet, "Failed to create culling binding set"); // Set compute state and dispatch culling @@ -2770,6 +4064,10 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( cmdList->setBufferState(set.compactBatchIndicesBuffer, nvrhi::ResourceStates::UnorderedAccess); cmdList->setBufferState(set.compactMaterialIDBuffer, nvrhi::ResourceStates::UnorderedAccess); + // Without drawIndirectCount, trailing compact slots must be zero (instanceCount=0). + if (GEnv.Backend && !GEnv.Backend->GetCapabilities().drawIndirectCount) + cmdList->clearBufferUInt(set.compactDrawArgsBuffer, 0); + auto* compactScatterRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("batch_compact", ".cs"); framegraph::BindingSetBuilder scatterBsb(*compactScatterRefl, nvDevice); scatterBsb.ConstantBuffer("CompactParams", mgr->m_device->GetNativeBuffer(mgr->m_compactParamsCB)) @@ -2802,8 +4100,8 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( }; dispatchCullSet(mgr->m_staticSet, - mgr->m_variantPartitionEnabled ? &mgr->m_staticPartition : nullptr); - dispatchCullSet(mgr->m_dynamicSet); + mgr->m_variantPartitionEnabled ? &mgr->m_staticPartition : nullptr, /*allowHiz*/ true); + dispatchCullSet(mgr->m_dynamicSet, nullptr, /*allowHiz*/ false); // ───────────────────────────────────────────────────── // TERRAIN CULLING PASS (uses same shader, different data) @@ -2814,9 +4112,11 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( } if (mgr->m_terrainObjectCount > 0 && mgr->m_terrainObjectBuffer && mgr->m_terrainDrawArgsBuffer) { - // Clear terrain visible count and visibility buffer + // Clear terrain visible count and visibility stamps (stale frameId → flicker) u32 zeroTerrain = 0; cmdList->writeBuffer(mgr->m_terrainVisibleCountBuffer, &zeroTerrain, sizeof(u32)); + if (mgr->m_terrainVisibilityBuffer) + cmdList->clearBufferUInt(mgr->m_terrainVisibilityBuffer, 0); // Update constant buffer for terrain (reuse same CB, different object count) CullParamsCB terrainCB; terrainCB.viewProj = Device.mFullTransform; @@ -2829,7 +4129,11 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( terrainCB.hizHeight = data.hizHeight; terrainCB.hizMipLevels = data.hizMipLevels; terrainCB.frameId = frameId; - terrainCB.padding[0] = terrainCB.padding[1] = terrainCB.padding[2] = 0; + // Terrain: same-frame Hi-Z with conservative AABB (Cull2 after prepass). + // Keep off for temporal/dummy (width<=1) — mip flicker on ground plane. + terrainCB.hizEnable = (data.forceDisableHiz || ps_r_hiz_occlusion == 0 || + data.hizWidth <= 1) ? 0u : 1u; + terrainCB.padding[0] = terrainCB.padding[1] = 0; mgr->ExtractFrustumPlanes(Device.mFullTransform, terrainCB.frustumPlanes); cmdList->writeBuffer(mgr->m_device->GetNativeBuffer(mgr->m_cullParamsCB), &terrainCB, sizeof(terrainCB)); @@ -2842,7 +4146,7 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( .BufferUAV("g_VisibleCount", mgr->m_terrainVisibleCountBuffer) .BufferUAV("g_Visibility", mgr->m_terrainVisibilityBuffer); - nvrhi::BindingSetHandle terrainBindingSet = nvDevice->createBindingSet(terrainBsb.Build(), mgr->m_cullLayout); + nvrhi::BindingSetHandle terrainBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(terrainBsb.Build(), mgr->m_cullLayout, nvDevice); R_ASSERT2(terrainBindingSet, "Terrain culling binding set creation failed"); nvrhi::ComputeState terrainState; @@ -2941,6 +4245,10 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( cmdList->setBufferState(mgr->m_terrainCompactBatchIndicesBuffer, nvrhi::ResourceStates::UnorderedAccess); cmdList->setBufferState(mgr->m_terrainCompactMaterialIDBuffer, nvrhi::ResourceStates::UnorderedAccess); + // Without drawIndirectCount, trailing compact slots must be zero (instanceCount=0). + if (GEnv.Backend && !GEnv.Backend->GetCapabilities().drawIndirectCount) + cmdList->clearBufferUInt(mgr->m_terrainCompactDrawArgsBuffer, 0); + auto* compactScatterRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("batch_compact", ".cs"); framegraph::BindingSetBuilder terrainScatterBsb(*compactScatterRefl, nvDevice); terrainScatterBsb.ConstantBuffer("CompactParams", mgr->m_device->GetNativeBuffer(mgr->m_compactParamsCB)) @@ -2972,8 +4280,13 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( // ───────────────────────────────────────────────────── // TRANSPARENT CULLING (uses CullSetBuffers + dispatchCullSet) // ───────────────────────────────────────────────────── + // Transparent/foliage: frustum only — same-frame Hi-Z false-culls swaying trees. dispatchCullSet(mgr->m_transparentSet, - mgr->m_variantPartitionEnabled ? &mgr->m_transparentPartition : nullptr); + mgr->m_variantPartitionEnabled ? &mgr->m_transparentPartition : nullptr, + /*allowHiz*/ false); + + // Tessellation uses CPU frustum + direct drawIndexed in ForwardColor + // (no GPU compact / DrawIndexedIndirect — hangs on MoltenVK with PatchList). // Note: partition buffers are left in IndirectArgument/ShaderResource state // for the render passes to consume. keepInitialState handles reset for next frame. @@ -2982,7 +4295,10 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( output.visibleIndices = VirtualResourceHandle(); // Not using framegraph for these output.visibleCount = VirtualResourceHandle(); - output.drawArgsBuffer = passData.staticDrawArgsBuffer; + // Forward/Depth sample compact draw-args; expose that producer edge (not the pre-compact buffer). + output.drawArgsBuffer = passData.staticCompactDrawArgsBuffer.is_valid() + ? passData.staticCompactDrawArgsBuffer + : passData.staticDrawArgsBuffer; output.staticDrawArgsBuffer = passData.staticDrawArgsBuffer; output.dynamicDrawArgsBuffer = passData.dynamicDrawArgsBuffer; output.staticObjectCount = m_staticSet.objectCount; @@ -2990,13 +4306,8 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( // Set compact output handles if compaction is enabled if (m_compactEnabled) { - // Import compact buffers into framegraph - ResourceDesc compactArgsDesc; - compactArgsDesc.type = ResourceDesc::Type::Buffer; - compactArgsDesc.debugName = "GPUCull_CompactDrawArgs"; - compactArgsDesc.bufferSize = m_maxObjects * sizeof(IndirectDrawArgs); - compactArgsDesc.isUAV = true; - compactArgsDesc.isTransient = false; + output.staticCompactDrawArgs = passData.staticCompactDrawArgsBuffer; + output.dynamicCompactDrawArgs = passData.dynamicCompactDrawArgsBuffer; ResourceDesc compactIndicesDesc; compactIndicesDesc.type = ResourceDesc::Type::Buffer; @@ -3006,11 +4317,12 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( compactIndicesDesc.isUAV = true; compactIndicesDesc.isTransient = false; - output.staticCompactDrawArgs = fg.ImportBuffer("gpu_cull_static_compact_drawargs", m_staticSet.compactDrawArgsBuffer, compactArgsDesc); - output.staticCompactBatchIndices = fg.ImportBuffer("gpu_cull_static_compact_batchindices", m_staticSet.compactBatchIndicesBuffer, compactIndicesDesc); - - output.dynamicCompactDrawArgs = fg.ImportBuffer("gpu_cull_dynamic_compact_drawargs", m_dynamicSet.compactDrawArgsBuffer, compactArgsDesc); - output.dynamicCompactBatchIndices = fg.ImportBuffer("gpu_cull_dynamic_compact_batchindices", m_dynamicSet.compactBatchIndicesBuffer, compactIndicesDesc); + output.staticCompactBatchIndices = fg.ImportBuffer( + skipUpload ? "gpu_cull_static_compact_batchindices_hiz" : "gpu_cull_static_compact_batchindices", + m_staticSet.compactBatchIndicesBuffer, compactIndicesDesc); + output.dynamicCompactBatchIndices = fg.ImportBuffer( + skipUpload ? "gpu_cull_dynamic_compact_batchindices_hiz" : "gpu_cull_dynamic_compact_batchindices", + m_dynamicSet.compactBatchIndicesBuffer, compactIndicesDesc); } else { output.staticCompactDrawArgs = VirtualResourceHandle(); output.staticCompactBatchIndices = VirtualResourceHandle(); @@ -3165,6 +4477,7 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( u32 hizWidth; u32 hizHeight; u32 hizMipLevels; + bool forceDisableHiz = false; }; // Import visibility buffer into framegraph for proper dependency tracking @@ -3189,24 +4502,48 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( VirtualResourceHandle argsBufferHandle = fg.ImportBuffer( "skinned_draw_args", m_skinnedDrawArgsBuffer, argsBufferDesc); + VirtualResourceHandle hizHandle = hizPyramid; + u32 effHizW = hizWidth; + u32 effHizH = hizHeight; + u32 effHizMips = hizMipLevels; + bool forceDisableHiz = false; + if (!hizHandle.is_valid() && m_dummyHizTexture) + { + ResourceDesc dummyDesc; + dummyDesc.type = ResourceDesc::Type::Texture2D; + dummyDesc.debugName = "rt_DummyHiZ_Skinned"; + dummyDesc.width = 1; + dummyDesc.height = 1; + dummyDesc.format = nvrhi::Format::R32_FLOAT; + dummyDesc.isImported = true; + dummyDesc.isTransient = false; + hizHandle = fg.ImportTexture("rt_DummyHiZ_Skinned", m_dummyHizTexture, dummyDesc); + effHizW = 1; + effHizH = 1; + effHizMips = 1; + forceDisableHiz = true; + } + auto& passData = fg.addCallbackPass( "Skinned GPU Culling", // Setup lambda - [&, hizWidth, hizHeight, hizMipLevels, geometry, prevViewProj, visBufferHandle, argsBufferHandle, overlayMgr](FrameGraph& builder, PassHandle passHandle, SkinnedCullPassData& data) { + [&, effHizW, effHizH, effHizMips, geometry, prevViewProj, visBufferHandle, argsBufferHandle, overlayMgr, hizHandle, forceDisableHiz](FrameGraph& builder, PassHandle passHandle, SkinnedCullPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); - passBuilder.asyncCompute(); + // Keep on graphics queue so Skinning sees compacted args same-frame. + passBuilder.sideEffects(); data.manager = this; data.geometry = geometry; data.overlayMgr = overlayMgr; data.prevViewProj = prevViewProj; - data.hizWidth = hizWidth; - data.hizHeight = hizHeight; - data.hizMipLevels = hizMipLevels; + data.hizWidth = effHizW; + data.hizHeight = effHizH; + data.hizMipLevels = effHizMips; + data.forceDisableHiz = forceDisableHiz; - // Read Hi-Z pyramid - data.hizPyramid = passBuilder.read(hizPyramid, ResourceState::ShaderResource); + // Read Hi-Z pyramid (real or dummy 1x1) + data.hizPyramid = passBuilder.read(hizHandle, ResourceState::ShaderResource); // Write visibility buffer (ensures pass isn't culled by framegraph) data.visibilityBuffer = passBuilder.write(visBufferHandle, ResourceState::UnorderedAccess); @@ -3251,6 +4588,25 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( cmdList->clearBufferUInt(mgr->m_skinnedVisibleCountBuffer, 0); + // Frustum-only cull false-rejects animated characters (tight rest-pose spheres). + // When Hi-Z occlusion is off, keep all skinned visible and only compact MDI. + if (data.forceDisableHiz || ps_r_hiz_occlusion == 0) + { + static thread_local xr_vector visStamp; + for (u32 f = SkinnedGeometryPools::FIRST_FORMAT; f < SkinnedGeometryPools::FORMAT_COUNT; ++f) + { + SkinnedBucket& bucket = mgr->m_skinnedBuckets[f]; + if (bucket.count == 0) + continue; + visStamp.assign(bucket.count, frameId); + cmdList->writeBuffer(bucket.visibilityBuffer, visStamp.data(), + u64(bucket.count) * sizeof(u32)); + mgr->DispatchSkinnedBucketCompaction(cmdList, nvDevice, bucket, frameId); + } + // Residual draw args already uploaded with instanceCount=1 — no gate. + return; + } + // Fill constant buffer (reuse m_cullParamsCB); objectCount rewritten per dispatch CullParamsCB cb; cb.viewProj = Device.mFullTransform; @@ -3262,7 +4618,8 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( cb.hizHeight = data.hizHeight; cb.hizMipLevels = data.hizMipLevels; cb.frameId = frameId; - cb.padding[0] = cb.padding[1] = cb.padding[2] = 0; + cb.hizEnable = 1u; + cb.padding[0] = cb.padding[1] = 0; mgr->ExtractFrustumPlanes(Device.mFullTransform, cb.frustumPlanes); auto* objectCullRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("object_cull", ".cs"); @@ -3281,7 +4638,7 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( .BufferUAV("g_VisibleCount", mgr->m_skinnedVisibleCountBuffer) .BufferUAV("g_Visibility", visibility); - nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_cullLayout); + nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_cullLayout, nvDevice); R_ASSERT2(bindingSet, "Failed to create skinned culling binding set"); nvrhi::ComputeState state; @@ -3313,7 +4670,7 @@ framegraph::VirtualResourceHandle GPUCullingManager::SetupSkinnedCullingPass( .BufferSRV("g_Visibility", mgr->m_skinnedVisibilityBuffer) .BufferUAV("g_DrawArgs", mgr->m_skinnedDrawArgsBuffer); - nvrhi::BindingSetHandle gateBindingSet = nvDevice->createBindingSet(gateBsb.Build(), mgr->m_skinnedArgsGateLayout); + nvrhi::BindingSetHandle gateBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(gateBsb.Build(), mgr->m_skinnedArgsGateLayout, nvDevice); R_ASSERT2(gateBindingSet, "Failed to create skinned args gate binding set"); nvrhi::ComputeState gateState; @@ -3506,7 +4863,6 @@ void GPUCullingManager::SetupDebugVisualizationPass( u32 hizWidth, u32 hizHeight, u32 hizMipLevels, - const Fmatrix& prevViewProj, const xr_vector* particleBatches) { using namespace framegraph; @@ -3531,13 +4887,12 @@ void GPUCullingManager::SetupDebugVisualizationPass( u32 hizWidth; u32 hizHeight; u32 hizMipLevels; - Fmatrix prevViewProj; }; fg.addCallbackPass( "GPU Culling Debug", - [&, hizWidth, hizHeight, hizMipLevels, particleCount, particleBatches, prevViewProj](FrameGraph& builder, PassHandle passHandle, DebugPassData& data) { + [&, hizWidth, hizHeight, hizMipLevels, particleCount, particleBatches](FrameGraph& builder, PassHandle passHandle, DebugPassData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.manager = this; @@ -3549,7 +4904,6 @@ void GPUCullingManager::SetupDebugVisualizationPass( data.hizWidth = hizWidth; data.hizHeight = hizHeight; data.hizMipLevels = hizMipLevels; - data.prevViewProj = prevViewProj; data.hizPyramid = passBuilder.read(hizPyramid, ResourceState::ShaderResource); data.colorTarget = passBuilder.write(colorTarget, ResourceState::RenderTarget); @@ -3583,7 +4937,6 @@ void GPUCullingManager::SetupDebugVisualizationPass( CullDebugParamsCB cb; cb.viewProj = Device.mFullTransform; - cb.prevViewProj = data.prevViewProj; cb.cameraPos = Device.vCameraPosition; cb.maxDistanceSq = farPlane * farPlane; cb.objectCount = objectCount; @@ -3603,7 +4956,7 @@ void GPUCullingManager::SetupDebugVisualizationPass( .Texture("g_HiZPyramid", hizTexture) .BufferUAV("g_DebugOutput", mgr->m_debugBuffer); - nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_debugComputeLayout); + nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_debugComputeLayout, nvDevice); R_ASSERT2(bindingSet, "Debug binding set creation failed"); nvrhi::ComputeState state; @@ -3642,7 +4995,6 @@ void GPUCullingManager::SetupDebugVisualizationPass( CullDebugParamsCB cb; cb.viewProj = Device.mFullTransform; - cb.prevViewProj = data.prevViewProj; cb.cameraPos = Device.vCameraPosition; cb.maxDistanceSq = farPlane * farPlane; cb.objectCount = static_cast(mgr->m_particleData.size()); @@ -3662,7 +5014,7 @@ void GPUCullingManager::SetupDebugVisualizationPass( .Texture("g_HiZPyramid", hizTexture) .BufferUAV("g_DebugOutput", mgr->m_debugBuffer); - nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_debugComputeLayout); + nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_debugComputeLayout, nvDevice); nvrhi::ComputeState state; state.pipeline = mgr->m_particleDebugComputePipeline; @@ -3692,7 +5044,7 @@ void GPUCullingManager::SetupDebugVisualizationPass( bsb.ConstantBuffer("CullDebugVSParams", mgr->m_device->GetNativeBuffer(mgr->m_debugGraphicsParamsCB)) .BufferSRV("g_DebugData", mgr->m_debugBuffer); - nvrhi::BindingSetHandle bindingSet = nvDevice->createBindingSet(bsb.Build(), mgr->m_debugGraphicsLayout); + nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_debugGraphicsLayout, nvDevice); nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(colorTexture); @@ -3735,16 +5087,314 @@ void GPUCullingManager::CreateParticleResources(fg::RenderDevice* device) nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); - nvrhi::BufferDesc desc; - desc.debugName = "GPUCull_Particles"; - desc.byteSize = m_maxParticles * sizeof(GPUParticleData); - desc.structStride = sizeof(GPUParticleData); - desc.initialState = nvrhi::ResourceStates::ShaderResource; - desc.keepInitialState = true; + auto particleCullResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("particle_cull"); + if (!particleCullResult.handle) { + Msg("! [GPUCulling] particle_cull.cs not found - particle culling disabled"); + return; + } + + { + nvrhi::BufferDesc desc; + desc.debugName = "GPUCull_Particles"; + desc.byteSize = m_maxParticles * sizeof(GPUParticleData); + desc.structStride = sizeof(GPUParticleData); + desc.initialState = nvrhi::ResourceStates::ShaderResource; + desc.keepInitialState = true; + + m_particleBuffer = nvDevice->createBuffer(desc); + if (!m_particleBuffer) { + Msg("! [GPUCulling] Failed to create particle buffer"); + return; + } + } + + { + nvrhi::BufferDesc desc; + desc.debugName = "GPUCull_ParticleDrawArgs"; + desc.byteSize = m_maxParticles * sizeof(IndirectDrawArgs); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.isDrawIndirectArgs = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; // Let NVRHI handle state transitions + + m_particleDrawArgsBuffer = nvDevice->createBuffer(desc); + if (!m_particleDrawArgsBuffer) { + Msg("! [GPUCulling] Failed to create particle draw args buffer"); + return; + } + } + + { + nvrhi::BufferDesc desc; + desc.debugName = "GPUCull_ParticleVisibleCount"; + desc.byteSize = sizeof(u32); + desc.structStride = sizeof(u32); + desc.canHaveUAVs = true; + desc.canHaveRawViews = true; + desc.initialState = nvrhi::ResourceStates::UnorderedAccess; + desc.keepInitialState = true; + + m_particleVisibleCountBuffer = nvDevice->createBuffer(desc); + if (!m_particleVisibleCountBuffer) { + Msg("! [GPUCulling] Failed to create particle visible count buffer"); + return; + } + } + + { + fg::RenderDevice::BufferDesc desc; + desc.debugName = "GPUCull_ParticleParams"; + desc.byteSize = sizeof(CullParamsCB); + desc.isConstantBuffer = true; + desc.isVolatile = true; + desc.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; + + m_particleCullParamsCB = m_device->CreateBuffer(desc); + if (!m_particleCullParamsCB.IsValid()) { + Msg("! [GPUCulling] Failed to create particle constant buffer"); + return; + } + } + + { + auto& cache = framegraph::GetPassResourceCache(); + auto* particleCullRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("particle_cull", ".cs"); + m_particleCullLayout = cache.GetOrCreateBindingLayoutFromReflection("GPUCull_ParticleCull", *particleCullRefl, nvDevice); + if (!m_particleCullLayout) { + Msg("! [GPUCulling] Failed to create particle binding layout"); + return; + } + } + + { + nvrhi::ComputePipelineDesc pipeDesc; + pipeDesc.CS = GEnv.Render->GetShaderLoader()->LoadComputeShader("particle_cull").handle; + pipeDesc.bindingLayouts = { m_particleCullLayout }; + + Msg("* [GPUCulling] Creating particle compute pipeline:"); + Msg(" CS shader: %p", pipeDesc.CS.Get()); + Msg(" Binding layouts: %u", pipeDesc.bindingLayouts.size()); + Msg(" Layout[0]: %p", pipeDesc.bindingLayouts[0].Get()); + + m_particleCullPipeline = nvDevice->createComputePipeline(pipeDesc); + if (!m_particleCullPipeline) { + Msg("! [GPUCulling] Failed to create particle compute pipeline"); + Msg("! [GPUCulling] Check NVRHI validation layer output above for details"); + return; + } + } + + m_particleCullEnabled = true; + Msg("* [GPUCulling] Particle culling resources created"); +} + +void GPUCullingManager::UploadParticleBatches(fg::RenderContext* ctx, const xr_vector* batches) +{ + ZoneScopedN("GPUCull::UploadParticles"); + + if (!m_particleCullEnabled || !batches) + return; + + m_particleCount = std::min(static_cast(batches->size()), m_maxParticles); + + if (m_particleCount == 0) + return; + + m_particleData.clear(); + m_particleData.reserve(m_particleCount); + m_particleDrawArgsData.clear(); + m_particleDrawArgsData.reserve(m_particleCount); + + for (u32 i = 0; i < m_particleCount; i++) { + const auto& batch = (*batches)[i]; + + if (!batch.visual) + continue; + + GPUParticleData particle; + particle.position = batch.visual->vis.sphere.P; + particle.radius = batch.visual->vis.sphere.R; + particle.batchIndex = i; + particle.flags = 0; + particle.pad0 = 0.0f; + particle.pad1 = 0.0f; + + m_particleData.push_back(particle); + + IndirectDrawArgs args; + args.indexCountPerInstance = batch.particleCount * 6; + args.instanceCount = 0; + args.startIndexLocation = 0; + args.baseVertexLocation = 0; + args.startInstanceLocation = 0; + + m_particleDrawArgsData.push_back(args); + } + + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + cmdList->writeBuffer(m_particleBuffer, m_particleData.data(), m_particleCount * sizeof(GPUParticleData)); + cmdList->writeBuffer(m_particleDrawArgsBuffer, m_particleDrawArgsData.data(), m_particleCount * sizeof(IndirectDrawArgs)); +} + +GPUParticleCullOutput GPUCullingManager::SetupParticleCullingPass( + framegraph::FrameGraph& fg, + framegraph::VirtualResourceHandle hizPyramid, + u32 hizWidth, + u32 hizHeight, + u32 hizMipLevels, + const xr_vector* batches) +{ + using namespace framegraph; + + GPUParticleCullOutput output; + output.maxParticles = m_maxParticles; + + if (!m_particleCullEnabled) { + output.drawArgsBuffer = VirtualResourceHandle(); + return output; + } + + m_particleCount = batches ? std::min(static_cast(batches->size()), m_maxParticles) : 0; + + if (m_particleCount == 0) { + output.drawArgsBuffer = VirtualResourceHandle(); + return output; + } + + struct ParticleCullPassData { + VirtualResourceHandle hizPyramid; + VirtualResourceHandle drawArgsBuffer; + + GPUCullingManager* manager; + const xr_vector* batches; + u32 particleCount; + u32 hizWidth; + u32 hizHeight; + u32 hizMipLevels; + bool forceDisableHiz = false; + }; - m_particleBuffer = nvDevice->createBuffer(desc); - if (!m_particleBuffer) - Msg("! [GPUCulling] Failed to create particle buffer"); + ResourceDesc drawArgsDesc; + drawArgsDesc.type = ResourceDesc::Type::Buffer; + drawArgsDesc.debugName = "GPUCull_ParticleDrawArgs"; + drawArgsDesc.bufferSize = m_maxParticles * sizeof(IndirectDrawArgs); + drawArgsDesc.structStride = sizeof(IndirectDrawArgs); + drawArgsDesc.isUAV = true; + drawArgsDesc.isTransient = false; + + VirtualResourceHandle drawArgsHandle = fg.ImportBuffer("gpu_cull_particle_drawargs", m_particleDrawArgsBuffer, drawArgsDesc); + + VirtualResourceHandle hizHandle = hizPyramid; + u32 effHizW = hizWidth; + u32 effHizH = hizHeight; + u32 effHizMips = hizMipLevels; + bool forceDisableHiz = false; + if (!hizHandle.is_valid() && m_dummyHizTexture) + { + ResourceDesc dummyDesc; + dummyDesc.type = ResourceDesc::Type::Texture2D; + dummyDesc.debugName = "rt_DummyHiZ_Particle"; + dummyDesc.width = 1; + dummyDesc.height = 1; + dummyDesc.format = nvrhi::Format::R32_FLOAT; + dummyDesc.isImported = true; + dummyDesc.isTransient = false; + hizHandle = fg.ImportTexture("rt_DummyHiZ_Particle", m_dummyHizTexture, dummyDesc); + effHizW = 1; + effHizH = 1; + effHizMips = 1; + forceDisableHiz = true; + } + + auto& passData = fg.addCallbackPass( + "GPU Particle Culling", + + [&, effHizW, effHizH, effHizMips, drawArgsHandle, batches, hizHandle, forceDisableHiz](FrameGraph& builder, PassHandle passHandle, ParticleCullPassData& data) { + RenderPassBuilder passBuilder(builder, passHandle); + passBuilder.sideEffects(); + + data.manager = this; + data.batches = batches; + data.particleCount = m_particleCount; + data.hizWidth = effHizW; + data.hizHeight = effHizH; + data.hizMipLevels = effHizMips; + data.forceDisableHiz = forceDisableHiz; + + data.hizPyramid = passBuilder.read(hizHandle, ResourceState::ShaderResource); + data.drawArgsBuffer = passBuilder.write(drawArgsHandle, ResourceState::UnorderedAccess); + }, + + [](const ParticleCullPassData& data, + const FrameGraph& fg, + fg::RenderContext* ctx) { + + GPUCullingManager* mgr = data.manager; + if (!mgr->m_particleCullEnabled || data.particleCount == 0) + return; + + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + nvrhi::IDevice* nvDevice = mgr->m_device->GetNVRHIDevice(); + + mgr->UploadParticleBatches(ctx, data.batches); + + nvrhi::ITexture* hizTexture = fg.GetPhysicalTexture(data.hizPyramid); + if (!hizTexture) { + Msg("! [GPUCulling] Hi-Z texture not available for particle culling"); + return; + } + + u32 zero = 0; + cmdList->writeBuffer(mgr->m_particleVisibleCountBuffer, &zero, sizeof(u32)); + + u32 frameId = Device.dwFrame + 1u; + if (frameId == 0) + frameId = 1; + + CullParamsCB cb; + cb.viewProj = Device.mFullTransform; + cb.cameraPos = Device.vCameraPosition; + float farPlane = g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.far_plane : 300.0f; + cb.maxDistanceSq = farPlane * farPlane; + cb.objectCount = data.particleCount; + cb.hizWidth = data.hizWidth; + cb.hizHeight = data.hizHeight; + cb.hizMipLevels = data.hizMipLevels; + cb.frameId = frameId; + cb.hizEnable = (data.forceDisableHiz || ps_r_hiz_occlusion == 0) ? 0u : 1u; + cb.padding[0] = cb.padding[1] = 0; + + mgr->ExtractFrustumPlanes(Device.mFullTransform, cb.frustumPlanes); + + cmdList->writeBuffer(mgr->m_device->GetNativeBuffer(mgr->m_particleCullParamsCB), &cb, sizeof(cb)); + + auto* particleCullRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("particle_cull", ".cs"); + framegraph::BindingSetBuilder bsb(*particleCullRefl, nvDevice, "GPUCull.ParticleCull"); + bsb.ConstantBuffer("ParticleCullParams", mgr->m_device->GetNativeBuffer(mgr->m_particleCullParamsCB)) + .BufferSRV("g_ParticleData", mgr->m_particleBuffer) + .Texture("g_HiZPyramid", hizTexture) + .BufferUAV("g_VisibleIndices", mgr->m_particleVisibleCountBuffer) + .BufferUAV("g_VisibleCount", mgr->m_particleDrawArgsBuffer); + + nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_particleCullLayout, nvDevice); + if (!bindingSet) { + Msg("! [GPUCulling] Failed to create particle binding set"); + return; + } + + nvrhi::ComputeState state; + state.pipeline = mgr->m_particleCullPipeline; + state.bindings = { bindingSet }; + cmdList->setComputeState(state); + + u32 groupCount = (data.particleCount + CULL_THREAD_GROUP_SIZE - 1) / CULL_THREAD_GROUP_SIZE; + cmdList->dispatch(groupCount, 1, 1); + } + ); + + output.drawArgsBuffer = passData.drawArgsBuffer; + return output; } // ═══════════════════════════════════════════════════════ diff --git a/src/Layers/xrRender/GPUCullingManager.h b/src/Layers/xrRender/GPUCullingManager.h index 6e7f3593d47..143e64752c4 100644 --- a/src/Layers/xrRender/GPUCullingManager.h +++ b/src/Layers/xrRender/GPUCullingManager.h @@ -29,6 +29,7 @@ namespace xray::render::fg { class dxRender_Visual; // Forward declaration for visual pointer map class CKinematics; class RTAccelStructManager; + struct LocalShadowTile; namespace decals { class OverlayManager; } @@ -157,6 +158,11 @@ struct GPUCullOutput { }; +struct GPUParticleCullOutput { + framegraph::VirtualResourceHandle drawArgsBuffer; + u32 maxParticles; +}; + // ═══════════════════════════════════════════════════════ // GPU CULLING MANAGER // ═══════════════════════════════════════════════════════ @@ -185,10 +191,12 @@ class GPUCullingManager { // Shutdown and release resources void Shutdown(); - // Upload scene objects to GPU (call once per frame before culling) - // Extracts bounding sphere data from geometry batches void UploadSceneObjects(fg::RenderContext* ctx, const GeometryCollector* geometry); + void SetStaticBatchSource(const xr_vector* batches) { m_staticBatchSource = batches; } + bool IsStaticDataCached() const { return m_staticDataCached; } + bool IsTerrainDataCached() const { return m_terrainDataCached; } + void InvalidateStaticCullingData(); void InvalidateShadersAndPipelines(); @@ -202,9 +210,14 @@ class GPUCullingManager { u32 hizHeight, u32 hizMipLevels, const GeometryCollector* geometry, - const Fmatrix& prevViewProj // Previous frame's viewProj for temporal Hi-Z + const Fmatrix& prevViewProj, + bool forceDisableHiz = false, + bool skipUpload = false, + framegraph::VirtualResourceHandle waitFor = {} ); + nvrhi::ITexture* GetDummyHiZTexture() const { return m_dummyHizTexture.Get(); } + // Get number of objects uploaded this frame (static + dynamic) u32 GetObjectCount() const { return m_objectCount; } u32 GetStaticObjectCount() const { return m_staticSet.objectCount; } @@ -280,12 +293,26 @@ class GPUCullingManager { u32 hizWidth, u32 hizHeight, u32 hizMipLevels, - const Fmatrix& prevViewProj, const xr_vector* particleBatches = nullptr ); bool IsDebugEnabled() const; + void UploadParticleBatches(fg::RenderContext* ctx, const xr_vector* batches); + + GPUParticleCullOutput SetupParticleCullingPass( + framegraph::FrameGraph& fg, + framegraph::VirtualResourceHandle hizPyramid, + u32 hizWidth, + u32 hizHeight, + u32 hizMipLevels, + const xr_vector* batches + ); + + u32 GetParticleCount() const { return m_particleCount; } + bool IsParticleCullingEnabled() const { return m_initialized && m_particleCullEnabled; } + nvrhi::IBuffer* GetParticleDrawArgsBuffer() const { return m_particleDrawArgsBuffer.Get(); } + nvrhi::IBuffer* GetStaticCompactDrawArgsBuffer() const { return m_staticSet.compactDrawArgsBuffer.Get(); } nvrhi::IBuffer* GetStaticCompactBatchIndicesBuffer() const { return m_staticSet.compactBatchIndicesBuffer.Get(); } nvrhi::IBuffer* GetStaticCompactMaterialIDBuffer() const { return m_staticSet.compactMaterialIDBuffer.Get(); } @@ -301,6 +328,60 @@ class GPUCullingManager { nvrhi::IBuffer* GetStaticDrawArgsBuffer() const { return m_staticSet.drawArgsBuffer.Get(); } nvrhi::IBuffer* GetDynamicDrawArgsBuffer() const { return m_dynamicSet.drawArgsBuffer.Get(); } + // ─────────────────────────────────────────────────────── + // SHADOW CASTER (view-independent, unculled) BUFFERS + // ─────────────────────────────────────────────────────── + // drawArgsBuffer holds the full template (instanceCount=1 for every batch) and is + // never touched by the camera cull in compaction mode; materialIDBuffer/instanceBuffer + // are full per-batch. Combined with an identity drawID->batch map these let the shadow + // pass draw all casters regardless of camera direction. + nvrhi::IBuffer* GetStaticFullMaterialIDBuffer() const { return m_staticSet.materialIDBuffer.Get(); } + nvrhi::IBuffer* GetStaticShadowIndicesBuffer() const { return m_staticSet.shadowIndicesBuffer.Get(); } + nvrhi::IBuffer* GetStaticShadowCountBuffer() const { return m_staticSet.shadowCountBuffer.Get(); } + nvrhi::IBuffer* GetDynamicFullMaterialIDBuffer() const { return m_dynamicSet.materialIDBuffer.Get(); } + nvrhi::IBuffer* GetDynamicShadowIndicesBuffer() const { return m_dynamicSet.shadowIndicesBuffer.Get(); } + nvrhi::IBuffer* GetDynamicShadowCountBuffer() const { return m_dynamicSet.shadowCountBuffer.Get(); } + + // Transparent set doubles as the foliage shadow caster (trees/bushes are alpha-blended). + nvrhi::IBuffer* GetTransparentDrawArgsBuffer() const { return m_transparentSet.drawArgsBuffer.Get(); } + nvrhi::IBuffer* GetTransparentFullMaterialIDBuffer() const { return m_transparentSet.materialIDBuffer.Get(); } + nvrhi::IBuffer* GetTransparentShadowIndicesBuffer() const { return m_transparentSet.shadowIndicesBuffer.Get(); } + nvrhi::IBuffer* GetTransparentShadowCountBuffer() const { return m_transparentSet.shadowCountBuffer.Get(); } + + // Light-frustum CSM caster compact (CPU filter + upload). Call once per cascade before draw. + void BuildLightFrustumCasters(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const Fmatrix& lightVP, float radiusInflate = 1.25f, float skipNearCameraClearance = 0.f); + + bool DispatchLightFrustumCasters(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const Fmatrix& lightVP, float radiusInflate = 1.25f); + + bool EnsureLightShadowCullPipeline(nvrhi::IDevice* nvDevice); + bool EnsureLocalShadowCullSlots(nvrhi::IDevice* nvDevice, u32 slotCount); + bool DispatchLightFrustumCastersToSlot(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const Fmatrix& lightVP, float radiusInflate, u32 slot); + bool DispatchAllLocalShadowTileCulls(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + const xr_vector& tiles, float radiusInflate); + + static constexpr u32 kLocalShadowCullMaxSlots = 256; + static constexpr u32 kLocalShadowCullSlotCap = 4096; + + nvrhi::IBuffer* GetLocalShadowSlotIndices(u32 slot, u32 setIdx) const; + nvrhi::IBuffer* GetLocalShadowSlotDrawArgs(u32 slot, u32 setIdx) const; + nvrhi::IBuffer* GetLocalShadowSlotMats(u32 slot, u32 setIdx) const; + nvrhi::IBuffer* GetLocalShadowSlotCount(u32 slot, u32 setIdx) const; + nvrhi::BindingSetHandle* MutLocalShadowSlotDrawBinding(u32 slot, u32 setIdx); + nvrhi::IBuffer** MutLocalShadowSlotDrawSrcInstance(u32 slot, u32 setIdx); + u32 GetLocalShadowCullSlotCount() const { return m_localShadowCullSlotCount; } + + void EnsureShadowCasterIdentityBuffers(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice); + + nvrhi::IBuffer* GetStaticShadowCompactDrawArgsBuffer() const { return m_staticSet.shadowCompactDrawArgsBuffer.Get(); } + nvrhi::IBuffer* GetStaticShadowCompactMaterialIDBuffer() const { return m_staticSet.shadowCompactMaterialIDBuffer.Get(); } + nvrhi::IBuffer* GetDynamicShadowCompactDrawArgsBuffer() const { return m_dynamicSet.shadowCompactDrawArgsBuffer.Get(); } + nvrhi::IBuffer* GetDynamicShadowCompactMaterialIDBuffer() const { return m_dynamicSet.shadowCompactMaterialIDBuffer.Get(); } + nvrhi::IBuffer* GetTransparentShadowCompactDrawArgsBuffer() const { return m_transparentSet.shadowCompactDrawArgsBuffer.Get(); } + nvrhi::IBuffer* GetTransparentShadowCompactMaterialIDBuffer() const { return m_transparentSet.shadowCompactMaterialIDBuffer.Get(); } + // ─────────────────────────────────────────────────────── // TERRAIN-SPECIFIC BUFFERS // ─────────────────────────────────────────────────────── @@ -318,6 +399,15 @@ class GPUCullingManager { nvrhi::IBuffer* GetTransparentCompactMaterialIDBuffer() const { return m_transparentSet.compactMaterialIDBuffer.Get(); } nvrhi::IBuffer* GetTransparentCompactCountBuffer() const { return m_transparentSet.compactCountBuffer.Get(); } + // Tessellation set (opaque bump# materials — direct drawIndexed, not MDI) + u32 GetTessObjectCount() const { return m_tessSet.objectCount; } + nvrhi::IBuffer* GetTessInstanceBuffer() const { return m_tessSet.instanceBuffer.Get(); } + nvrhi::IBuffer* GetTessMaterialIDBuffer() const { return m_tessSet.materialIDBuffer.Get(); } + nvrhi::IBuffer* GetTessBatchIndicesBuffer() const { return m_tessBatchIndicesBuffer.Get(); } + nvrhi::IBuffer* GetTessDrawArgsBuffer() const { return m_tessSet.drawArgsBuffer.Get(); } + const xr_vector& GetTessDrawArgsData() const { return m_tessDrawArgsData; } + const xr_vector& GetTessObjectData() const { return m_tessObjectData; } + // ─────────────────────────────────────────────────────── // VARIANT PARTITIONING (multi-PSO rendering) // ─────────────────────────────────────────────────────── @@ -482,6 +572,14 @@ class GPUCullingManager { nvrhi::BufferHandle compactGroupCountsBuffer; // Visible count per group (scratch) nvrhi::BufferHandle compactGroupOffsetsBuffer; // Prefix offsets per group (scratch) nvrhi::BufferHandle instanceBuffer; // Instance data buffer (GPUInstanceData) + // Shadow caster (view-independent) buffers: identity drawID->batch map + count. + // Used to draw ALL batches into the shadow map regardless of camera frustum, + // so casters that leave the camera view still cast shadows. + nvrhi::BufferHandle shadowIndicesBuffer; // Identity or light-culled batch indices + nvrhi::BufferHandle shadowCountBuffer; // Single u32 = caster count (indirect count) + nvrhi::BufferHandle shadowCompactDrawArgsBuffer; // Compacted draw args for light-frustum CSM + nvrhi::BufferHandle shadowCompactMaterialIDBuffer; + u32 shadowBuffersCapacity = 0; // Capacity the identity buffer was built for u32 objectCount = 0; u32 maxObjects = 0; bool drawArgsUploaded = false; @@ -492,6 +590,11 @@ class GPUCullingManager { CullSetBuffers m_staticSet; CullSetBuffers m_dynamicSet; + // Ensure the view-independent shadow-caster identity/count buffers for a set + // exist and are sized/filled for the current objectCount. + void EnsureShadowCasterBuffers(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, + CullSetBuffers& set, const char* name); + // Shared constant buffer fg::BufferHandle m_cullParamsCB; // Constant buffer @@ -501,8 +604,31 @@ class GPUCullingManager { nvrhi::ComputePipelineHandle m_compactCountPipeline; nvrhi::ComputePipelineHandle m_compactScanPipeline; nvrhi::ComputePipelineHandle m_compactScatterPipeline; + nvrhi::ComputePipelineHandle m_lightShadowCullPipeline; nvrhi::BindingLayoutHandle m_cullLayout; nvrhi::BindingLayoutHandle m_clearArgsLayout; + nvrhi::BindingLayoutHandle m_lightShadowCullLayout; + nvrhi::BufferHandle m_lightShadowCullParamsCB; + bool m_lightShadowCullReady = false; + + struct LocalShadowCullSlotSet + { + nvrhi::BufferHandle indices; + nvrhi::BufferHandle drawArgs; + nvrhi::BufferHandle mats; + nvrhi::BufferHandle count; + nvrhi::BindingSetHandle cullBinding; + nvrhi::BindingSetHandle drawBinding; + nvrhi::IBuffer* cullSrcObject = nullptr; + nvrhi::IBuffer* drawSrcInstance = nullptr; + }; + struct LocalShadowCullSlot + { + LocalShadowCullSlotSet sets[3]; + }; + xr_vector m_localShadowCullSlots; + u32 m_localShadowCullSlotCount = 0; + nvrhi::BindingLayoutHandle m_compactCountLayout; nvrhi::BindingLayoutHandle m_compactScanLayout; nvrhi::BindingLayoutHandle m_compactScatterLayout; @@ -527,11 +653,17 @@ class GPUCullingManager { const CullSetBuffers& set, VariantPartitionBuffers& partition); - bool m_staticTerrainDrawArgsUploaded = false; // True after first upload (terrain) + bool m_staticTerrainDrawArgsUploaded = false; + bool m_terrainDataCached = false; + u32 m_lastMaterialCountForStatic = 0; + u64 m_transientUploadFingerprint = 0; + bool m_transientDataCached = false; + bool m_lastTessFlagOn = false; bool m_compactEnabled = false; bool m_staticDataCached = false; + const xr_vector* m_staticBatchSource = nullptr; // ─────────────────────────────────────────────────────── // TERRAIN-SPECIFIC BUFFERS @@ -564,6 +696,10 @@ class GPUCullingManager { // ─────────────────────────────────────────────────────── CullSetBuffers m_transparentSet; + // Opaque tessellation materials (direct drawIndexed; identity batch map) + CullSetBuffers m_tessSet; + nvrhi::BufferHandle m_tessBatchIndicesBuffer; + // ─────────────────────────────────────────────────────── // DEBUG VISUALIZATION RESOURCES // ─────────────────────────────────────────────────────── @@ -581,15 +717,24 @@ class GPUCullingManager { nvrhi::InputLayoutHandle m_debugInputLayout; nvrhi::BufferHandle m_particleBuffer; + nvrhi::BufferHandle m_particleDrawArgsBuffer; + nvrhi::BufferHandle m_particleVisibleCountBuffer; + fg::BufferHandle m_particleCullParamsCB; + nvrhi::ComputePipelineHandle m_particleCullPipeline; + nvrhi::BindingLayoutHandle m_particleCullLayout; + u32 m_particleCount = 0; u32 m_maxParticles = 0; + bool m_particleCullEnabled = false; xr_vector m_particleData; + xr_vector m_particleDrawArgsData; fg::RenderDevice* m_device = nullptr; RTAccelStructManager* m_rtAccelMgr = nullptr; u32 m_objectCount = 0; u32 m_maxObjects = 0; bool m_initialized = false; bool m_computeEnabled = false; + nvrhi::TextureHandle m_dummyHizTexture; // 1x1 R32F when no real Hi-Z yet xr_vector m_staticObjectData; xr_vector m_staticDrawArgsData; @@ -616,6 +761,12 @@ class GPUCullingManager { xr_vector m_transparentMaterialIDData; xr_vector m_transparentInstanceData; + // Tessellation-specific CPU data (bump# / PN+HM materials) + xr_vector m_tessObjectData; + xr_vector m_tessDrawArgsData; + xr_vector m_tessMaterialIDData; + xr_vector m_tessInstanceData; + // ─────────────────────────────────────────────────────── // SKINNED MESH CULLING (GPU-Driven) // ─────────────────────────────────────────────────────── @@ -650,7 +801,7 @@ class GPUCullingManager { // Global bone buffer for GPU-driven skinned rendering // All skeleton bones are uploaded here each frame, indexed by per-instance offset - static constexpr u32 MAX_TOTAL_BONES = 16384; // ~200 skeletons * 78 bones + static constexpr u32 MAX_TOTAL_BONES = 8192; // ~100 skeletons * 78 bones static constexpr u32 BONE_STRIDE = sizeof(Fmatrix); // 64 bytes nvrhi::BufferHandle m_globalBoneBuffer; u32 m_boneUploadFrameId = 0; diff --git a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp index 14845a14115..f4eaba7c7f3 100644 --- a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp +++ b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp @@ -1,9 +1,11 @@ #include "stdafx.h" #include "VariantPSOCache.h" #include "Layers/xrRender/FrameGraph/ShaderLoader.h" +#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" #include "Layers/xrRender/GPUCullingManager.h" #include "Layers/xrRender/FrameGraphPasses/PassCommon.h" -#include "Layers/xrRender/FrameGraph/PassResourceCache.h" +#include "Layers/xrRender/FrameGraphPasses/IBLPrefilterPassSetup.h" namespace xray::render { @@ -46,14 +48,22 @@ static const char* GetDefaultSkinnedVS(u32 vertexFormat) switch (vertexFormat) { case VF_SKINNED_NONHQ: return "bindless_skinned"; - case VF_SKINNED_HQ1W: - case VF_SKINNED_HQ2W: - case VF_SKINNED_HQ3W: - case VF_SKINNED_HQ4W: return "bindless_skinned_hq"; + case VF_SKINNED_HQ1W: return "bindless_skinned_hq"; + case VF_SKINNED_HQ2W: return "bindless_skinned_2w"; + case VF_SKINNED_HQ3W: return "bindless_skinned_3w"; + case VF_SKINNED_HQ4W: return "bindless_skinned_4w"; default: return nullptr; } } +static const char* GetDefaultSkinnedPS(u32 vertexFormat) +{ + // All skinned VS share the same lit PS (IBL + CSM + clustered) + if (vertexFormat >= VF_SKINNED_NONHQ && vertexFormat <= VF_SKINNED_HQ3W) + return "bindless_skinned"; + return nullptr; +} + nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( nvrhi::IDevice* device, nvrhi::IFramebuffer* framebuffer, @@ -77,34 +87,84 @@ nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( const auto& pass = variant.GetPass(passIndex); const char* vsName = pass.vsName.c_str(); - if (vertexFormat >= VF_SKINNED_NONHQ && !xr_strcmp(vsName, "bindless_forward")) + const char* psName = pass.psName.c_str(); + bool remappedToSkinned = false; + + // Model materials are authored as bindless_forward; skinned draws must use + // bindless_skinned* so NPCs/bots get the same IBL/CSM/clustered path as world. + if (vertexFormat >= VF_SKINNED_NONHQ && vertexFormat <= VF_SKINNED_HQ3W) + { + if (!xr_strcmp(vsName, "bindless_forward")) + { + if (const char* skinnedVS = GetDefaultSkinnedVS(vertexFormat)) + { + vsName = skinnedVS; + remappedToSkinned = true; + } + } + if (!xr_strcmp(psName, "bindless_forward")) + { + if (const char* skinnedPS = GetDefaultSkinnedPS(vertexFormat)) + { + psName = skinnedPS; + remappedToSkinned = true; + } + } + } + + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + if (!shaderLoader) + return nullptr; + + auto vsResult = shaderLoader->LoadVertexShader(vsName, "main"); + auto psResult = shaderLoader->LoadPixelShader(psName, "main"); + if (!vsResult.handle || !psResult.handle) + return nullptr; + + // Cache handles for reuse { - const char* skinnedVS = GetDefaultSkinnedVS(vertexFormat); - if (skinnedVS) - vsName = skinnedVS; + string256 vsKey, psKey; + xr_sprintf(vsKey, "%d:%s", static_cast(nvrhi::ShaderType::Vertex), vsName); + xr_sprintf(psKey, "%d:%s", static_cast(nvrhi::ShaderType::Pixel), psName); + m_shaderCache[shared_str(vsKey)] = vsResult.handle; + m_shaderCache[shared_str(psKey)] = psResult.handle; } - auto vs = LoadShader(nvrhi::ShaderType::Vertex, vsName); - auto ps = LoadShader(nvrhi::ShaderType::Pixel, pass.psName.c_str()); - if (!vs || !ps) + // Remapped skinned default shaders share SkinningPass layout (IBL/CSM binds). + // Other custom variants (e.g. water) keep a reflection-matched layout. + nvrhi::BindingLayoutHandle layoutHandle = passBindingLayout; + const bool customShaders = + !remappedToSkinned && + (xr_strcmp(vsName, "bindless_forward") != 0 || + xr_strcmp(psName, "bindless_forward") != 0); + if (customShaders && vsResult.reflection && psResult.reflection) + { + string256 layoutName; + xr_sprintf(layoutName, "Variant_%s_p%u_fmt%u", variant.name.c_str(), passIndex, vertexFormat); + auto customLayout = framegraph::GetPassResourceCache().GetOrCreateBindingLayoutFromReflection( + layoutName, *vsResult.reflection, *psResult.reflection, device); + if (customLayout) + layoutHandle = customLayout; + } + if (!layoutHandle) return nullptr; nvrhi::GraphicsPipelineDesc pipeDesc; - pipeDesc.VS = vs; - pipeDesc.PS = ps; + pipeDesc.VS = vsResult.handle; + pipeDesc.PS = psResult.handle; pipeDesc.inputLayout = inputLayout; pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; if (bindlessLayout) - pipeDesc.bindingLayouts = {passBindingLayout, bindlessLayout}; + pipeDesc.bindingLayouts = { layoutHandle, bindlessLayout }; else - pipeDesc.bindingLayouts = {passBindingLayout}; + pipeDesc.bindingLayouts = { layoutHandle }; pipeDesc.renderState.depthStencilState = pass.depthStencil; pipeDesc.renderState.rasterState = pass.rasterState; pipeDesc.renderState.rasterState.frontCounterClockwise = false; - if (pass.blendEnabled) + if (pass.blendEnabled && !(remappedToSkinned && !variant.transparent)) { pipeDesc.renderState.blendState.targets[0] = pass.blendRT; pipeDesc.renderState.blendState.alphaToCoverageEnable = pass.alphaToCoverage; @@ -117,9 +177,9 @@ nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( return nullptr; } - Msg("* [VariantPSO] Created pipeline: '%s' pass=%d vs=%s ps=%s blend=%s", - variant.name.c_str(), passIndex, vsName, pass.psName.c_str(), - pass.blendEnabled ? "yes" : "no"); + Msg("* [VariantPSO] Created pipeline: '%s' pass=%d vs=%s ps=%s blend=%s skinnedRemap=%d", + variant.name.c_str(), passIndex, vsName, psName, + pass.blendEnabled ? "yes" : "no", remappedToSkinned ? 1 : 0); m_cache[key] = pipeline; return pipeline.Get(); @@ -140,34 +200,19 @@ void DrawVariantPartition( { auto& registry = ShaderVariantRegistry::Instance(); auto& psoCache = VariantPSOCache::Instance(); + auto& smpCache = framegraph::GetPassResourceCache(); const auto& p = cfg.partition; - - nvrhi::BindingSetDesc bindDesc = cfg.baseBindings; - bool foundBatch = false, foundMat = false; - for (auto& item : bindDesc.bindings) { - if (item.type == nvrhi::ResourceType::StructuredBuffer_SRV && item.slot == 15) { - item.resourceHandle = p.batchIndicesBuffer; - foundBatch = true; - } else if (item.type == nvrhi::ResourceType::StructuredBuffer_SRV && item.slot == 16) { - item.resourceHandle = p.materialIDsBuffer; - foundMat = true; - } - } - if (!foundBatch || !foundMat) { - Msg("! [VariantPSO] partition draw: compact index/material slots missing in base bindings"); - return; - } - auto bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bindDesc, cfg.passLayout, nvDevice); - if (!bindingSet) { - Msg("! [VariantPSO] partition binding set creation failed"); + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + if (!shaderLoader) return; - } - - const u32 maxDraws = (cfg.objectCount && cfg.objectCount < p.binCapacity) ? cfg.objectCount : p.binCapacity; for (u32 v = 0; v < p.variantCount; v++) { nvrhi::IGraphicsPipeline* pso; + const char* vsName = "bindless_forward"; + const char* psName = "bindless_forward"; + nvrhi::IBindingLayout* layout = cfg.passLayout; + if (v == 0) { pso = cfg.defaultPipeline; } else { @@ -177,6 +222,99 @@ void DrawVariantPartition( pso = psoCache.GetOrCreatePSO(nvDevice, framebuffer, v, *variant, 0, VF_MDI, cfg.inputLayout, cfg.passLayout, cfg.bindlessLayout); if (!pso) continue; + + if (!variant->passes.empty()) + { + vsName = variant->passes[0].vsName.c_str(); + psName = variant->passes[0].psName.c_str(); + } + // Prefer layout baked into the variant PSO (may differ from default forward). + const auto& pipeDesc = pso->getDesc(); + if (!pipeDesc.bindingLayouts.empty()) + layout = pipeDesc.bindingLayouts[0]; + } + + nvrhi::ITexture* dummy2D = smpCache.GetDummyShadowMap2D(nvDevice); + + nvrhi::ITexture* sky0 = cfg.envSky0 ? cfg.envSky0 : smpCache.GetDummyCubeMap(nvDevice); + nvrhi::ITexture* sky1 = cfg.envSky1 ? cfg.envSky1 : smpCache.GetDummyCubeMap(nvDevice); + + auto* vsReflection = shaderLoader->GetCachedReflection(vsName, ".vs"); + auto* psReflection = shaderLoader->GetCachedReflection(psName, ".ps"); + if (!vsReflection || !psReflection || !layout) + { + Msg("! [VariantPartition] Missing reflection/layout for %s/%s — skip variant %u", + vsName, psName, v); + continue; + } + + framegraph::BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "VariantPartition"); + bsb.ConstantBuffer("static_globals", cfg.staticGlobalsCB); + fg::passes::BindBindlessMaterialTables(bsb); + bsb.BufferSRV("g_InstanceData", cfg.instanceBuffer); + bsb.BufferSRV("g_CompactBatchIndices", p.batchIndicesBuffer); + bsb.BufferSRV("g_CompactMaterialIDs", p.materialIDsBuffer); + + if (cfg.lightDataBuffer) + bsb.BufferSRV("g_LightData", cfg.lightDataBuffer); + if (cfg.shadowDataBuffer) + bsb.BufferSRV("g_ShadowData", cfg.shadowDataBuffer); + if (cfg.clusterGridBuffer) + bsb.BufferSRV("g_ClusterGrid", cfg.clusterGridBuffer); + if (cfg.lightIndexListBuffer) + bsb.BufferSRV("g_LightIndexList", cfg.lightIndexListBuffer); + + if (xr_strcmp(psName, "water") != 0) + { + static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* t = cfg.shadowCascades[i] ? cfg.shadowCascades[i] + : (i == 0 && cfg.shadowMapArray ? cfg.shadowMapArray : dummy2D); + if (t) + bsb.Texture(kNames[i], t); + } + nvrhi::ITexture* contactHist = cfg.contactHistory + ? cfg.contactHistory + : smpCache.GetDummyContactHistory(nvDevice); + if (contactHist) + bsb.Texture("g_ContactHistory", contactHist); + nvrhi::ITexture* localAtlas = cfg.localShadowAtlas + ? cfg.localShadowAtlas + : smpCache.GetDummyShadowMap(nvDevice); + if (localAtlas) + bsb.Texture("g_LocalShadowAtlas", localAtlas); + nvrhi::ITexture* localEsm = cfg.localShadowESM + ? cfg.localShadowESM + : smpCache.GetDummyLocalShadowESM(nvDevice); + if (localEsm) + bsb.Texture("g_LocalShadowESM", localEsm); + static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; + nvrhi::ITexture* dummyHzb = smpCache.GetDummyContactDepth(nvDevice); + for (u32 i = 0; i < 3; ++i) + { + nvrhi::ITexture* hz = cfg.shadowHZB[i] ? cfg.shadowHZB[i] : dummyHzb; + if (hz) + bsb.Texture(kHzb[i], hz); + } + nvrhi::ITexture* shadowMask = cfg.shadowMask + ? cfg.shadowMask + : smpCache.GetDummyContactHistory(nvDevice); + if (shadowMask) + bsb.Texture("g_ShadowMask", shadowMask); + } + if (sky0) + bsb.Texture("s_env0", sky0); + if (sky1) + bsb.Texture("s_env1", sky1); + xray::render::fg::passes::BindIBLResources(bsb, xray::render::fg::passes::GetCurrentIBLBindResources(), nvDevice); + + auto bindingSet = smpCache.GetOrCreateBindingSet(bsb.Build(), layout, nvDevice); + if (!bindingSet) + { + Msg("! [VariantPartition] createBindingSet failed for variant %u (%s/%s)", + v, vsName, psName); + continue; } state.pipeline = pso; @@ -195,7 +333,7 @@ void DrawVariantPartition( cmdList, v * p.binCapacity * sizeof(fg::IndirectDrawArgs), v * sizeof(u32), - maxDraws); + p.binCapacity); } cmdList->setBufferState(p.drawArgsBuffer, nvrhi::ResourceStates::UnorderedAccess); diff --git a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h index 11a515dcb8c..5be3aac4c95 100644 --- a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h +++ b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h @@ -67,9 +67,26 @@ struct VariantPartitionDrawConfig nvrhi::IBindingLayout* passLayout = nullptr; nvrhi::IBindingLayout* bindlessLayout = nullptr; nvrhi::IBindingSet* bindlessTable = nullptr; + nvrhi::ISampler* sampler = nullptr; + nvrhi::IBuffer* staticGlobalsCB = nullptr; + nvrhi::IBuffer* lightingCB = nullptr; + nvrhi::IBuffer* materialBuffer = nullptr; + nvrhi::IBuffer* variantTexBuffer = nullptr; + nvrhi::IBuffer* instanceBuffer = nullptr; nvrhi::IBuffer* megaVertexBuffer = nullptr; - nvrhi::BindingSetDesc baseBindings; - u32 objectCount = 0; + nvrhi::ITexture* shadowMapArray = nullptr; + nvrhi::ITexture* shadowCascades[3] = {}; + nvrhi::ITexture* shadowHZB[3] = {}; + nvrhi::ITexture* localShadowAtlas = nullptr; + nvrhi::ITexture* localShadowESM = nullptr; + nvrhi::ITexture* contactHistory = nullptr; + nvrhi::ITexture* shadowMask = nullptr; + nvrhi::ITexture* envSky0 = nullptr; + nvrhi::ITexture* envSky1 = nullptr; + nvrhi::IBuffer* lightDataBuffer = nullptr; + nvrhi::IBuffer* shadowDataBuffer = nullptr; + nvrhi::IBuffer* clusterGridBuffer = nullptr; + nvrhi::IBuffer* lightIndexListBuffer = nullptr; VariantPartitionConfig partition; bool selectTransparent = false; }; diff --git a/src/Layers/xrRender/Upscaling/DlssFgPassSetup.cpp b/src/Layers/xrRender/Upscaling/DlssFgPassSetup.cpp index fdba00e4ce0..6b7fb15e660 100644 --- a/src/Layers/xrRender/Upscaling/DlssFgPassSetup.cpp +++ b/src/Layers/xrRender/Upscaling/DlssFgPassSetup.cpp @@ -14,7 +14,7 @@ using namespace framegraph; namespace { -void CopyMatrix(float dst[16], const Fmatrix& m) +void DlssFgCopyMatrix(float dst[16], const Fmatrix& m) { dst[0] = m._11; dst[1] = m._12; dst[2] = m._13; dst[3] = m._14; dst[4] = m._21; dst[5] = m._22; dst[6] = m._23; dst[7] = m._24; @@ -112,10 +112,10 @@ void setupDlssFgPass( in.jitterX = data.jitterX; in.jitterY = data.jitterY; in.reset = data.reset; - CopyMatrix(in.viewToClip, data.viewToClip); - CopyMatrix(in.clipToView, data.clipToView); - CopyMatrix(in.clipToPrevClip, data.clipToPrev); - CopyMatrix(in.prevClipToClip, data.prevToClip); + DlssFgCopyMatrix(in.viewToClip, data.viewToClip); + DlssFgCopyMatrix(in.clipToView, data.clipToView); + DlssFgCopyMatrix(in.clipToPrevClip, data.clipToPrev); + DlssFgCopyMatrix(in.prevClipToClip, data.prevToClip); in.cameraPos[0] = Device.vCameraPosition.x; in.cameraPos[1] = Device.vCameraPosition.y; in.cameraPos[2] = Device.vCameraPosition.z; diff --git a/src/Layers/xrRender/Upscaling/UpscalePassSetup.cpp b/src/Layers/xrRender/Upscaling/UpscalePassSetup.cpp index b14240a79a7..0e46402a047 100644 --- a/src/Layers/xrRender/Upscaling/UpscalePassSetup.cpp +++ b/src/Layers/xrRender/Upscaling/UpscalePassSetup.cpp @@ -88,7 +88,7 @@ void EnsureRRGuideResources(fg::RenderDevice* device, UpscalePassState& state, u state.rrGuideH = h; } -void CopyMatrix(float dst[16], const Fmatrix& m) +static void UpscaleCopyMatrix(float dst[16], const Fmatrix& m) { dst[0] = m._11; dst[1] = m._12; dst[2] = m._13; dst[3] = m._14; dst[4] = m._21; dst[5] = m._22; dst[6] = m._23; dst[7] = m._24; @@ -322,8 +322,8 @@ framegraph::VirtualResourceHandle setupUpscaleOrResolvePass( in.enableRR = data.wantRR && (ps_r_upscale == 2 && ps_r_dlss_rr); if (ps_r_upscale == 2 && ps_r_dlss_auto_exposure) in.exposure = nullptr; - CopyMatrix(in.worldToView, Device.mView); - CopyMatrix(in.viewToClip, Device.mProject); + UpscaleCopyMatrix(in.worldToView, Device.mView); + UpscaleCopyMatrix(in.viewToClip, Device.mProject); bool ok = false; if (data.backend && data.backend->IsAvailable()) diff --git a/src/Layers/xrRender/fgRainRender.cpp b/src/Layers/xrRender/fgRainRender.cpp index 9aa587d17b1..33746c9871c 100644 --- a/src/Layers/xrRender/fgRainRender.cpp +++ b/src/Layers/xrRender/fgRainRender.cpp @@ -20,6 +20,7 @@ namespace constexpr float kDropWidth = 0.30f; constexpr int kMaxDesired = 2500; constexpr int kParticleCacheLimit = 400; + constexpr float kParticlesTime = 0.3f; } FGRainRender::FGRainRender() @@ -51,15 +52,36 @@ void FGRainRender::InitResources() m_streakTexture = textureManager->GetNVRHITexture(textureManager->LoadTexture("fx" DELIMITER "fx_rain")); R_ASSERT2(m_streakTexture, "FGRainRender: failed to load fx/fx_rain texture"); + // Splash: prefer detail-model texture, else SSFX rain_splash, else streak + m_splashTexture = m_streakTexture; + if (m_dropModel && m_dropModel->textureName.size()) + { + auto h = textureManager->LoadTexture(m_dropModel->textureName.c_str()); + if (auto* t = textureManager->GetNVRHITexture(h)) + m_splashTexture = t; + } + if (m_splashTexture == m_streakTexture) + { + auto h = textureManager->LoadTexture("fx" DELIMITER "rain_splash"); + if (auto* t = textureManager->GetNVRHITexture(h)) + m_splashTexture = t; + else + { + h = textureManager->LoadTexture("fx" DELIMITER "rain_drop"); + if (auto* t = textureManager->GetNVRHITexture(h)) + m_splashTexture = t; + } + } + auto* shaderLoader = RImplementation.GetShaderLoader(); R_ASSERT(shaderLoader); - auto vsResult = shaderLoader->LoadVertexShader("effects_world_textured", "main"); - R_ASSERT2(vsResult.handle, "FGRainRender: failed to load effects_world_textured.vs"); + auto vsResult = shaderLoader->LoadVertexShader("effects_world_soft", "main"); + R_ASSERT2(vsResult.handle, "FGRainRender: failed to load effects_world_soft.vs"); m_vs = vsResult.handle; - auto psResult = shaderLoader->LoadPixelShader("effects_world_textured", "main"); - R_ASSERT2(psResult.handle, "FGRainRender: failed to load effects_world_textured.ps"); + auto psResult = shaderLoader->LoadPixelShader("effects_world_soft", "main"); + R_ASSERT2(psResult.handle, "FGRainRender: failed to load effects_world_soft.ps"); m_ps = psResult.handle; nvrhi::VertexAttributeDesc vertexAttrs[] = { @@ -71,11 +93,11 @@ void FGRainRender::InitResources() R_ASSERT2(m_inputLayout, "FGRainRender: createInputLayout failed"); nvrhi::BufferDesc cbDesc; - cbDesc.byteSize = sizeof(passes::DynamicTransforms); + cbDesc.byteSize = sizeof(passes::SoftFXConstants); cbDesc.isConstantBuffer = true; cbDesc.isVolatile = true; - cbDesc.maxVersions = 16; - cbDesc.debugName = "FGRainRender_CB"; + cbDesc.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; + cbDesc.debugName = "FGRainRender_SoftCB"; m_constantBuffer = m_device->createBuffer(cbDesc); R_ASSERT2(m_constantBuffer, "FGRainRender: createBuffer(CB) failed"); @@ -90,20 +112,12 @@ void FGRainRender::InitResources() bindingLayoutDesc.bindings = { nvrhi::BindingLayoutItem::VolatileConstantBuffer(0), nvrhi::BindingLayoutItem::Texture_SRV(0), + nvrhi::BindingLayoutItem::Texture_SRV(1), nvrhi::BindingLayoutItem::Sampler(0), }; m_bindingLayout = m_device->createBindingLayout(bindingLayoutDesc); R_ASSERT2(m_bindingLayout, "FGRainRender: createBindingLayout failed"); - nvrhi::BindingSetDesc bindingSetDesc; - bindingSetDesc.bindings = { - nvrhi::BindingSetItem::ConstantBuffer(0, m_constantBuffer), - nvrhi::BindingSetItem::Texture_SRV(0, m_streakTexture), - nvrhi::BindingSetItem::Sampler(0, m_sampler), - }; - m_bindingSet = m_device->createBindingSet(bindingSetDesc, m_bindingLayout); - R_ASSERT2(m_bindingSet, "FGRainRender: createBindingSet failed at init"); - nvrhi::FramebufferInfo fbInfo; fbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); fbInfo.setDepthFormat(nvrhi::Format::D32); @@ -116,6 +130,7 @@ void FGRainRender::InitResources() pipelineDesc.bindingLayouts = { m_bindingLayout }; pipelineDesc.primType = nvrhi::PrimitiveType::TriangleList; pipelineDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + // Soft occlusion is primary; HW depth is a backup. pipelineDesc.renderState.depthStencilState.depthTestEnable = true; pipelineDesc.renderState.depthStencilState.depthWriteEnable = false; pipelineDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; @@ -128,10 +143,28 @@ void FGRainRender::InitResources() m_pipeline = m_device->createGraphicsPipeline(pipelineDesc, fbInfo); R_ASSERT2(m_pipeline, "FGRainRender: createGraphicsPipeline failed"); + + if (renderDevice) + m_gpuSimReady = m_gpuSim.Initialize(renderDevice); + + Msg("* [FGRainRender] Soft-depth rain pipeline ready (GPU sim=%s)", + m_gpuSimReady ? "on" : "off"); } void FGRainRender::Copy(IRainRender& _in) { *this = *static_cast(&_in); } +void FGRainRender::SetRainShadowInputs(nvrhi::ITexture* rainSM, const Fmatrix& sampleVP, bool valid) +{ + m_rainShadow = rainSM; + m_rainSampleVP = sampleVP; + m_rainSMValid = valid; +} + +bool FGRainRender::HasWork() const +{ + return m_rainDensity > EPS_L || !m_splashBatches.empty() || !m_quadBatches.empty(); +} + const Fsphere& FGRainRender::GetDropBounds() const { static Fsphere fallback; @@ -143,18 +176,94 @@ void FGRainRender::Clear() m_vertices.clear(); m_indices.clear(); m_quadBatches.clear(); + m_splashBatches.clear(); + m_desiredParticles = 0; + m_rainDensity = 0.f; } -void FGRainRender::Render(CEffect_Rain& owner) +void FGRainRender::AppendSplashParticles(CEffect_Rain& owner, u32 rainColor) { - Clear(); - if (!m_dropModel) return; + if (!m_dropModel || m_dropModel->number_vertices == 0 || m_dropModel->number_indices == 0) + return; - const float factor = g_pGamePersistent->Environment().CurrentEnv.rain_density; - if (factor < EPS_L) return; + const float dt = Device.fTimeDelta; + CEffect_Rain::Particle* P = owner.particle_active; + if (!P) + return; + + Fmatrix mScale, mXform; + int pcount = 0; + u32 splashIndexStart = static_cast(m_indices.size()); + + auto flushSplashBatch = [&]() { + const u32 splashIndexCount = static_cast(m_indices.size()) - splashIndexStart; + if (splashIndexCount > 0) + { + Batch b{}; + b.indexOffset = splashIndexStart; + b.indexCount = splashIndexCount; + m_splashBatches.push_back(b); + } + splashIndexStart = static_cast(m_indices.size()); + pcount = 0; + }; + + while (P) + { + CEffect_Rain::Particle* next = P->next; + + P->time -= dt; + if (P->time < 0.f) + { + owner.p_free(P); + P = next; + continue; + } + + if (RImplementation.ViewBase.testSphere_dirty(P->bounds.P, P->bounds.R)) + { + const float scale = P->time / kParticlesTime; + mScale.scale(scale, scale, scale); + mXform.mul_43(P->mXForm, mScale); + + const u32 vBase = static_cast(m_vertices.size()); + // Stay within 16-bit index range for absolute indices + if (vBase + m_dropModel->number_vertices > 65000) + { + flushSplashBatch(); + // Drop remaining if still over (extreme); rare with pool=1000 + if (m_vertices.size() + m_dropModel->number_vertices > 65000) + { + P = next; + continue; + } + } - const u32 desired = iFloor(0.5f * (1.f + factor) * float(kMaxDesired)); + const u32 iBase = static_cast(m_indices.size()); + m_vertices.resize(vBase + m_dropModel->number_vertices); + m_indices.resize(iBase + m_dropModel->number_indices); + m_dropModel->transfer( + mXform, + reinterpret_cast(m_vertices.data() + vBase), + rainColor, + m_indices.data() + iBase, + vBase); + + ++pcount; + if (pcount >= kParticleCacheLimit) + flushSplashBatch(); + } + + P = next; + } + + flushSplashBatch(); +} + +void FGRainRender::SimulateRainItems(CEffect_Rain& owner) +{ + const u32 desired = m_desiredParticles; if (owner.items.size() < desired) { owner.items.reserve(desired); @@ -166,30 +275,33 @@ void FGRainRender::Render(CEffect_Rain& owner) } } - const float factor_visual = factor / 2.f + .5f; - const Fvector3 fc = g_pGamePersistent->Environment().CurrentEnv.rain_color; - const u32 col = color_rgba_f(fc.x, fc.y, fc.z, factor_visual); const float radius_wrap_sqr = _sqr(kSourceRadius + .5f); - Fplane src_plane; Fvector norm = {0.f, -1.f, 0.f}; Fvector upper; upper.set(Device.vCameraPosition.x, Device.vCameraPosition.y + kSourceOffset, Device.vCameraPosition.z); src_plane.build(upper, norm); - static Fvector2 UV[2][4] = {{{0,1},{0,0},{1,1},{1,0}},{{1,0},{1,1},{0,0},{0,1}}}; - const Fvector& vEye = Device.vCameraPosition; - const u32 quadIndexStart = static_cast(m_indices.size()); for (u32 I = 0; I < desired; I++) { CEffect_Rain::Item& one = owner.items[I]; - if (one.dwTime_Hit < Device.dwTimeGlobal) owner.Hit(one.Phit); - if (one.dwTime_Life < Device.dwTimeGlobal) owner.Born(one, kSourceRadius); + if (one.dwTime_Hit < Device.dwTimeGlobal) + owner.Hit(one.Phit); + if (one.dwTime_Life < Device.dwTimeGlobal) + owner.Born(one, kSourceRadius); + + one.P.mad(one.D, one.fSpeed * Device.fTimeDelta); + + if ((one.P.y - vEye.y) < kSinkOffset) + { + owner.Hit(one.P); + one.invalidate(); + owner.Born(one, kSourceRadius); + continue; + } - float dt = Device.fTimeDelta; - one.P.mad(one.D, one.fSpeed * dt); Fvector wdir; wdir.set(one.P.x - vEye.x, 0, one.P.z - vEye.z); float wlen = wdir.square_magnitude(); @@ -217,21 +329,44 @@ void FGRainRender::Render(CEffect_Rain& owner) else one.invalidate(); } } + } +} + +void FGRainRender::BuildCPUStreaks(CEffect_Rain& owner) +{ + const u32 desired = m_desiredParticles; + SimulateRainItems(owner); + static Fvector2 UV[2][4] = {{{0,1},{0,0},{1,1},{1,0}},{{1,0},{1,1},{0,0},{0,1}}}; + const Fvector& vEye = Device.vCameraPosition; + const u32 quadIndexStart = static_cast(m_indices.size()); + + for (u32 I = 0; I < desired; I++) + { + CEffect_Rain::Item& one = owner.items[I]; Fvector& pos_head = one.P; Fvector pos_trail; - pos_trail.mad(pos_head, one.D, -kDropLength * factor_visual); + pos_trail.mad(pos_head, one.D, -kDropLength * m_factorVisual); Fvector sC, lineD; - float sR; sC.sub(pos_head, pos_trail); lineD.normalize(sC); sC.mul(.5f); - sR = sC.magnitude(); sC.add(pos_trail); - if (!RImplementation.ViewBase.testSphere_dirty(sC, sR)) + if (!RImplementation.ViewBase.testSphere_dirty(sC, sC.magnitude())) continue; + Fvector sky = pos_head; + sky.y += 80.f; + Fvector down(0.f, -1.f, 0.f); + float coverRange = 80.f; + if (owner.RayPick(sky, down, coverRange, collide::rqtBoth)) + { + const float hitY = sky.y - coverRange; + if (hitY > pos_head.y + 0.35f) + continue; + } + Fvector lineTop, camDir; camDir.sub(sC, vEye); camDir.normalize(); @@ -242,13 +377,13 @@ void FGRainRender::Render(CEffect_Rain& owner) const u32 base = static_cast(m_vertices.size()); P.mad(pos_trail, lineTop, -w); - m_vertices.push_back({P.x, P.y, P.z, col, UV[s][0].x, UV[s][0].y}); + m_vertices.push_back({P.x, P.y, P.z, m_rainColor, UV[s][0].x, UV[s][0].y}); P.mad(pos_trail, lineTop, w); - m_vertices.push_back({P.x, P.y, P.z, col, UV[s][1].x, UV[s][1].y}); + m_vertices.push_back({P.x, P.y, P.z, m_rainColor, UV[s][1].x, UV[s][1].y}); P.mad(pos_head, lineTop, -w); - m_vertices.push_back({P.x, P.y, P.z, col, UV[s][2].x, UV[s][2].y}); + m_vertices.push_back({P.x, P.y, P.z, m_rainColor, UV[s][2].x, UV[s][2].y}); P.mad(pos_head, lineTop, w); - m_vertices.push_back({P.x, P.y, P.z, col, UV[s][3].x, UV[s][3].y}); + m_vertices.push_back({P.x, P.y, P.z, m_rainColor, UV[s][3].x, UV[s][3].y}); m_indices.push_back(static_cast(base + 0)); m_indices.push_back(static_cast(base + 1)); @@ -268,6 +403,32 @@ void FGRainRender::Render(CEffect_Rain& owner) } } +void FGRainRender::Render(CEffect_Rain& owner) +{ + Clear(); + + const float factor = g_pGamePersistent->Environment().CurrentEnv.rain_density; + m_rainDensity = factor; + if (factor < EPS_L) + return; + + m_desiredParticles = iFloor(0.5f * (1.f + factor) * float(kMaxDesired)); + m_factorVisual = factor / 2.f + .5f; + const Fvector3 fc = g_pGamePersistent->Environment().CurrentEnv.rain_color; + m_rainColor = color_rgba_f(fc.x, fc.y, fc.z, m_factorVisual); + + // GPU streaks + rain-SM cover; CPU keeps item sim for splash particles. + if (m_gpuSimReady) + SimulateRainItems(owner); + else if (m_dropModel) + BuildCPUStreaks(owner); + else + SimulateRainItems(owner); + + if (m_dropModel) + AppendSplashParticles(owner, m_rainColor); +} + void FGRainRender::EnsureGeometryCapacity(size_t vertexCount, size_t indexCount) { if (vertexCount > m_vertexCapacity || !m_vertexBuffer) @@ -290,32 +451,111 @@ void FGRainRender::EnsureGeometryCapacity(size_t vertexCount, size_t indexCount) } } -void FGRainRender::Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer) +void FGRainRender::DrawGPUStreaks(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, + nvrhi::ITexture* sceneWorldPos) { - EnsureGeometryCapacity(m_vertices.size(), m_indices.size()); + if (!m_streakTexture || !sceneWorldPos || !m_pipeline || !m_bindingLayout) + return; - if (!m_vertices.empty()) - cmdList->writeBuffer(m_vertexBuffer, m_vertices.data(), m_vertices.size() * sizeof(Vertex)); - if (!m_indices.empty()) - cmdList->writeBuffer(m_indexBuffer, m_indices.data(), m_indices.size() * sizeof(u16)); + nvrhi::IBuffer* vb = m_gpuSim.GetVertexBuffer(); + nvrhi::IBuffer* drawArgs = m_gpuSim.GetDrawArgsBuffer(); + if (!vb || !drawArgs) + return; - passes::DynamicTransforms cb{}; - passes::FillDynamicTransforms(cb); - cmdList->writeBuffer(m_constantBuffer, &cb, sizeof(cb)); + nvrhi::BindingSetDesc bindingSetDesc; + bindingSetDesc.bindings = { + nvrhi::BindingSetItem::ConstantBuffer(0, m_constantBuffer), + nvrhi::BindingSetItem::Texture_SRV(0, m_streakTexture), + nvrhi::BindingSetItem::Texture_SRV(1, sceneWorldPos), + nvrhi::BindingSetItem::Sampler(0, m_sampler), + }; + nvrhi::BindingSetHandle bindingSet = m_device->createBindingSet(bindingSetDesc, m_bindingLayout); + if (!bindingSet) + return; + + cmdList->setBufferState(vb, nvrhi::ResourceStates::VertexBuffer); + cmdList->setBufferState(drawArgs, nvrhi::ResourceStates::IndirectArgument); - cmdList->setBufferState(m_vertexBuffer, nvrhi::ResourceStates::VertexBuffer); - cmdList->setBufferState(m_indexBuffer, nvrhi::ResourceStates::IndexBuffer); + nvrhi::VertexBufferBinding vertexBinding; + vertexBinding.buffer = vb; + vertexBinding.slot = 0; + vertexBinding.offset = 0; + + const auto& fbInfo = framebuffer->getFramebufferInfo(); + + nvrhi::GraphicsState state; + state.pipeline = m_pipeline; + state.framebuffer = framebuffer; + state.bindings = { bindingSet }; + state.vertexBuffers = { vertexBinding }; + state.indexBuffer = {}; + state.indirectParams = drawArgs; + state.viewport = nvrhi::ViewportState().addViewportAndScissorRect( + nvrhi::Viewport(static_cast(fbInfo.width), static_cast(fbInfo.height))); + cmdList->setGraphicsState(state); + cmdList->drawIndirect(0); +} + +void FGRainRender::Draw(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, + nvrhi::ITexture* sceneWorldPos, + const passes::RainHeightmapInfo& heightmap) +{ + passes::SoftFXConstants cb{}; + passes::FillSoftFXConstants(cb); + cmdList->writeBuffer(m_constantBuffer, &cb, sizeof(cb)); cmdList->setBufferState(m_constantBuffer, nvrhi::ResourceStates::ConstantBuffer); - DrawBatches(cmdList, framebuffer, m_quadBatches, m_streakTexture); + (void)heightmap; + + if (m_gpuSimReady && m_desiredParticles > 0) + { + m_gpuSim.Dispatch(cmdList, m_rainShadow, m_rainSampleVP, m_rainSMValid, + m_desiredParticles, m_rainDensity, m_factorVisual, m_rainColor); + DrawGPUStreaks(cmdList, framebuffer, sceneWorldPos); + if (!m_splashBatches.empty()) + { + EnsureGeometryCapacity(m_vertices.size(), m_indices.size()); + if (!m_vertices.empty()) + cmdList->writeBuffer(m_vertexBuffer, m_vertices.data(), m_vertices.size() * sizeof(Vertex)); + if (!m_indices.empty()) + cmdList->writeBuffer(m_indexBuffer, m_indices.data(), m_indices.size() * sizeof(u16)); + cmdList->setBufferState(m_vertexBuffer, nvrhi::ResourceStates::VertexBuffer); + cmdList->setBufferState(m_indexBuffer, nvrhi::ResourceStates::IndexBuffer); + DrawBatches(cmdList, framebuffer, m_splashBatches, m_splashTexture, sceneWorldPos); + } + Clear(); + return; + } + + EnsureGeometryCapacity(m_vertices.size(), m_indices.size()); + if (!m_vertices.empty()) + cmdList->writeBuffer(m_vertexBuffer, m_vertices.data(), m_vertices.size() * sizeof(Vertex)); + if (!m_indices.empty()) + cmdList->writeBuffer(m_indexBuffer, m_indices.data(), m_indices.size() * sizeof(u16)); + cmdList->setBufferState(m_vertexBuffer, nvrhi::ResourceStates::VertexBuffer); + cmdList->setBufferState(m_indexBuffer, nvrhi::ResourceStates::IndexBuffer); + DrawBatches(cmdList, framebuffer, m_quadBatches, m_streakTexture, sceneWorldPos); + DrawBatches(cmdList, framebuffer, m_splashBatches, m_splashTexture, sceneWorldPos); Clear(); } void FGRainRender::DrawBatches(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer* framebuffer, - const xr_vector& batches, nvrhi::ITexture* texture) + const xr_vector& batches, nvrhi::ITexture* colorTex, + nvrhi::ITexture* sceneWorldPos) { - if (!texture || batches.empty() || !m_bindingSet) + if (!colorTex || !sceneWorldPos || batches.empty() || !m_pipeline || !m_bindingLayout) + return; + + nvrhi::BindingSetDesc bindingSetDesc; + bindingSetDesc.bindings = { + nvrhi::BindingSetItem::ConstantBuffer(0, m_constantBuffer), + nvrhi::BindingSetItem::Texture_SRV(0, colorTex), + nvrhi::BindingSetItem::Texture_SRV(1, sceneWorldPos), + nvrhi::BindingSetItem::Sampler(0, m_sampler), + }; + nvrhi::BindingSetHandle bindingSet = m_device->createBindingSet(bindingSetDesc, m_bindingLayout); + if (!bindingSet) return; nvrhi::VertexBufferBinding vertexBinding; @@ -328,7 +568,7 @@ void FGRainRender::DrawBatches(nvrhi::ICommandList* cmdList, nvrhi::IFramebuffer nvrhi::GraphicsState state; state.pipeline = m_pipeline; state.framebuffer = framebuffer; - state.bindings = { m_bindingSet }; + state.bindings = { bindingSet }; state.vertexBuffers = { vertexBinding }; state.indexBuffer.buffer = m_indexBuffer; state.indexBuffer.format = nvrhi::Format::R16_UINT; diff --git a/src/Layers/xrRender/r_FrameGraphRenderer.h b/src/Layers/xrRender/r_FrameGraphRenderer.h index 99de1eff6c5..1fb38e0b54a 100644 --- a/src/Layers/xrRender/r_FrameGraphRenderer.h +++ b/src/Layers/xrRender/r_FrameGraphRenderer.h @@ -490,6 +490,7 @@ class FrameGraphRenderer: public xray::render::fg::FGRenderBase { // Eliminates double vertex processing cost (~1.5-2ms savings) nvrhi::TextureHandle m_prevFrameDepth; nvrhi::TextureHandle m_normals[2]; + nvrhi::TextureHandle m_worldPos[2]; u32 m_pingPongIndex = 0; Fmatrix m_prevViewProj; // Previous frame's view-projection diff --git a/src/Layers/xrRender/xrRender_console.cpp b/src/Layers/xrRender/xrRender_console.cpp index d1f387dae04..417f266bef2 100644 --- a/src/Layers/xrRender/xrRender_console.cpp +++ b/src/Layers/xrRender/xrRender_console.cpp @@ -113,6 +113,7 @@ int ps_r_local_shadow_tiles = 96; int ps_r_local_shadow_update_div = 1; int ps_r_local_shadow_redraw_budget = 16; int ps_r_local_shadow_skinned_max = 24; +float SunshaftsIntensity = 0.f; float ps_r_local_shadow_near = 10.f; float ps_r_local_shadow_mid = 30.f; int ps_r_local_shadow_far_period = 5; diff --git a/src/Layers/xrRender/xrRender_console.h b/src/Layers/xrRender/xrRender_console.h index fd16753d1bf..253312bcf29 100644 --- a/src/Layers/xrRender/xrRender_console.h +++ b/src/Layers/xrRender/xrRender_console.h @@ -288,6 +288,7 @@ extern ECORE_API float ps_r_smoke_max_width; extern ECORE_API float ps_r_smoke_gravity; extern ECORE_API float ps_r_smoke_buoyancy; extern ECORE_API float ps_r_smoke_turbulence; +extern float SunshaftsIntensity; extern void xrRender_initconsole(); } // namespace xray::render::fg From 41bb1d1c78c39835de90385ceab0f6fb2e9cd51c Mon Sep 17 00:00:00 2001 From: FoxConED Date: Wed, 29 Jul 2026 10:38:36 +0300 Subject: [PATCH 252/261] xrGame: pass session_name.c_str() to Connect --- src/xrGame/xrServer_Connect.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrGame/xrServer_Connect.cpp b/src/xrGame/xrServer_Connect.cpp index 6a96ec31ad4..51e6493637d 100644 --- a/src/xrGame/xrServer_Connect.cpp +++ b/src/xrGame/xrServer_Connect.cpp @@ -83,7 +83,7 @@ xrServer::EConnect xrServer::Connect(shared_str& session_name, GameDescriptionDa game->Create(session_name); - return NET_SERVER_CLASS::Connect(*session_name, game_descr); + return NET_SERVER_CLASS::Connect(session_name.c_str(), game_descr); } IClient* xrServer::new_client(SClientConnectData* cl_data) From 769f52f276f9c48ed78444c24a3da37265e992bf Mon Sep 17 00:00:00 2001 From: FoxConED Date: Thu, 30 Jul 2026 00:01:28 +0300 Subject: [PATCH 253/261] xrRender: stabilize ReSTIR temporal on camera motion --- res/gamedata/shaders/r5/restir_gi_spatial.cs | 26 +++++++++----- res/gamedata/shaders/r5/restir_gi_temporal.cs | 34 ++++++++++++++----- .../shaders/r5/restir_gi_temporal_filter.cs | 8 ++--- .../shaders/r5/restir_motion_vectors.cs | 4 +-- 4 files changed, 49 insertions(+), 23 deletions(-) diff --git a/res/gamedata/shaders/r5/restir_gi_spatial.cs b/res/gamedata/shaders/r5/restir_gi_spatial.cs index 62687d400e9..f56c68a6aec 100644 --- a/res/gamedata/shaders/r5/restir_gi_spatial.cs +++ b/res/gamedata/shaders/r5/restir_gi_spatial.cs @@ -20,7 +20,9 @@ uint g_SkinnedBatchStart; uint g_GrassBatchStart; uint g_DetailAtlasIndex; - uint3 g_Pad; + uint g_ParticleBatchStart; + float g_LodDist; + float g_CameraMotion; }; RaytracingAccelerationStructure g_SceneTLAS : register(t1); @@ -29,8 +31,10 @@ ByteAddressBuffer g_MegaIB : register(t18); ByteAddressBuffer g_GrassVB : register(t12); ByteAddressBuffer g_GrassIB : register(t13); +ByteAddressBuffer g_ParticleVB : register(t4); +ByteAddressBuffer g_ParticleIB : register(t8); -Texture2D t_Depth : register(t4); +Texture2D t_Depth : register(t0); Texture2D t_Normal : register(t5); Texture2D t_BaseColor : register(t6); Texture2D t_WorldPos : register(t7); @@ -50,10 +54,10 @@ bool VisibilityOK(float3 worldPos, float3 N, float3 samplePos) { float3 biasedPos = worldPos + N * 0.01; return TraceVisibilityClear( - g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, + g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, g_ParticleVB, g_ParticleIB, biasedPos, samplePos, g_IdentityStaticCount, g_TerrainBatchCount, g_SkinnedBatchStart, g_GrassBatchStart, - g_DetailAtlasIndex); + g_ParticleBatchStart, g_DetailAtlasIndex); } float TargetLuminance(GIReservoir r, float3 worldPos, float3 N, float3 albedo, float metallic) @@ -96,7 +100,10 @@ GIReservoir SpatialReuse( output.age = center.age; } + float farLod = saturate(linearDepth / max(g_LodDist, 1.0)); uint samples = min(g_SpatialSamples, 16u); + if (farLod > 0.7) + samples = min(samples, 2u); for (uint i = 0; i < samples; ++i) { float ang = rand_float(rng) * 6.2831853; float rad = sqrt(rand_float(rng)) * g_SpatialRadius * radiusScale; @@ -107,7 +114,7 @@ GIReservoir SpatialReuse( continue; float nDepth = t_Depth.Load(int3(nPixel, 0)); - if (nDepth >= 1.0) + if (nDepth <= 0.0) continue; float4 nWorldPosData = t_WorldPos.Load(int3(nPixel, 0)); @@ -134,6 +141,8 @@ GIReservoir SpatialReuse( float neighLum = TargetLuminance(neighbor, worldPos, N, albedo, metallic); if (neighLum <= 0) continue; + if (centerLum > 1e-5 && neighLum > centerLum * 8.0) + continue; float conf = AgeConfidence(neighbor.age, 32u); uint clampedM = TemporalMClamp(neighbor.M, neighbor.age, g_MMax); @@ -163,7 +172,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) return; float depth = t_Depth.Load(int3(pixel, 0)); - if (depth >= 1.0) { + if (depth <= 0.0) { u_ReservoirA[pixel] = 0; u_ReservoirB[pixel] = 0; u_ReservoirC[pixel] = 0; @@ -183,6 +192,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float metallic = UnpackMetallicFromBaseA(baseColorData.a); float linearDepth = length(worldPos - g_CameraPos.xyz); uint rng = pcg_hash(pixel.x + pixel.y * 6287u + g_FrameIndex * 33461u); + float radiusScale = lerp(1.0, 0.7, saturate(g_CameraMotion)); GIReservoir center = UnpackReservoir( t_ReservoirA.Load(int3(pixel, 0)), @@ -193,13 +203,13 @@ void main(uint3 dispatchID : SV_DispatchThreadID) t_SpecB.Load(int3(pixel, 0))); GIReservoir output = SpatialReuse(center, t_ReservoirA, t_ReservoirB, t_ReservoirC, true, - pixel, worldPos, centerMark, N, albedo, metallic, linearDepth, 1.0, rng); + pixel, worldPos, centerMark, N, albedo, metallic, linearDepth, radiusScale, rng); float3 specAlbedo = lerp(float3(1, 1, 1), albedo, metallic); float specRadius = roughness < 0.2 ? 0.35 : (roughness < 0.45 ? 0.6 : 0.15); GIReservoir outputSpec = centerSpec; if (roughness < 0.55) { outputSpec = SpatialReuse(centerSpec, t_SpecA, t_SpecB, t_ReservoirC, false, - pixel, worldPos, centerMark, N, specAlbedo, metallic, linearDepth, specRadius, rng); + pixel, worldPos, centerMark, N, specAlbedo, metallic, linearDepth, specRadius * radiusScale, rng); } float4 outA, outB; diff --git a/res/gamedata/shaders/r5/restir_gi_temporal.cs b/res/gamedata/shaders/r5/restir_gi_temporal.cs index b96edb65551..3eb1012aada 100644 --- a/res/gamedata/shaders/r5/restir_gi_temporal.cs +++ b/res/gamedata/shaders/r5/restir_gi_temporal.cs @@ -17,7 +17,8 @@ uint g_SkinnedBatchStart; uint g_GrassBatchStart; uint g_DetailAtlasIndex; - uint2 g_Pad; + uint g_ParticleBatchStart; + float g_CameraMotion; }; RaytracingAccelerationStructure g_SceneTLAS : register(t1); @@ -26,9 +27,11 @@ ByteAddressBuffer g_MegaIB : register(t18); ByteAddressBuffer g_GrassVB : register(t12); ByteAddressBuffer g_GrassIB : register(t13); +ByteAddressBuffer g_ParticleVB : register(t4); +ByteAddressBuffer g_ParticleIB : register(t8); Texture2D t_PrevReservoirA : register(t0); -Texture2D t_PrevReservoirB : register(t4); +Texture2D t_PrevReservoirB : register(t22); Texture2D t_MotionVectors : register(t5); Texture2D t_Depth : register(t6); Texture2D t_PrevNormal : register(t7); @@ -50,10 +53,10 @@ bool VisibilityOK(float3 worldPos, float3 N, float3 samplePos) { float3 biasedPos = worldPos + N * 0.01; return TraceVisibilityClear( - g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, + g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, g_ParticleVB, g_ParticleIB, biasedPos, samplePos, g_IdentityStaticCount, g_TerrainBatchCount, g_SkinnedBatchStart, g_GrassBatchStart, - g_DetailAtlasIndex); + g_ParticleBatchStart, g_DetailAtlasIndex); } void TemporalMerge( @@ -130,7 +133,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) return; float depth = t_Depth.Load(int3(pixel, 0)); - if (depth >= 1.0) { + if (depth <= 0.0) { u_ReservoirA[pixel] = 0; u_ReservoirB[pixel] = 0; u_ReservoirC[pixel] = 0; @@ -171,8 +174,10 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float3 prevN = normalize(t_PrevNormal.Load(int3(prevPixel, 0)).xyz); float viewDist = length(worldPos - g_CameraPos.xyz); float posDist = length(worldPos - prevWorldPos); + float posTol = lerp(0.08, 0.05, saturate(g_CameraMotion)) * viewDist; + float nTol = lerp(0.88, 0.94, saturate(g_CameraMotion)); bool valid = SameHudSurfClass(centerMark, prevWorldPosData.w) && - posDist < 0.1 * viewDist && dot(N, prevN) > 0.906; + posDist < posTol && dot(N, prevN) > nTol; if (valid) { prevRes = UnpackReservoir( t_PrevReservoirA.Load(int3(prevPixel, 0)), @@ -181,13 +186,24 @@ void main(uint3 dispatchID : SV_DispatchThreadID) prevSpec = UnpackReservoir( t_PrevSpecA.Load(int3(prevPixel, 0)), t_PrevSpecB.Load(int3(prevPixel, 0))); + if (g_CameraMotion > 0.5) + { + prevRes.M = max(1u, (prevRes.M * 3u) / 4u); + prevSpec.M = max(1u, (prevSpec.M * 3u) / 4u); + } } } - TemporalMerge(output, currRes, prevRes, worldPos, prevWorldPos, N, albedo, metallic, true, rng); + bool requireVis = true; + if (g_CameraMotion < 0.2) + { + uint h = pcg_hash(pixel.x + pixel.y * 1973u + g_FrameIndex); + requireVis = ((h & 3u) != 0u); + } + TemporalMerge(output, currRes, prevRes, worldPos, prevWorldPos, N, albedo, metallic, requireVis, rng); float3 specAlbedo = lerp(float3(1, 1, 1), albedo, metallic); - TemporalMerge(outputSpec, currSpec, prevSpec, worldPos, prevWorldPos, N, specAlbedo, metallic, true, rng); - if (roughness > 0.55) + TemporalMerge(outputSpec, currSpec, prevSpec, worldPos, prevWorldPos, N, specAlbedo, metallic, requireVis, rng); + if (roughness > 0.55 || g_CameraMotion > 0.65) outputSpec = currSpec; float4 outA, outB; diff --git a/res/gamedata/shaders/r5/restir_gi_temporal_filter.cs b/res/gamedata/shaders/r5/restir_gi_temporal_filter.cs index e84352b9592..cafbc15b3ca 100644 --- a/res/gamedata/shaders/r5/restir_gi_temporal_filter.cs +++ b/res/gamedata/shaders/r5/restir_gi_temporal_filter.cs @@ -52,14 +52,14 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float3 currS = currS4.rgb; float specHitDist = currS4.a; - if (depth >= 1.0 || g_Enabled == 0) { + if (depth <= 0.0 || g_Enabled == 0) { u_OutDiffuse[pixel] = float4(currD, 1.0); u_OutSpecular[pixel] = float4(currS, specHitDist); return; } float2 mv = t_MotionVectors.Load(int3(pixel, 0)); - float2 histUV = (float2(pixel) + 0.5) * g_InvScreenSize - mv; + float2 histUV = (float2(pixel) + 0.5) * g_InvScreenSize + mv; if (any(histUV < 0.0) || any(histUV > 1.0)) { u_OutDiffuse[pixel] = float4(currD, 1.0); u_OutSpecular[pixel] = float4(currS, specHitDist); @@ -115,8 +115,8 @@ void main(uint3 dispatchID : SV_DispatchThreadID) } } - histD = ClipAABB(histD, minD * 0.15, maxD * 4.5); - histS = ClipAABB(histS, minS * 0.1, maxS * 5.0); + histD = ClipAABB(histD, minD * 0.55, maxD * 2.0); + histS = ClipAABB(histS, minS * 0.4, maxS * 2.4); float alpha = saturate(g_Alpha) * trust; float3 outD = lerp(currD, histD, alpha); diff --git a/res/gamedata/shaders/r5/restir_motion_vectors.cs b/res/gamedata/shaders/r5/restir_motion_vectors.cs index 2b8bbe9d860..dc19221cd1c 100644 --- a/res/gamedata/shaders/r5/restir_motion_vectors.cs +++ b/res/gamedata/shaders/r5/restir_motion_vectors.cs @@ -27,7 +27,7 @@ float2 ProjectToUv(float4x4 viewProj, float3 worldPos) float3 ReconstructFarWorld(float2 uv) { float2 ndc = float2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0); - float4 farH = mul(g_InvViewProj, float4(ndc, 1.0, 1.0)); + float4 farH = mul(g_InvViewProj, float4(ndc, 0.0, 1.0)); return farH.xyz / max(farH.w, 1e-6); } @@ -40,7 +40,7 @@ void main(uint3 dtid : SV_DispatchThreadID) float2 uv = (float2(pixel) + 0.5) * g_InvScreenSize; float depth = t_Depth.Load(int3(pixel, 0)); - if (depth >= 0.9995) + if (IsSkyDepth(depth)) { if (g_HasPrevCamera == 0) { From 3aa9d0669d5bd331f758f1b6a849306ebf2cafde Mon Sep 17 00:00:00 2001 From: FoxConED Date: Thu, 30 Jul 2026 00:01:28 +0300 Subject: [PATCH 254/261] xrRender: light CHAR mutants in ReSTIR like world geometry --- res/gamedata/shaders/r5/bindless_skinned.ps | Bin 2462 -> 3477 bytes .../shaders/r5/bindless_skinned_hud.ps | Bin 4166 -> 4298 bytes .../shaders/r5/bindless_skinned_mdi.ps | Bin 2215 -> 3578 bytes res/gamedata/shaders/r5/restir_di_eval.h | 22 +- res/gamedata/shaders/r5/restir_di_shade.cs | 25 ++- res/gamedata/shaders/r5/restir_di_spatial.cs | 12 +- res/gamedata/shaders/r5/restir_di_temporal.cs | 2 +- res/gamedata/shaders/r5/restir_gi_initial.cs | 210 +++++++++++++----- res/gamedata/shaders/r5/rt_visibility.h | 55 +++-- .../shaders/r5/shared/surface_marks.h | 2 +- .../ShaderVariant/VariantPSOCache.cpp | 55 +++-- .../xrRender/ShaderVariant/VariantPSOCache.h | 8 +- 12 files changed, 279 insertions(+), 112 deletions(-) diff --git a/res/gamedata/shaders/r5/bindless_skinned.ps b/res/gamedata/shaders/r5/bindless_skinned.ps index b3b38fda58e4840cc194c94d6d1e3ba7728bb937..10bf7db449c6a7b839b23e431ff2bfcfab5bc3c5 100644 GIT binary patch literal 3477 zcma)9X>a2;5dH37!R`Wu94KlMJN?k0C}dk38|w>_;w62qXU#HFby1#`aTRAN`V9pOJXfQ;J*0zP zvVuim0Z8cm*`Lk)!Ic5HK^5JtvaE;~-hNhKa?;O~d@hPG! z5os2u6)p0N5fMcVLzEDhiU}JH441cetsi$SLyXx&c%sHCV_5-C1__d!Ab>cvbiPoW=5^IpY1@Ocx`KIF$Fq4H_wQ(f z16gg8Mhz*M9uG)GxKxN^^L9|FG|GU`;o4%eH2HVX6d)olBME53va$rjm7_}d zP2xj0z|)yOamUBRdv%g4RhIDBNHZ13*-(Z@$v?7Ec2FxlPD&50Mtv10$8x~Fnv$AX zd=E!XLBe{|wt?2+2#grWV3lI;1~>3RcXSe*>VFZfNX(9NFDB0f*rY@~Tp-JI;6QHB z1-fyr4CifOE1~s-os?xmI>hM@Q;pJ{=Yy6I}y3hfeIodlmL0g;+2hxtln%> zcp2GANtsC79k0CxAZ>Zxl>NOx6TP*JX6osfK#Ms(M9 zM|G3j#WmZ_pCcJQ?CaiLX4H4H0c=5(C?W?5q`G zE1YBzWth)evaQ?D?QUTQ@u{RyG(H}?e2?F<+rhaAgj=bNfbJMjz z89Bupm(3+|m3j&<>lXIsNI8(rL)rHi9tmm22Pmy3_akndVq-n)>|VdwD0uL{h-*IX z4_#leC@2(Ts!|7+q*ik+pZDR+EoS%L7%#5jQr;L2FZb@i``CW-!TQbzcK)T{{Qmv1 t8w4Zx-tacX2J$wGAao(c?>Pye``ipZLg2Z3?}2J>lkvSi+-ABG@*jD)FUZ8#YCUei2;~rCURR=o07c0aWDE{xQW2(N z@Q5fBl=#qWK%Q|0yGQM=!O_>q3Xx{a?V#kLaIk^W5j0R-JV6q?Y;o3Q)M$=ofF^Ts zk~bD{QaFqS6i!qq5QyWLg)Hmy)sxDNldpT<4p%;|=yR!ul|*FWZ`qP3_-J9cA9@}N zT}u_WEilh8cTvHd;1WgjFtxKCWkp7TkKj_z3njwTh8F2^jtCtMdMkZJcQW+iS)Trp$zjZltVEBQAc#-jpa~#WAZ&n46^j2w z^{F|=KX9!%OaIQi*fc9OP!a0SSLtQ4`e^n3-?mOytNMIqL+`C!cmOr1*p+HL$@4i; zAb2MQFkHei)qTWY1{^zH`ge_ z*{Dp!NkU*gr6Rsh9V0QP+hWmQ_7?+gMQ5}J3q)*h!R;rVcl!HUTX(Ij{$SNt3i@FE n_ND3V6OT60pVpt}k{j8?=$A%hlJ~~0J*yR?uqyZT=Ek)@CEpxR diff --git a/res/gamedata/shaders/r5/bindless_skinned_hud.ps b/res/gamedata/shaders/r5/bindless_skinned_hud.ps index b6fd47a733c6c13ec78b67d7d4605c64969e6d9e..4f5ce05353cfccb8d30a2845a04fc418537cb5e7 100644 GIT binary patch delta 137 zcmX@6a7uB5HKVL@W?phmX-cYsQgKFNQEG~QacNOnVsdJHZemgPW;4bPMgu=jO5dZGKf@^_64iqhk-F{e546?1pM*V`MxEqFnpe5SYLOq~I;=`;eU1g(E$Rwg0VYVe0?RFE{5*(En)EGFFYAcTRm!wgQYjbt3Ed($s*Ro z6upb`G|e+-_s*DGCt1vBRpC>@>LJN88sjuhPRnp$s3eA~ z!c|qB7B=OEL=>l_d=Q9ERhLavqh)|6^W}Zm`QAbBY*cBD`o|h*APm31O3wq4L9-1#wB9CRt4P2L9u1=f`cwAa8LWzG5%R zSzf{-pe`ikmewXQ{e$1F+~lQ?)}*4tmnuwd`X>3U5@3g$1_6~efx}5=>G+}~E1J5i z!h8VbO%3y~jz|9`PoLx}=RlpLYo@3ik(zK`(I>RUdb0Za0RKlR7HQQ(VFpMU_T(G2 zgh`HyV)rPxgHy;h;>XzemgeaR#d0GPdxM%s04~I7TB^fzzp}t!Te+&UR*!X>+hDwn z2JkDk1FeU{&_{8y*)$amNLnxoJII%joR#XEN4+N5AU@@67}-|nS2S}&{CVo$VRyP5 zyEycMuq%{*7k_zRB9;`^(IyT-mz;WsuwqAix>0`qFzo6f!qzm-!Jy-{<#uh_-$QeN z2)~RVpe@U*3Itb<%HKB)AG-mbFZ`K1eQEfhNejiw8eUs!=Hl!d%y2LHBQs?Wxia8J z>7muKul(d#4me~}+Axdn;m9cn*kIl@(Aph=5d-P0V(hGQBQJC(ubflGGNv_2*vr_9 z$rBE?AyF?E$TAf;5F2!XZk#K_bZ*-(r*no=FRjCa``NN91HjqgkM>#qzd%Pj5xLJ1 z3&JIog7J(ck&Vi{+3qsvUN$1RjdTMV%SAq|a`s4PsvIl_zIND_TigFb75FLgJdmw$ z1Xb6k5pNtNjUd8ii-Z{vsKU|KXT?;@EN`ZyDA+Tu3g`gPSfL(Uuoj#U2jcbNKAcX; z0QAwIE@`%{cVI$$0%STOkcPaa==`*6D-u3(eHF`>^Cx9$B>RXtthjGXfx3F9FF-Mmt!m8H2m zWXIe_t2WXGWwvHWzGxNdi-8^v8=XL*NS3=CAbLkyj&iZu%B5Bd2)TvpGes zVo%{^-opMIDF?FjDEZ;8Bq8ni2&LKNdc*}+Y^-OU{qr{)1^51E@l}C`W7k(!loXON zQ=x}be1E8~qocitQ#S}E@P+cG nbuHpe3xd#v*q%`lK-IY!{RDuQ?wto@y=jQ=4dDjVmx%uWU5-^J delta 543 zcmew*yg3=PblGKXi{QRO6O>3^n6IkUY ze`TqgY{H^6xtTSS4^_qFcdW{(Tnb1aFTW@^F(rtpyuptWc0t6r7k_kdq2j4`Be+YUC!C=qR8D z2r$TCfsvb9l9-c|nGDnd*REHDkVkeOSb;jo0=;yE3b6AKdXpeBwUghnH)DyZeOwxo z-*Y)JYEC}Qt<9g3nU+>soC 0) { + float3 spotDir = light.directionAndSpotScale.xyz; + float spotScale = light.directionAndSpotScale.w; + float spotOffset = light.spotParamsAndType.x; + float cone = SpotAttenDI(toLight, spotDir, spotScale, spotOffset); + if (projPos.w > 1e-4) { float2 projUV = projPos.xy / projPos.w * 0.5 + 0.5; projUV.y = 1.0 - projUV.y; - Texture2D spotTex = GetBindlessTexture(texIdx); - atten *= spotTex.SampleLevel(smp_linear, projUV, 0).r; + if (all(projUV >= 0.0) && all(projUV <= 1.0)) { + Texture2D spotTex = GetBindlessTexture(texIdx); + atten *= spotTex.SampleLevel(smp_linear, projUV, 0).r * cone; + } else { + atten = 0; + } } else { atten = 0; } @@ -83,7 +91,13 @@ float LightEmitterRadiusDI(GPULightDataDI light) float SoftShadowAmountDI(float dist) { - return saturate((dist - 1.25) / 10.0); + return saturate((dist - 0.35) / 6.0); +} + +float LightEmitterRadiusDISoft(GPULightDataDI light) +{ + float range = max(abs(light.colorAndRange.w), 0.5); + return clamp(range * 0.035, 0.05, 0.65); } #endif diff --git a/res/gamedata/shaders/r5/restir_di_shade.cs b/res/gamedata/shaders/r5/restir_di_shade.cs index 0ac19dc615a..56330e5d1eb 100644 --- a/res/gamedata/shaders/r5/restir_di_shade.cs +++ b/res/gamedata/shaders/r5/restir_di_shade.cs @@ -19,7 +19,7 @@ uint g_IdentityStaticCount; uint g_TerrainBatchCount; uint g_SkinnedBatchStart; - uint g_Pad0; + uint g_ParticleBatchStart; }; RaytracingAccelerationStructure g_SceneTLAS : register(t1); @@ -29,6 +29,8 @@ ByteAddressBuffer g_MegaIB : register(t18); ByteAddressBuffer g_GrassVB : register(t12); ByteAddressBuffer g_GrassIB : register(t13); +ByteAddressBuffer g_ParticleVB : register(t19); +ByteAddressBuffer g_ParticleIB : register(t4); Texture2D t_DIReservoir : register(t0); Texture2D t_Depth : register(t4); Texture2D t_BaseColor : register(t6); @@ -37,16 +39,16 @@ RWTexture2D u_DirectLighting : register(u0); -float TraceShadowRayDI(float3 origin, float3 dir, float tMax) +float TraceShadowRayDI(float3 origin, float3 dir, float tMax, float skinnedSelfMax) { return TraceVisibilityAtten( - g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, + g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, g_ParticleVB, g_ParticleIB, origin, dir, tMax, g_IdentityStaticCount, g_TerrainBatchCount, g_SkinnedBatchStart, g_GrassBatchStart, - g_DetailAtlasIndex, false); + g_ParticleBatchStart, g_DetailAtlasIndex, true, skinnedSelfMax); } -float TraceSoftShadowDI(float3 origin, float3 lightPos, float emitterRadius, bool hudLight, inout uint rng) +float TraceSoftShadowDI(float3 origin, float3 lightPos, float emitterRadius, bool hudLight, float skinnedSelfMax, inout uint rng) { float3 toLight = lightPos - origin; float dist = length(toLight); @@ -57,9 +59,9 @@ float TraceSoftShadowDI(float3 origin, float3 lightPos, float emitterRadius, boo float soft = hudLight ? 0.0 : SoftShadowAmountDI(dist); float radius = emitterRadius * lerp(0.12, 1.0, soft); uint samples = 1; - if (soft > 0.08) + if (soft > 0.05) samples = 2; - if (soft > 0.4) + if (soft > 0.25) samples = 4; float3 up = (abs(L.y) < 0.99) ? float3(0, 1, 0) : float3(1, 0, 0); @@ -81,7 +83,7 @@ float TraceSoftShadowDI(float3 origin, float3 lightPos, float emitterRadius, boo vis += 1.0; continue; } - vis += TraceShadowRayDI(origin, dir / max(d, 1e-4), tMax); + vis += TraceShadowRayDI(origin, dir / max(d, 1e-4), tMax, skinnedSelfMax); } return vis / (float)samples; } @@ -94,7 +96,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) return; float depth = t_Depth.Load(int3(pixel, 0)); - if (depth >= 1.0) + if (depth <= 0.0) return; DIReservoir r = UnpackDIReservoir(t_DIReservoir.Load(int3(pixel, 0))); @@ -104,6 +106,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float4 worldPosData = t_WorldPos.Load(int3(pixel, 0)); float3 worldPos = worldPosData.xyz; const bool hudSurf = IsHudSurfMark(worldPosData.w); + const float skinnedSelfMax = IsCharSurfMark(worldPosData.w) ? 0.65 : 0.0; float4 normalData = t_Normal.Load(int3(pixel, 0)); float3 N = normalize(normalData.xyz); float roughness = max(normalData.w, MIN_ROUGHNESS); @@ -126,9 +129,9 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float shadow = 1.0; if (!hudLight && asuint(light.spotParamsAndType.w) != 0xFFFFFFFFu) { - float3 biased = worldPos + N * 0.05; + float3 biased = worldPos + N * (skinnedSelfMax > 0.0 ? 0.06 : 0.05); uint rng = pcg_hash(pixel.x + pixel.y * 1973u + r.lightIndex * 26699u + asuint(dist * 100.0)); - shadow = TraceSoftShadowDI(biased, light.positionAndInvRangeSq.xyz, LightEmitterRadiusDI(light), false, rng); + shadow = TraceSoftShadowDI(biased, light.positionAndInvRangeSq.xyz, LightEmitterRadiusDISoft(light), false, skinnedSelfMax, rng); if (!hudSurf && shadow <= 0.001 && light.spotParamsAndType.y < 0.5) shadow = 0.35; } diff --git a/res/gamedata/shaders/r5/restir_di_spatial.cs b/res/gamedata/shaders/r5/restir_di_spatial.cs index 1ca8b2c9cb7..9e2d2add5cb 100644 --- a/res/gamedata/shaders/r5/restir_di_spatial.cs +++ b/res/gamedata/shaders/r5/restir_di_spatial.cs @@ -24,7 +24,7 @@ uint g_IdentityStaticCount; uint g_TerrainBatchCount; uint g_SkinnedBatchStart; - uint g_Pad0; + uint g_ParticleBatchStart; uint g_Pad1; uint g_Pad2; }; @@ -36,8 +36,10 @@ ByteAddressBuffer g_MegaIB : register(t18); ByteAddressBuffer g_GrassVB : register(t12); ByteAddressBuffer g_GrassIB : register(t13); +ByteAddressBuffer g_ParticleVB : register(t19); +ByteAddressBuffer g_ParticleIB : register(t4); Texture2D t_SrcDI : register(t0); -Texture2D t_Depth : register(t4); +Texture2D t_Depth : register(t14); Texture2D t_BaseColor : register(t6); Texture2D t_WorldPos : register(t7); Texture2D t_Normal : register(t11); @@ -47,10 +49,10 @@ float TraceShadowDI(float3 origin, float3 dir, float tMax) { return TraceVisibilityAtten( - g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, + g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, g_ParticleVB, g_ParticleIB, origin, dir, tMax, g_IdentityStaticCount, g_TerrainBatchCount, g_SkinnedBatchStart, g_GrassBatchStart, - g_DetailAtlasIndex, false); + g_ParticleBatchStart, g_DetailAtlasIndex, true, 0.0); } [numthreads(8, 8, 1)] @@ -61,7 +63,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) return; float depth = t_Depth.Load(int3(pixel, 0)); - if (depth >= 1.0 || g_SpatialSamples == 0) { + if (depth <= 0.0 || g_SpatialSamples == 0) { u_DIReservoir[pixel] = t_SrcDI.Load(int3(pixel, 0)); return; } diff --git a/res/gamedata/shaders/r5/restir_di_temporal.cs b/res/gamedata/shaders/r5/restir_di_temporal.cs index 956236e3141..74e4da4e1b6 100644 --- a/res/gamedata/shaders/r5/restir_di_temporal.cs +++ b/res/gamedata/shaders/r5/restir_di_temporal.cs @@ -39,7 +39,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) return; float depth = t_Depth.Load(int3(pixel, 0)); - if (depth >= 1.0) { + if (depth <= 0.0) { u_DIReservoir[pixel] = PackDIReservoir(EmptyDIReservoir()); return; } diff --git a/res/gamedata/shaders/r5/restir_gi_initial.cs b/res/gamedata/shaders/r5/restir_gi_initial.cs index ce8f7aee6dd..3906eee8b0d 100644 --- a/res/gamedata/shaders/r5/restir_gi_initial.cs +++ b/res/gamedata/shaders/r5/restir_gi_initial.cs @@ -38,7 +38,11 @@ uint g_CacheSize; float g_CacheCellSize; uint g_CacheMaxAge; - uint g_Pad1; + uint g_ParticleBatchStart; + float g_LodDist; + float g_SunAngular; + uint g_SunSoftSamples; + float g_CameraMotion; }; RaytracingAccelerationStructure g_SceneTLAS : register(t1); @@ -50,6 +54,8 @@ ByteAddressBuffer g_SkinnedIB : register(t11); ByteAddressBuffer g_GrassVB : register(t12); ByteAddressBuffer g_GrassIB : register(t13); +ByteAddressBuffer g_ParticleVB : register(t19); +ByteAddressBuffer g_ParticleIB : register(t4); Texture2D t_Depth : register(t14); Texture2D t_Normal : register(t15); Texture2D t_BaseColor : register(t16); @@ -73,12 +79,14 @@ bool IsSkinnedBatch(uint batchIdx) { return g_SkinnedBatchStart != 0xFFFFFFFFu && batchIdx >= g_SkinnedBatchStart && - (g_GrassBatchStart == 0xFFFFFFFFu || batchIdx < g_GrassBatchStart); + (g_GrassBatchStart == 0xFFFFFFFFu || batchIdx < g_GrassBatchStart) && + (g_ParticleBatchStart == 0xFFFFFFFFu || batchIdx < g_ParticleBatchStart); } bool IsGrassBatch(uint batchIdx) { - return g_GrassBatchStart != 0xFFFFFFFFu && batchIdx >= g_GrassBatchStart; + return g_GrassBatchStart != 0xFFFFFFFFu && batchIdx >= g_GrassBatchStart && + (g_ParticleBatchStart == 0xFFFFFFFFu || batchIdx < g_ParticleBatchStart); } bool IsTerrainBatch(uint batchIdx) @@ -95,13 +103,60 @@ float3 SampleSky(float3 dir) return lerp(s0, s1, w); } -float TraceShadow(float3 origin, float3 dir, float tMax, bool nearSkinnedOccludes) +float3 SampleSkyDiffuse(float3 dir) +{ + float w = g_SunColor_SkyWeight.w; + float3 s0 = g_Sky0.SampleLevel(smp_linear, dir, 4.0).rgb; + float3 s1 = g_Sky1.SampleLevel(smp_linear, dir, 4.0).rgb; + return lerp(s0, s1, w); +} + +float3 SampleSkySpec(float3 dir, float roughness) +{ + float mip = saturate(roughness) * 5.0; + float w = g_SunColor_SkyWeight.w; + float3 s0 = g_Sky0.SampleLevel(smp_linear, dir, mip).rgb; + float3 s1 = g_Sky1.SampleLevel(smp_linear, dir, mip).rgb; + return lerp(s0, s1, w); +} + +float TraceShadow(float3 origin, float3 dir, float tMax, bool nearSkinnedOccludes, float skinnedSelfMax) { return TraceVisibilityAtten( - g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, + g_SceneTLAS, g_BatchInfo, g_MegaVB, g_MegaIB, g_GrassVB, g_GrassIB, g_ParticleVB, g_ParticleIB, origin, dir, tMax, g_IdentityStaticCount, g_TerrainBatchCount, g_SkinnedBatchStart, g_GrassBatchStart, - g_DetailAtlasIndex, nearSkinnedOccludes); + g_ParticleBatchStart, g_DetailAtlasIndex, nearSkinnedOccludes, skinnedSelfMax); +} + +float TraceSoftShadowSun(float3 origin, float3 sunDir, float viewDist, bool nearSkinnedOccludes, float skinnedSelfMax, inout uint rng) +{ + float lod = saturate(viewDist / max(g_LodDist, 1.0)); + uint maxS = max(g_SunSoftSamples, 1u); + uint samples = 1u; + if (lod < 0.55) + samples = maxS; + else if (lod < 0.85) + samples = max(1u, maxS / 2u); + + if (samples <= 1u) + return TraceShadow(origin, sunDir, 10000.0, nearSkinnedOccludes, skinnedSelfMax); + + float3 L = normalize(sunDir); + float3 up = abs(L.y) < 0.99 ? float3(0, 1, 0) : float3(1, 0, 0); + float3 T = normalize(cross(up, L)); + float3 B = cross(L, T); + float radius = max(g_SunAngular, 0.001); + float vis = 0.0; + for (uint i = 0; i < samples; ++i) + { + float2 u = float2(rand_float(rng), rand_float(rng)); + float r = sqrt(u.x) * radius; + float a = u.y * 6.2831853; + float3 dir = normalize(L + T * (cos(a) * r) + B * (sin(a) * r)); + vis += TraceShadow(origin, dir, 10000.0, nearSkinnedOccludes, skinnedSelfMax); + } + return vis / (float)samples; } float PointLightAttenuationRT(float distSq, float invRangeSq) @@ -181,7 +236,7 @@ float3 EvaluateLocalLightsRT( float endSkip = max(0.55, LightEmitterRadiusDI(light) * 3.0 + 0.25); float shadowDist = max(dist - endSkip, dist * 0.88); if (shadowDist > 0.02) - shadow = TraceShadow(biasedPos, L, shadowDist, false); + shadow = TraceShadow(biasedPos, L, shadowDist, true, 0.0); } if (shadow <= 0.001) continue; @@ -230,7 +285,7 @@ DIReservoir SampleDIReservoir( return r; } -float3 SampleTerrainAlbedo(TerrainMaterialData mat, float2 uv); +float3 SampleTerrainAlbedo(TerrainMaterialData mat, float2 uv, float hitDist); struct BounceHit { float3 position; @@ -296,7 +351,7 @@ BounceHit TraceBounce(float3 origin, float3 direction, inout uint rng, bool skip hitN = GetSkinnedHitNormal(g_GrassVB, g_GrassIB, info, primIdx, bary); geoN = GetSkinnedHitGeoNormal(g_GrassVB, g_GrassIB, info, primIdx); } else if (IsSkinnedBatch(batchIdx)) { - if (skipNearSkinned && q.CommittedRayT() < 0.45) { + if (skipNearSkinned && q.CommittedRayT() < 0.2) { rayOrigin = rayOrigin + direction * (q.CommittedRayT() + 0.002); continue; } @@ -334,7 +389,7 @@ BounceHit TraceBounce(float3 origin, float3 direction, inout uint rng, bool skip baked = g_HemiColor.rgb * albedo * 0.35; } else if (IsTerrainBatch(batchIdx)) { TerrainMaterialData tmat = g_TerrainMaterials[info.materialID]; - albedo = SampleTerrainAlbedo(tmat, hitUV); + albedo = SampleTerrainAlbedo(tmat, hitUV, q.CommittedRayT()); baked = ShadeBakedFromTerrainLmap(tmat, lmUV, albedo, g_HemiColor.rgb, hemi); } else { MaterialData mat = g_Materials[info.materialID]; @@ -383,14 +438,24 @@ float4 SampleTerrainTexture(uint index, float2 uv) return GetBindlessTexture(index).SampleLevel(smp_linear, uv, 0); } -float3 SampleTerrainAlbedo(TerrainMaterialData mat, float2 uv) +float3 SampleTerrainAlbedo(TerrainMaterialData mat, float2 uv, float hitDist) { float2 baseUV = uv; - float2 detailUV = uv * mat.detailScale; float4 baseSample = SampleTerrainTexture(mat.baseAlbedoIndex, baseUV); float4 mask = SampleTerrainTexture(mat.blendMaskIndex, baseUV); float maskSum = dot(mask, float4(1, 1, 1, 1)); mask = maskSum > 0.001 ? mask / maskSum : float4(0.25, 0.25, 0.25, 0.25); + if (hitDist > g_LodDist * 0.5) + { + float m = max(max(mask.r, mask.g), max(mask.b, mask.a)); + float3 d = baseSample.rgb; + if (mask.r >= m) d = SampleTerrainTexture(mat.detailR_Index, uv * mat.detailScale).rgb; + else if (mask.g >= m) d = SampleTerrainTexture(mat.detailG_Index, uv * mat.detailScale).rgb; + else if (mask.b >= m) d = SampleTerrainTexture(mat.detailB_Index, uv * mat.detailScale).rgb; + else d = SampleTerrainTexture(mat.detailA_Index, uv * mat.detailScale).rgb; + return baseSample.rgb * d * 2.0; + } + float2 detailUV = uv * mat.detailScale; float3 detailR = SampleTerrainTexture(mat.detailR_Index, detailUV).rgb; float3 detailG = SampleTerrainTexture(mat.detailG_Index, detailUV).rgb; float3 detailB = SampleTerrainTexture(mat.detailB_Index, detailUV).rgb; @@ -423,37 +488,45 @@ float3 EvaluateSecondaryLo( float3 sunDir, float3 sunColor, float2 pixelPos, + float primaryViewDist, inout uint rngState) { float3 hitBiased = h.position + h.geoNormal * 0.005; float3 hitV = normalize(primaryPos - h.position); - float hitShadow = TraceShadow(hitBiased, sunDir, 10000.0, false); + float hitShadow = TraceShadow(hitBiased, sunDir, 10000.0, true, 0.0); float3 Lo = ShadeHitDirect(h.albedo, h.normal, hitV, h.metallic, h.roughness, sunDir, sunColor, hitShadow, h.baked); Lo += h.emissive; float hitLinDepth = abs(mul(g_WorldToView, float4(h.position, 1.0)).z); - Lo += EvaluateLocalLightsRT(h.position, h.normal, hitV, h.albedo, h.metallic, h.roughness, - pixelPos, hitLinDepth, min(g_LocalLightSamples, 4u), rngState); - if (g_Bounces >= 2u && rand_float(rngState) < 0.75) { + float farLod = saturate(primaryViewDist / max(g_LodDist, 1.0)); + uint localS = (farLod > 0.7) ? 0u : ((rand_float(rngState) < 0.5) ? 1u : 0u); + if (localS > 0) + Lo += EvaluateLocalLightsRT(h.position, h.normal, hitV, h.albedo, h.metallic, h.roughness, + pixelPos, hitLinDepth, localS, rngState); + if (g_Bounces >= 2u && farLod < 0.65 && rand_float(rngState) < 0.75) { float2 u2 = float2(rand_float(rngState), rand_float(rngState)); float3 dir2 = cosine_weighted_hemisphere(u2, h.normal); BounceHit h2 = TraceBounce(hitBiased, dir2, rngState, false); if (h2.valid) { float3 hit2Biased = h2.position + h2.geoNormal * 0.005; float3 hit2V = normalize(h.position - h2.position); - float sh2 = TraceShadow(hit2Biased, sunDir, 10000.0, false); + float sh2 = TraceShadow(hit2Biased, sunDir, 10000.0, true, 0.0); float3 Lo2 = ShadeHitDirect(h2.albedo, h2.normal, hit2V, h2.metallic, h2.roughness, sunDir, sunColor, sh2, h2.baked); Lo2 += h2.emissive; - Lo2 += EvaluateLocalLightsRT(h2.position, h2.normal, hit2V, h2.albedo, h2.metallic, h2.roughness, - pixelPos, abs(mul(g_WorldToView, float4(h2.position, 1.0)).z), min(g_LocalLightSamples, 2u), rngState); + float3 cached2 = QueryIrradianceCacheRW(u_IrradianceCache, h2.position, g_CacheCellSize, g_CacheSize, g_FrameIndex, g_CacheMaxAge); + if (any(cached2 > 0)) + Lo2 = max(Lo2, cached2 * h2.albedo * 0.65); Lo += Lo2 * h2.albedo * (1.0 / 0.75) * 0.55; UpdateIrradianceCache(u_IrradianceCache, h2.position, Lo2, g_CacheCellSize, g_CacheSize, g_FrameIndex); } else { - Lo += SampleSky(dir2) * (1.0 / 0.75) * 0.45; + Lo += SampleSkyDiffuse(dir2) * (1.0 / 0.75) * 0.45; } } float3 cached = QueryIrradianceCacheRW(u_IrradianceCache, h.position, g_CacheCellSize, g_CacheSize, g_FrameIndex, g_CacheMaxAge); - if (Luminance(Lo) < 0.02 && any(cached > 0)) - Lo = max(Lo, cached * h.albedo); + if (any(cached > 0)) + { + float cacheW = saturate(0.55 + 0.35 * (1.0 - saturate(Luminance(Lo) * 4.0))); + Lo = lerp(Lo, max(Lo, cached * h.albedo), cacheW); + } UpdateIrradianceCache(u_IrradianceCache, h.position, Lo, g_CacheCellSize, g_CacheSize, g_FrameIndex); return min(Lo, RESTIR_MAX_RADIANCE); } @@ -467,7 +540,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float depth = t_Depth.Load(int3(pixel, 0)); float4 worldPosData = t_WorldPos.Load(int3(pixel, 0)); - if (depth >= 1.0) { + if (depth <= 0.0) { u_DirectLighting[pixel] = 0; u_ReservoirA[pixel] = 0; u_ReservoirB[pixel] = 0; @@ -479,8 +552,18 @@ void main(uint3 dispatchID : SV_DispatchThreadID) return; } - const bool isHud = IsHudSurfMark(worldPosData.w); float3 worldPos = worldPosData.xyz; + if (all(worldPos == 0.0)) { + float2 uv = (float2(pixel) + 0.5) / g_ScreenSize; + float2 ndc = float2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0); + float4 worldH = mul(g_InvViewProj, float4(ndc, depth, 1.0)); + worldPos = worldH.xyz / max(worldH.w, 1e-5); + worldPosData.xyz = worldPos; + } + + const bool isHud = IsHudSurfMark(worldPosData.w); + const bool isChar = IsCharSurfMark(worldPosData.w); + const float skinnedSelfMax = isChar ? 0.65 : 0.0; float4 normalData = t_Normal.Load(int3(pixel, 0)); float4 baseColorData = t_BaseColor.Load(int3(pixel, 0)); @@ -493,11 +576,13 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float sunIntensity = g_SunDir_Intensity.w; float3 sunColor = g_SunColor_SkyWeight.xyz * sunIntensity; - float3 biasedPos = worldPos + N * (isHud ? 0.03 : 0.01); + float3 biasedPos = worldPos + N * (isHud ? 0.03 : (isChar ? 0.035 : 0.01)); float linearDepth = abs(mul(g_WorldToView, float4(worldPos, 1.0)).z); + float viewDist = length(worldPos - g_CameraPos.xyz); float2 pixelPos = float2(pixel) + 0.5; - float shadow = TraceShadow(biasedPos, sunDir, 10000.0, isHud); + uint rng = pcg_hash(pixel.x + pixel.y * 1973u + g_FrameIndex * 26699u); + float shadow = TraceSoftShadowSun(biasedPos, sunDir, viewDist, isHud, skinnedSelfMax, rng); float3 direct = 0; if (shadow > 0.001) direct = PBRDirectLighting(albedo, N, V, sunDir, sunColor * shadow, metallic, roughness, 1); @@ -510,8 +595,6 @@ void main(uint3 dispatchID : SV_DispatchThreadID) albedo, N, V, sunDir, sunColor, max(shadow, 0.35), sssTint, sssThickness, sssMask); } - - uint rng = pcg_hash(pixel.x + pixel.y * 1973u + g_FrameIndex * 26699u); DIReservoir diRes = SampleDIReservoir( worldPos, N, V, albedo, metallic, roughness, pixelPos, linearDepth, max(g_DICandidates, 1u), isHud, rng); @@ -529,21 +612,27 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float2 u = float2(rand_float(rng), rand_float(rng)); float3 bounceDir = cosine_weighted_hemisphere(u, N); float cosPDF = max(dot(bounceDir, N), 0) / PI; - BounceHit hit = TraceBounce(biasedPos, bounceDir, rng, isHud); + BounceHit hit = TraceBounce(biasedPos, bounceDir, rng, isHud || isChar); float3 LoSurf = 0; - float3 LoSky = min(SampleSky(bounceDir), RESTIR_MAX_RADIANCE); + float3 LoSky = min(SampleSkyDiffuse(bounceDir), RESTIR_MAX_RADIANCE); float3 samplePos = worldPos + bounceDir * g_ClusterScales.y; float3 sampleN = -bounceDir; bool usedSurf = false; if (hit.valid && cosPDF > 1e-6) { - LoSurf = EvaluateSecondaryLo(hit, worldPos, sunDir, sunColor, pixelPos, rng); + LoSurf = EvaluateSecondaryLo(hit, worldPos, sunDir, sunColor, pixelPos, viewDist, rng); if (isHud) LoSurf = min(LoSurf, sunColor * 0.35 + 0.08); samplePos = hit.position; sampleN = hit.normal; usedSurf = true; + if (Luminance(LoSurf) < 0.015) + { + float3 pCache = QueryIrradianceCacheRW(u_IrradianceCache, worldPos, g_CacheCellSize, g_CacheSize, g_FrameIndex, g_CacheMaxAge); + if (any(pCache > 0)) + LoSurf = max(LoSurf, pCache * albedo * 0.5); + } } else if (isHud) { LoSky = min(LoSky, sunColor * 0.25 + 0.06); } @@ -569,46 +658,61 @@ void main(uint3 dispatchID : SV_DispatchThreadID) } } - float3 R; - if (roughness < 0.08) { - R = reflect(-V, N); - } else { - float2 uSpec = float2(rand_float(rng), rand_float(rng)); - float3 H = ImportanceSampleGGX(uSpec, N, roughness); - R = normalize(2.0 * max(dot(V, H), 0.0) * H - V); - if (dot(R, N) <= 0.0) - R = reflect(-V, N); - } + float3 Fenv = NRD_EnvironmentTerm_Rtg(F0primary, abs(dot(N, V)), roughness); + float sampleRough = max(roughness, 0.06); + uint specSamples = 1u; float3 specular = 0; float specHitDist = 0; - float3 Fenv = NRD_EnvironmentTerm_Rtg(F0primary, abs(dot(N, V)), roughness); + float3 bestSamplePos = worldPos; + float3 bestSampleN = N; + float3 bestLo = 0; + float accW = 0; + + [loop] for (uint si = 0; si < specSamples; si++) { + float2 uSpec = float2(rand_float(rng), rand_float(rng)); + float3 H = ImportanceSampleGGX(uSpec, N, sampleRough); + float3 R = normalize(2.0 * max(dot(V, H), 0.0) * H - V); + if (dot(R, N) <= 0.0) + R = reflect(-V, N); + if (dot(R, N) <= 0.0) + continue; - if (dot(R, N) > 0.0) { - BounceHit specHit = TraceBounce(biasedPos, R, rng, isHud); + BounceHit specHit = TraceBounce(biasedPos, R, rng, isHud || isChar); float3 Lo = 0; float3 samplePos = worldPos + R * g_ClusterScales.y; float3 sampleN = -R; + float hitDist = g_ClusterScales.y; if (specHit.valid) { - Lo = EvaluateSecondaryLo(specHit, worldPos, sunDir, sunColor, pixelPos, rng); + Lo = EvaluateSecondaryLo(specHit, worldPos, sunDir, sunColor, pixelPos, viewDist, rng); if (isHud) Lo = min(Lo, sunColor * 0.3 + 0.05); samplePos = specHit.position; sampleN = specHit.normal; - specHitDist = length(specHit.position - worldPos); - if (specHitDist < 1e-3) - specHitDist = 0; + hitDist = length(specHit.position - worldPos); + if (hitDist < 1e-3) + hitDist = 0; } else { - Lo = min(SampleSky(R), RESTIR_MAX_RADIANCE); + Lo = min(SampleSkySpec(R, sampleRough), RESTIR_MAX_RADIANCE); if (isHud) Lo = min(Lo, sunColor * 0.2 + 0.04); - specHitDist = g_ClusterScales.y; } - specular = min(Lo, RESTIR_MAX_RADIANCE) * Fenv; + Lo = min(Lo, RESTIR_MAX_RADIANCE); + float w = 1.0 / (float)specSamples; + specular += Lo * Fenv * w; + specHitDist += hitDist * w; + accW += w; + if (Luminance(Lo) >= Luminance(bestLo)) { + bestLo = Lo; + bestSamplePos = samplePos; + bestSampleN = sampleN; + } + } + + if (accW > 1e-4) { float targetLum = Luminance(specular); if (targetLum > 0) { - float w = targetLum; - ReservoirUpdate(specReservoir, w, samplePos, sampleN, Lo, rng); + ReservoirUpdate(specReservoir, targetLum, bestSamplePos, bestSampleN, bestLo, rng); specReservoir.W = (specReservoir.M > 0) ? specReservoir.w_sum / (targetLum * (float)specReservoir.M) : 0; specReservoir.age = 0; } diff --git a/res/gamedata/shaders/r5/rt_visibility.h b/res/gamedata/shaders/r5/rt_visibility.h index 164a5688092..63f7c1be3cc 100644 --- a/res/gamedata/shaders/r5/rt_visibility.h +++ b/res/gamedata/shaders/r5/rt_visibility.h @@ -4,6 +4,7 @@ #include "bindless_common.h" #include "rt_common.h" #include "rt_grass_alpha.h" +#include "rt_particle_alpha.h" static const uint RT_VIS_MAX_SKIPS = 8; @@ -14,6 +15,8 @@ float TraceVisibilityAtten( ByteAddressBuffer megaIB, ByteAddressBuffer grassVB, ByteAddressBuffer grassIB, + ByteAddressBuffer particleVB, + ByteAddressBuffer particleIB, float3 origin, float3 dir, float tMax, @@ -21,14 +24,17 @@ float TraceVisibilityAtten( uint terrainBatchCount, uint skinnedBatchStart, uint grassBatchStart, + uint particleBatchStart, uint detailAtlasIndex, - bool nearSkinnedOccludes) + bool nearSkinnedOccludes, + float skinnedSelfMax) { float atten = 1.0; float3 shadowOrigin = origin; float3 shadowDir = normalize(dir); float remain = tMax; const float selfSkip = nearSkinnedOccludes ? 0.004 : 0.02; + const float skinSelf = max(skinnedSelfMax, 0.0); for (uint si = 0; si < RT_VIS_MAX_SKIPS; si++) { RayDesc ray; @@ -43,6 +49,7 @@ float TraceVisibilityAtten( if (q.CandidateType() == CANDIDATE_NON_OPAQUE_TRIANGLE) { uint candBatch = q.CandidateInstanceID() + q.CandidateGeometryIndex(); if (grassBatchStart != 0xFFFFFFFFu && candBatch >= grassBatchStart && + (particleBatchStart == 0xFFFFFFFFu || candBatch < particleBatchStart) && GrassTexelOpaque(grassVB, grassIB, batchInfo, candBatch, q.CandidatePrimitiveIndex(), q.CandidateTriangleBarycentrics(), detailAtlasIndex)) q.CommitNonOpaqueTriangleHit(); @@ -63,7 +70,8 @@ float TraceVisibilityAtten( uint sBatchIdx = q.CommittedInstanceID() + q.CommittedGeometryIndex(); RTBatchInfo sInfo = batchInfo[sBatchIdx]; - bool isGrass = grassBatchStart != 0xFFFFFFFFu && sBatchIdx >= grassBatchStart; + bool isGrass = grassBatchStart != 0xFFFFFFFFu && sBatchIdx >= grassBatchStart && + (particleBatchStart == 0xFFFFFFFFu || sBatchIdx < particleBatchStart); if (isGrass) { float tHit = q.CommittedRayT(); if (tHit < 0.1) { @@ -82,6 +90,28 @@ float TraceVisibilityAtten( MaterialData sMat = g_Materials[sInfo.materialID]; + bool isParticle = particleBatchStart != 0xFFFFFFFFu && sBatchIdx >= particleBatchStart; + if (isParticle) { + float tHit = q.CommittedRayT() + 0.002; + if (sMat.flags & MAT_FLAG_EMISSIVE) { + shadowOrigin = shadowOrigin + shadowDir * tHit; + remain -= tHit; + if (remain <= 0.001) break; + continue; + } + float2 sUV = GetParticleHitUV(particleVB, particleIB, sInfo, + q.CommittedPrimitiveIndex(), q.CommittedTriangleBarycentrics()); + float2 d = sUV * 2.0 - 1.0; + float soft = saturate(1.0 - dot(d, d)); + soft *= soft; + atten *= (1.0 - 0.65 * soft); + if (atten < 0.2) { atten = 0.2; break; } + shadowOrigin = shadowOrigin + shadowDir * tHit; + remain -= tHit; + if (remain <= 0.001) break; + continue; + } + if (sMat.flags & MAT_FLAG_EMISSIVE) { float tHit = q.CommittedRayT() + 0.002; shadowOrigin = shadowOrigin + shadowDir * tHit; @@ -108,19 +138,15 @@ float TraceVisibilityAtten( if (isTerrain) { atten = 0; break; } bool isSkinned = skinnedBatchStart != 0xFFFFFFFFu && sBatchIdx >= skinnedBatchStart && - (grassBatchStart == 0xFFFFFFFFu || sBatchIdx < grassBatchStart); + (grassBatchStart == 0xFFFFFFFFu || sBatchIdx < grassBatchStart) && + (particleBatchStart == 0xFFFFFFFFu || sBatchIdx < particleBatchStart); if (isSkinned) { - float tHit = q.CommittedRayT(); - if (!nearSkinnedOccludes && tHit < 0.85) { - shadowOrigin = shadowOrigin + shadowDir * (tHit + 0.002); - remain -= (tHit + 0.002); + if (tHit0 < skinSelf) { + shadowOrigin = shadowOrigin + shadowDir * (tHit0 + 0.002); + remain -= (tHit0 + 0.002); if (remain <= 0.001) break; continue; } - if (!nearSkinnedOccludes) { - atten *= 0.35; - break; - } atten = 0; break; } @@ -157,12 +183,15 @@ bool TraceVisibilityClear( ByteAddressBuffer megaIB, ByteAddressBuffer grassVB, ByteAddressBuffer grassIB, + ByteAddressBuffer particleVB, + ByteAddressBuffer particleIB, float3 origin, float3 target, uint identityStaticCount, uint terrainBatchCount, uint skinnedBatchStart, uint grassBatchStart, + uint particleBatchStart, uint detailAtlasIndex) { float3 dir = target - origin; @@ -170,10 +199,10 @@ bool TraceVisibilityClear( if (dist < 1e-4) return false; float atten = TraceVisibilityAtten( - tlas, batchInfo, megaVB, megaIB, grassVB, grassIB, + tlas, batchInfo, megaVB, megaIB, grassVB, grassIB, particleVB, particleIB, origin, dir / dist, max(dist - 0.02, 0.001), identityStaticCount, terrainBatchCount, skinnedBatchStart, grassBatchStart, - detailAtlasIndex, false); + particleBatchStart, detailAtlasIndex, true, 0.0); return atten > 0.15; } diff --git a/res/gamedata/shaders/r5/shared/surface_marks.h b/res/gamedata/shaders/r5/shared/surface_marks.h index 7b2d3290acc..8dcf222ae34 100644 --- a/res/gamedata/shaders/r5/shared/surface_marks.h +++ b/res/gamedata/shaders/r5/shared/surface_marks.h @@ -29,7 +29,7 @@ bool IsWaterSurfMark(float w) bool SkipRtSurfLighting(float classifyW, float guideW) { if (IsHudSurfMark(guideW)) - return false; + return true; return IsWaterSurfMark(classifyW); } diff --git a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp index f4eaba7c7f3..b9543d55cff 100644 --- a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp +++ b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.cpp @@ -43,6 +43,11 @@ nvrhi::ShaderHandle VariantPSOCache::LoadShader(nvrhi::ShaderType type, const ch return result.handle; } +static bool IsSkinnedVertexFormat(u32 vertexFormat) +{ + return vertexFormat >= VF_SKINNED_NONHQ && vertexFormat <= VF_SKINNED_HQ3W; +} + static const char* GetDefaultSkinnedVS(u32 vertexFormat) { switch (vertexFormat) @@ -58,10 +63,7 @@ static const char* GetDefaultSkinnedVS(u32 vertexFormat) static const char* GetDefaultSkinnedPS(u32 vertexFormat) { - // All skinned VS share the same lit PS (IBL + CSM + clustered) - if (vertexFormat >= VF_SKINNED_NONHQ && vertexFormat <= VF_SKINNED_HQ3W) - return "bindless_skinned"; - return nullptr; + return IsSkinnedVertexFormat(vertexFormat) ? "bindless_skinned" : nullptr; } nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( @@ -75,7 +77,17 @@ nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( nvrhi::IBindingLayout* passBindingLayout, nvrhi::IBindingLayout* bindlessLayout) { - VariantPSOKey key{variantIndex, passIndex, vertexFormat}; + if (!framebuffer) + return nullptr; + + const auto& fbInfo = framebuffer->getFramebufferInfo(); + VariantPSOKey key{ + variantIndex, + passIndex, + vertexFormat, + static_cast(fbInfo.colorFormats.size()), + static_cast(fbInfo.depthFormat), + fbInfo.sampleCount}; auto it = m_cache.find(key); if (it != m_cache.end()) @@ -90,25 +102,17 @@ nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( const char* psName = pass.psName.c_str(); bool remappedToSkinned = false; - // Model materials are authored as bindless_forward; skinned draws must use - // bindless_skinned* so NPCs/bots get the same IBL/CSM/clustered path as world. - if (vertexFormat >= VF_SKINNED_NONHQ && vertexFormat <= VF_SKINNED_HQ3W) + if (IsSkinnedVertexFormat(vertexFormat)) { - if (!xr_strcmp(vsName, "bindless_forward")) + if (const char* skinnedVS = GetDefaultSkinnedVS(vertexFormat)) { - if (const char* skinnedVS = GetDefaultSkinnedVS(vertexFormat)) - { - vsName = skinnedVS; - remappedToSkinned = true; - } + vsName = skinnedVS; + remappedToSkinned = true; } - if (!xr_strcmp(psName, "bindless_forward")) + if (const char* skinnedPS = GetDefaultSkinnedPS(vertexFormat)) { - if (const char* skinnedPS = GetDefaultSkinnedPS(vertexFormat)) - { - psName = skinnedPS; - remappedToSkinned = true; - } + psName = skinnedPS; + remappedToSkinned = true; } } @@ -121,7 +125,6 @@ nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( if (!vsResult.handle || !psResult.handle) return nullptr; - // Cache handles for reuse { string256 vsKey, psKey; xr_sprintf(vsKey, "%d:%s", static_cast(nvrhi::ShaderType::Vertex), vsName); @@ -130,8 +133,6 @@ nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( m_shaderCache[shared_str(psKey)] = psResult.handle; } - // Remapped skinned default shaders share SkinningPass layout (IBL/CSM binds). - // Other custom variants (e.g. water) keep a reflection-matched layout. nvrhi::BindingLayoutHandle layoutHandle = passBindingLayout; const bool customShaders = !remappedToSkinned && @@ -163,11 +164,19 @@ nvrhi::IGraphicsPipeline* VariantPSOCache::GetOrCreatePSO( pipeDesc.renderState.depthStencilState = pass.depthStencil; pipeDesc.renderState.rasterState = pass.rasterState; pipeDesc.renderState.rasterState.frontCounterClockwise = false; + if (variant.wmark) + { + pipeDesc.renderState.rasterState.depthBias = 16; + pipeDesc.renderState.rasterState.slopeScaledDepthBias = 2.f; + } if (pass.blendEnabled && !(remappedToSkinned && !variant.transparent)) { pipeDesc.renderState.blendState.targets[0] = pass.blendRT; pipeDesc.renderState.blendState.alphaToCoverageEnable = pass.alphaToCoverage; + if (variant.wmark) + pipeDesc.renderState.blendState.targets[0].setColorWriteMask( + nvrhi::ColorMask::Red | nvrhi::ColorMask::Green | nvrhi::ColorMask::Blue); } auto pipeline = device->createGraphicsPipeline(pipeDesc, framebuffer); diff --git a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h index 5be3aac4c95..07d55cd7990 100644 --- a/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h +++ b/src/Layers/xrRender/ShaderVariant/VariantPSOCache.h @@ -13,12 +13,18 @@ struct VariantPSOKey u32 variantIndex; u32 passIndex; u32 vertexFormat; + u32 colorTargetCount = 0; + u32 depthFormat = 0; + u32 sampleCount = 1; bool operator<(const VariantPSOKey& o) const { if (variantIndex != o.variantIndex) return variantIndex < o.variantIndex; if (passIndex != o.passIndex) return passIndex < o.passIndex; - return vertexFormat < o.vertexFormat; + if (vertexFormat != o.vertexFormat) return vertexFormat < o.vertexFormat; + if (colorTargetCount != o.colorTargetCount) return colorTargetCount < o.colorTargetCount; + if (depthFormat != o.depthFormat) return depthFormat < o.depthFormat; + return sampleCount < o.sampleCount; } }; From 17fd794beb286c3dde6e6883aa317224577c9347 Mon Sep 17 00:00:00 2001 From: FoxConED Date: Thu, 30 Jul 2026 00:01:28 +0300 Subject: [PATCH 255/261] xrRender: improve RT water reflections lighting and gloss --- res/gamedata/shaders/r5/restir_water_rt.cs | 153 +++++++++++++++++-- res/gamedata/shaders/r5/shared/waterconfig.h | 8 +- res/gamedata/shaders/r5/water.ps | Bin 5247 -> 5426 bytes 3 files changed, 146 insertions(+), 15 deletions(-) diff --git a/res/gamedata/shaders/r5/restir_water_rt.cs b/res/gamedata/shaders/r5/restir_water_rt.cs index 48840f45db9..48ed0aeda10 100644 --- a/res/gamedata/shaders/r5/restir_water_rt.cs +++ b/res/gamedata/shaders/r5/restir_water_rt.cs @@ -51,6 +51,12 @@ bool IsGrassBatch(uint batchIdx) return g_GrassBatchStart != 0xFFFFFFFFu && batchIdx >= g_GrassBatchStart; } +bool IsTerrainBatch(uint batchIdx) +{ + return batchIdx >= g_IdentityStaticCount && + batchIdx < g_IdentityStaticCount + g_TerrainBatchCount; +} + float3 SampleSkyW(float3 dir) { float3 d = normalize(dir); @@ -77,13 +83,74 @@ float3 DecodeNormal(float3 nEnc) return n * rsqrt(lenSq); } -float3 TraceWaterReflect(float3 origin, float3 dir, float3 skyRefl) +float4 SampleTerrainTexture(uint index, float2 uv, float mip) +{ + if (index == INVALID_TEXTURE_INDEX) + return float4(0.5, 0.5, 0.5, 1.0); + return GetBindlessTexture(index).SampleLevel(smp_linear, uv, mip); +} + +float3 SampleTerrainAlbedoWater(TerrainMaterialData mat, float2 uv, float hitDist) +{ + float lodDist = max(g_CameraPos.w, 10.0); + float mip = saturate(hitDist / lodDist) * 0.45; + float2 baseUV = uv; + float4 baseSample = SampleTerrainTexture(mat.baseAlbedoIndex, baseUV, mip * 0.35); + float4 mask = SampleTerrainTexture(mat.blendMaskIndex, baseUV, mip * 0.35); + float maskSum = dot(mask, float4(1, 1, 1, 1)); + mask = maskSum > 0.001 ? mask / maskSum : float4(0.25, 0.25, 0.25, 0.25); + float2 detailUV = uv * mat.detailScale; + float detailMip = mip + 0.1; + if (hitDist > lodDist * 0.65) + { + float m = max(max(mask.r, mask.g), max(mask.b, mask.a)); + float3 d = baseSample.rgb; + if (mask.r >= m) d = SampleTerrainTexture(mat.detailR_Index, detailUV, detailMip).rgb; + else if (mask.g >= m) d = SampleTerrainTexture(mat.detailG_Index, detailUV, detailMip).rgb; + else if (mask.b >= m) d = SampleTerrainTexture(mat.detailB_Index, detailUV, detailMip).rgb; + else d = SampleTerrainTexture(mat.detailA_Index, detailUV, detailMip).rgb; + return baseSample.rgb * d * 2.0; + } + float3 detailR = SampleTerrainTexture(mat.detailR_Index, detailUV, detailMip).rgb; + float3 detailG = SampleTerrainTexture(mat.detailG_Index, detailUV, detailMip).rgb; + float3 detailB = SampleTerrainTexture(mat.detailB_Index, detailUV, detailMip).rgb; + float3 detailA = SampleTerrainTexture(mat.detailA_Index, detailUV, detailMip).rgb; + float3 blendedDetail = detailR * mask.r + detailG * mask.g + detailB * mask.b + detailA * mask.a; + return baseSample.rgb * blendedDetail * 2.0; +} + +float3 ShadeBakedFromMaterialLmap(MaterialData mat, float2 lmUV, float3 albedo, float hemi) +{ + if ((mat.flags & MAT_FLAG_HAS_LMAP) != 0 && mat.lmapIndex != INVALID_TEXTURE_INDEX) + { + float2 luv = (dot(lmUV, lmUV) > 1e-8) ? lmUV : float2(0.5, 0.5); + float3 L = GetBindlessTexture(mat.lmapIndex).SampleLevel(smp_linear, luv, 0).rgb; + return L * albedo; + } + return ShadeBakedFromHemi(hemi, albedo, g_HemiColor.rgb); +} + +float3 ImportanceSampleGGX(float2 Xi, float3 N, float roughness) +{ + float a = max(roughness * roughness, 0.001); + float phi = 6.2831853 * Xi.x; + float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y)); + float sinTheta = sqrt(max(1.0 - cosTheta * cosTheta, 0.0)); + float3 H = float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); + float3 up = abs(N.z) < 0.999 ? float3(0, 0, 1) : float3(1, 0, 0); + float3 tangent = normalize(cross(up, N)); + float3 bitangent = cross(N, tangent); + return normalize(tangent * H.x + bitangent * H.y + N * H.z); +} + +float3 TraceWaterReflect(float3 origin, float3 dir, float3 skyRefl, uint maxSteps) { float3 rayOrigin = origin; float remain = 8000.0; + maxSteps = clamp(maxSteps, 1u, 8u); [loop] - for (uint step = 0; step < 8; ++step) + for (uint step = 0; step < maxSteps; ++step) { if (remain <= 0.05) break; @@ -139,7 +206,10 @@ float3 TraceWaterReflect(float3 origin, float3 dir, float3 skyRefl) float3 albedo = float3(0.5, 0.5, 0.5); float3 hitN = float3(0.0, 1.0, 0.0); float hemi = 0.5; + float2 lmUV = 0; bool skipHit = false; + bool hasLmap = false; + float3 baked = 0; if (IsGrassBatch(batchIdx)) { @@ -158,6 +228,7 @@ float3 TraceWaterReflect(float3 origin, float3 dir, float3 skyRefl) { albedo = lerp(float3(0.08, 0.18, 0.03), float3(0.15, 0.35, 0.06), 1.0 - hitUV.y); } + baked = g_HemiColor.rgb * albedo * 0.45; } else if (IsSkinnedBatch(batchIdx)) { @@ -175,10 +246,26 @@ float3 TraceWaterReflect(float3 origin, float3 dir, float3 skyRefl) else albedo = diffuse.rgb; hemi = 0.55; + baked = ShadeBakedFromHemi(hemi, albedo, g_HemiColor.rgb); + if ((mat.flags & MAT_FLAG_EMISSIVE) && mat.emissiveIntensity > 0.0) + baked += albedo * mat.emissiveIntensity; + } + else if (IsTerrainBatch(batchIdx)) + { + float2 hitUV = GetHitUV(g_MegaVB, g_MegaIB, info, primIdx, bary); + lmUV = GetHitLightmapUV(g_MegaVB, g_MegaIB, info, primIdx, bary); + hemi = GetHitHemi(g_MegaVB, g_MegaIB, info, primIdx, bary); + hitN = normalize(TransformNormalToWorld( + GetHitNormal(g_MegaVB, g_MegaIB, info, primIdx, bary), objectToWorld)); + TerrainMaterialData tmat = g_TerrainMaterials[info.materialID]; + albedo = SampleTerrainAlbedoWater(tmat, hitUV, tHit); + baked = ShadeBakedFromTerrainLmap(tmat, lmUV, albedo, g_HemiColor.rgb, hemi); + hasLmap = (tmat.flags & MAT_FLAG_HAS_LMAP) != 0 && tmat.lmapIndex != INVALID_TEXTURE_INDEX; } else { float2 hitUV = GetHitUV(g_MegaVB, g_MegaIB, info, primIdx, bary); + lmUV = GetHitLightmapUV(g_MegaVB, g_MegaIB, info, primIdx, bary); hemi = GetHitHemi(g_MegaVB, g_MegaIB, info, primIdx, bary); hitN = normalize(TransformNormalToWorld( GetHitNormal(g_MegaVB, g_MegaIB, info, primIdx, bary), objectToWorld)); @@ -192,6 +279,10 @@ float3 TraceWaterReflect(float3 origin, float3 dir, float3 skyRefl) skipHit = true; else albedo = diffuse.rgb; + baked = ShadeBakedFromMaterialLmap(mat, lmUV, albedo, hemi); + hasLmap = (mat.flags & MAT_FLAG_HAS_LMAP) != 0 && mat.lmapIndex != INVALID_TEXTURE_INDEX; + if ((mat.flags & MAT_FLAG_EMISSIVE) && mat.emissiveIntensity > 0.0) + baked += albedo * mat.emissiveIntensity; } if (skipHit) @@ -209,15 +300,44 @@ float3 TraceWaterReflect(float3 origin, float3 dir, float3 skyRefl) float sunI = saturate(g_SunDir_Intensity.w); float3 sunCol = max(g_SunColor_SkyWeight.rgb, float3(0.25, 0.25, 0.25)) * sunI; float ndl = saturate(dot(hitN, sunDir)); - float3 baked = ShadeBakedFromHemi(hemi, albedo, g_HemiColor.rgb); - float3 hitCol = baked + albedo * sunCol * (0.35 + 0.65 * ndl); - hitCol = max(hitCol, albedo * 0.2); - return lerp(hitCol, skyRefl, 0.08); + float3 hitCol = baked; + if (!hasLmap) + hitCol += albedo * sunCol * ndl * 0.85; + else + hitCol += albedo * sunCol * ndl * 0.12; + hitCol += SampleSkyW(hitN) * albedo * 0.08; + return lerp(hitCol, skyRefl, 0.05); } return skyRefl; } +float3 TraceWaterReflectGlossy(float3 origin, float3 N, float3 V, uint maxSteps, uint spp, inout uint rng) +{ + float3 R = reflect(-V, N); + float rough = 0.045; + spp = clamp(spp, 1u, 2u); + float3 acc = 0; + float wSum = 0; + [loop] + for (uint i = 0; i < spp; ++i) + { + float2 xi = float2(rand_float(rng), rand_float(rng)); + float3 H = ImportanceSampleGGX(xi, N, rough); + float3 L = normalize(reflect(-V, H)); + if (dot(L, N) <= 0.0) + L = R; + float3 skyRefl = SampleSkyW(L); + float3 c = TraceWaterReflect(origin, L, skyRefl, maxSteps); + float w = max(dot(L, N), 0.05); + acc += c * w; + wSum += w; + } + float3 glossy = acc / max(wSum, 1e-3); + float3 mirrorSky = SampleSkyW(R); + return lerp(glossy, mirrorSky, 0.02); +} + [numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) { @@ -236,7 +356,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) } float depth = t_Depth.Load(int3(pixel, 0)); - if (depth >= 1.0) + if (depth <= 0.0) { u_SceneColor[pixel] = under4; return; @@ -244,6 +364,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float3 waterPos = classifyData.xyz; float waterDist = length(waterPos - g_CameraPos.xyz); + float lodDist = max(g_CameraPos.w, 10.0); float4 underWP = t_UnderWorldPos.Load(int3(pixel, 0)); float waterDepth = 8.0; @@ -262,20 +383,30 @@ void main(uint3 dispatchID : SV_DispatchThreadID) } float3 N = DecodeNormal(t_Normal.Load(int3(pixel, 0)).xyz); - N = normalize(lerp(float3(0.0, 1.0, 0.0), float3(N.x, abs(N.y) + 0.05, N.z), 0.35)); + N = normalize(lerp(float3(0.0, 1.0, 0.0), float3(N.x, abs(N.y) + 0.05, N.z), 0.2)); float3 V = normalize(g_CameraPos.xyz - waterPos); float3 R = normalize(reflect(-V, N)); float F = FresnelSchlickWater(abs(dot(N, V))); float3 skyRefl = SampleSkyW(R); - float3 reflected = TraceWaterReflect(waterPos + N * 0.08, R, skyRefl); + float3 reflected = skyRefl; + float lod = saturate(waterDist / lodDist); + if (lod < 0.95) + { + uint steps = (lod < 0.4) ? 3u : ((lod < 0.7) ? 2u : 1u); + uint spp = (lod < 0.5) ? 2u : 1u; + uint rng = pcg_hash(pixel.x + pixel.y * 7919u + asuint(waterPos.x) * 97u); + reflected = TraceWaterReflectGlossy(waterPos + N * 0.08, N, V, steps, spp, rng); + } float3 sunDir = normalize(-g_SunDir_Intensity.xyz); float3 sunColor = g_SunColor_SkyWeight.rgb * saturate(g_SunDir_Intensity.w); reflected += sunColor * pow(saturate(dot(R, sunDir)), 384.0) * 0.7; - float reflA = saturate(0.45 + 0.45 * F) * edgeFade; - float3 color = lerp(under4.rgb, reflected, reflA); + float reflA = saturate(0.5 + 0.45 * F) * edgeFade; + float3 body = t_BaseColor.Load(int3(pixel, 0)).rgb; + float3 under = lerp(body * float3(0.45, 0.55, 0.5), under4.rgb, 0.2); + float3 color = lerp(under, reflected, reflA); u_SceneColor[pixel] = float4(color, under4.a); } diff --git a/res/gamedata/shaders/r5/shared/waterconfig.h b/res/gamedata/shaders/r5/shared/waterconfig.h index 89d1e220053..56424cdd4de 100644 --- a/res/gamedata/shaders/r5/shared/waterconfig.h +++ b/res/gamedata/shaders/r5/shared/waterconfig.h @@ -1,7 +1,7 @@ #ifndef _WATERCONFIG_H #define _WATERCONFIG_H -// : (1) +//настройки для: (1) //waterdistortion //waterdistortion2 @@ -11,7 +11,7 @@ #define W_DISTORT_BASE_TILE_1 (1.1f) //(1.1f) #define W_DISTORT_AMP_0 (+0.15f) //(+0.15f) #define W_DISTORT_AMP_1 (+0.55f) //(-0.30f) -#define W_DISTORT_POWER (1.0f) //(1.0f) +#define W_DISTORT_POWER (5.0f) #endif @@ -20,7 +20,7 @@ //////////////////////////////////////////////////////////////////////////////// -- waters clear //////////////////////////////////////////////////////////////////////////////// - : +настройки для: waterdistortion waterdistortion2 //////////////////////////////////////////////////////////////////////////////// @@ -32,7 +32,7 @@ #define W_DISTORT_AMP_1 (-1.75f) //(-0.30f) #define W_DISTORT_POWER (1.0f) //(1.0f) //////////////////////////////////////////////////////////////////////////////// - : +настройки для: waterdistortion waterdistortion //////////////////////////////////////////////////////////////////////////////// diff --git a/res/gamedata/shaders/r5/water.ps b/res/gamedata/shaders/r5/water.ps index 68d62bc1f2de77eb62f6d0b7fea28f15485b8688..d1c1450aed5e6fcb3980a756adf10d4f178c9537 100644 GIT binary patch delta 433 zcmZ9IF;2rU7=(}?5-}4Xp3^oT|d*jgoB@jonn<;L;k4}%{)b8S}n_2trjVF(ysues4 z!9l90;=baAf-x+aWSp}m(r^u79A#HNm{ZIFo&9mx?cUl$|9q{P$|cf5^4XoJ2~0JC zyn4o+&T8jeFa_F}*G#RX;Za@7NcWK;EL?<=H4bQAUR~fmXR+H=Y5WYiMl+$1*^us? zAu{C?dTEuTBy-;ok0`~MK$3@P-^@KaSx8YbV`u|h?ELoyx>#oJxZocV+J;~o8#&MA a5}7Np0NK~o5I_>88E!X&ldlhd>-_@je0k&m delta 243 zcmYk0K?=e!5Jkb2wc=KAHMmM8gfwkc&>p~zc!huwglbD#ZLP=1MMN*=O5EurZk)wm z%>4Q9-zOh(>v(Yr2iXvHJX6?ZxvV0>Stux=#!ja}u;>8}dSH@*V8Xn+%R!Xbb2PUa z3sZRM>gs&f`-_5gS!zh(nCpYg_N^kB(gj)9e=JTRWGI~Rz(pm Date: Thu, 30 Jul 2026 00:01:28 +0300 Subject: [PATCH 256/261] xrRender: fix DLAA with RT and tighten DLSS feature recreate --- .../xrRender/Upscaling/DlssFgPassSetup.cpp | 2 +- src/Layers/xrRender/Upscaling/NgxDLSS.cpp | 162 +++++++++++------- .../xrRender/Upscaling/StreamlineDLSS.h | 2 + .../xrRender/Upscaling/UpscalePassSetup.cpp | 5 + .../xrRender/Upscaling/UpscaleState.cpp | 12 +- src/Layers/xrRender/Upscaling/UpscaleState.h | 2 +- 6 files changed, 119 insertions(+), 66 deletions(-) diff --git a/src/Layers/xrRender/Upscaling/DlssFgPassSetup.cpp b/src/Layers/xrRender/Upscaling/DlssFgPassSetup.cpp index 6b7fb15e660..2429730577f 100644 --- a/src/Layers/xrRender/Upscaling/DlssFgPassSetup.cpp +++ b/src/Layers/xrRender/Upscaling/DlssFgPassSetup.cpp @@ -131,7 +131,7 @@ void setupDlssFgPass( in.cameraRight[0] = right.x; in.cameraRight[1] = right.y; in.cameraRight[2] = right.z; - in.cameraNear = VIEWPORT_NEAR; + in.cameraNear = RENDER_VIEWPORT_NEAR; in.cameraFar = g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.far_plane : 600.f; in.cameraFOV = deg2rad(Device.fFOV); in.cameraAspect = (data.displayH > 0) ? ((float)data.displayW / (float)data.displayH) : 1.f; diff --git a/src/Layers/xrRender/Upscaling/NgxDLSS.cpp b/src/Layers/xrRender/Upscaling/NgxDLSS.cpp index ba9bca0f834..cb336b3371f 100644 --- a/src/Layers/xrRender/Upscaling/NgxDLSS.cpp +++ b/src/Layers/xrRender/Upscaling/NgxDLSS.cpp @@ -47,6 +47,9 @@ u32 g_featW = 0, g_featH = 0, g_outW = 0, g_outH = 0; int g_featQuality = -1; int g_featCreateFlags = -1; bool g_featRR = false; +bool g_featNeedsSubmit = false; +bool g_featRecreated = false; +bool g_featForceSR = false; u32 g_fgW = 0, g_fgH = 0, g_fgRenderW = 0, g_fgRenderH = 0; u32 g_fgFormat = 0; nvrhi::TextureHandle g_fgInterp; @@ -198,10 +201,18 @@ bool MakeResourceVK(nvrhi::ITexture* tex, bool readWrite, NVSDK_NGX_Resource_VK& return true; } +void WaitGpuForNgx() +{ + if (GEnv.Backend) + GEnv.Backend->WaitForIdle(); +} + void DestroyFgFeature() { - if (g_fgHandle && g_params) + const bool hadFg = g_fgHandle != nullptr; + if (hadFg && g_params) { + WaitGpuForNgx(); NVSDK_NGX_VULKAN_ReleaseFeature(g_fgHandle); g_fgHandle = nullptr; } @@ -214,20 +225,26 @@ void DestroyFgFeature() void DestroyFeatures() { - if (g_dlssHandle && g_params) - { - NVSDK_NGX_VULKAN_ReleaseFeature(g_dlssHandle); - g_dlssHandle = nullptr; - } - if (g_rrHandle && g_params) + const bool hadFeat = g_dlssHandle || g_rrHandle; + if (hadFeat && g_params) { - NVSDK_NGX_VULKAN_ReleaseFeature(g_rrHandle); - g_rrHandle = nullptr; + WaitGpuForNgx(); + if (g_dlssHandle) + { + NVSDK_NGX_VULKAN_ReleaseFeature(g_dlssHandle); + g_dlssHandle = nullptr; + } + if (g_rrHandle) + { + NVSDK_NGX_VULKAN_ReleaseFeature(g_rrHandle); + g_rrHandle = nullptr; + } } g_featW = g_featH = g_outW = g_outH = 0; g_featQuality = -1; g_featCreateFlags = -1; g_featRR = false; + g_featNeedsSubmit = false; } void CopyTo44(float dst[4][4], const float src[16]) @@ -243,7 +260,9 @@ bool EnsureFeature(nvrhi::ICommandList* cmd, const UpscaleInputs& inputs, bool w return false; int createFlags = NVSDK_NGX_DLSS_Feature_Flags_IsHDR | - NVSDK_NGX_DLSS_Feature_Flags_MVLowRes; + NVSDK_NGX_DLSS_Feature_Flags_DepthInverted; + if (inputs.renderWidth < inputs.displayWidth || inputs.renderHeight < inputs.displayHeight) + createFlags |= NVSDK_NGX_DLSS_Feature_Flags_MVLowRes; if (ps_r_dlss_auto_exposure) createFlags |= NVSDK_NGX_DLSS_Feature_Flags_AutoExposure; @@ -309,6 +328,8 @@ bool EnsureFeature(nvrhi::ICommandList* cmd, const UpscaleInputs& inputs, bool w g_featQuality = ps_r_dlss_quality; g_featCreateFlags = createFlags; g_featRR = wantRR && g_rrHandle != nullptr; + g_featNeedsSubmit = true; + g_featRecreated = true; return true; } @@ -452,6 +473,8 @@ void Streamline_Shutdown() { DestroyFgFeature(); DestroyFeatures(); + if (g_params || g_ngxReady) + WaitGpuForNgx(); if (g_params) { NVSDK_NGX_VULKAN_DestroyParameters(g_params); @@ -465,12 +488,28 @@ void Streamline_Shutdown() } g_ngxReady = false; g_dlssAvailable = g_rrAvailable = g_fgAvailable = false; + g_featRecreated = false; + g_featForceSR = false; } bool Streamline_IsDLSSAvailable() { return g_dlssAvailable; } bool Streamline_IsFGAvailable() { return g_fgAvailable; } bool Streamline_IsRRAvailable() { return g_rrAvailable; } +bool Streamline_ConsumeFeatureReset() +{ + const bool r = g_featRecreated; + g_featRecreated = false; + return r; +} + +void Streamline_ReleaseFeatures() +{ + DestroyFgFeature(); + DestroyFeatures(); + g_featForceSR = false; +} + bool EvaluateDLSSSR(VkCommandBuffer vkCmd, const UpscaleInputs& inputs, NVSDK_NGX_Resource_VK& color, NVSDK_NGX_Resource_VK& depth, NVSDK_NGX_Resource_VK& mv, NVSDK_NGX_Resource_VK& out, NVSDK_NGX_Resource_VK* exposure) @@ -505,18 +544,25 @@ bool Streamline_EvaluateDLSS(nvrhi::ICommandList* cmd, const UpscaleInputs& inpu if (!g_ngxReady || !cmd || !inputs.color || !inputs.output || !inputs.depth || !inputs.motionVectors) return false; - const bool wantRR = inputs.enableRR && g_rrAvailable && + bool wantRR = !g_featForceSR && inputs.enableRR && g_rrAvailable && inputs.normals && inputs.diffuseAlbedo && inputs.specularAlbedo; if (!EnsureFeature(cmd, inputs, wantRR)) return false; + UpscaleInputs evalIn = inputs; + if (g_featNeedsSubmit) + { + g_featNeedsSubmit = false; + evalIn.reset = true; + } + wantRR = g_featRR && wantRR; NVSDK_NGX_Resource_VK color{}, depth{}, mv{}, out{}, exposure{}; - if (!MakeResourceVK(inputs.color, false, color) || - !MakeResourceVK(inputs.depth, false, depth) || - !MakeResourceVK(inputs.motionVectors, false, mv) || - !MakeResourceVK(inputs.output, true, out)) + if (!MakeResourceVK(evalIn.color, false, color) || + !MakeResourceVK(evalIn.depth, false, depth) || + !MakeResourceVK(evalIn.motionVectors, false, mv) || + !MakeResourceVK(evalIn.output, true, out)) return false; - const bool hasExposure = inputs.exposure && MakeResourceVK(inputs.exposure, false, exposure); + const bool hasExposure = evalIn.exposure && MakeResourceVK(evalIn.exposure, false, exposure); auto* vkCmd = (VkCommandBuffer)cmd->getNativeObject(nvrhi::ObjectTypes::VK_CommandBuffer).integer; if (!vkCmd) @@ -526,16 +572,16 @@ bool Streamline_EvaluateDLSS(nvrhi::ICommandList* cmd, const UpscaleInputs& inpu if (g_featRR && g_rrHandle && wantRR) { NVSDK_NGX_Resource_VK normals{}, diffAlb{}, specAlb{}, diffHit{}, specHit{}; - if (!MakeResourceVK(inputs.normals, false, normals) || - !MakeResourceVK(inputs.diffuseAlbedo, false, diffAlb) || - !MakeResourceVK(inputs.specularAlbedo, false, specAlb)) + if (!MakeResourceVK(evalIn.normals, false, normals) || + !MakeResourceVK(evalIn.diffuseAlbedo, false, diffAlb) || + !MakeResourceVK(evalIn.specularAlbedo, false, specAlb)) { Msg("! [Upscale] DLSS-RR missing G-buffer — falling back to SR"); } else { - const bool hasDiffHit = inputs.diffuseHitDistance && MakeResourceVK(inputs.diffuseHitDistance, false, diffHit); - const bool hasSpecHit = inputs.specularHitDistance && MakeResourceVK(inputs.specularHitDistance, false, specHit); + const bool hasDiffHit = evalIn.diffuseHitDistance && MakeResourceVK(evalIn.diffuseHitDistance, false, diffHit); + const bool hasSpecHit = evalIn.specularHitDistance && MakeResourceVK(evalIn.specularHitDistance, false, specHit); NVSDK_NGX_VK_DLSSD_Eval_Params eval{}; eval.pInColor = &color; @@ -550,15 +596,15 @@ bool Streamline_EvaluateDLSS(nvrhi::ICommandList* cmd, const UpscaleInputs& inpu eval.pInDiffuseHitDistance = &diffHit; if (hasSpecHit) eval.pInSpecularHitDistance = &specHit; - eval.pInWorldToViewMatrix = const_cast(inputs.worldToView); - eval.pInViewToClipMatrix = const_cast(inputs.viewToClip); - eval.InJitterOffsetX = inputs.jitterX; - eval.InJitterOffsetY = inputs.jitterY; - eval.InRenderSubrectDimensions = { inputs.renderWidth, inputs.renderHeight }; - eval.InReset = inputs.reset ? 1 : 0; - eval.InMVScaleX = (float)inputs.renderWidth; - eval.InMVScaleY = (float)inputs.renderHeight; - eval.InFrameTimeDeltaInMsec = inputs.frameTimeMs; + eval.pInWorldToViewMatrix = const_cast(evalIn.worldToView); + eval.pInViewToClipMatrix = const_cast(evalIn.viewToClip); + eval.InJitterOffsetX = evalIn.jitterX; + eval.InJitterOffsetY = evalIn.jitterY; + eval.InRenderSubrectDimensions = { evalIn.renderWidth, evalIn.renderHeight }; + eval.InReset = evalIn.reset ? 1 : 0; + eval.InMVScaleX = (float)evalIn.renderWidth; + eval.InMVScaleY = (float)evalIn.renderHeight; + eval.InFrameTimeDeltaInMsec = evalIn.frameTimeMs; eval.InPreExposure = 1.f; eval.InExposureScale = 1.f; if (hasExposure) @@ -577,16 +623,14 @@ bool Streamline_EvaluateDLSS(nvrhi::ICommandList* cmd, const UpscaleInputs& inpu } else { - Msg("! [Upscale] DLSS-RR Evaluate failed 0x%08x — falling back to SR", (u32)res); - DestroyFeatures(); - if (!EnsureFeature(cmd, inputs, false)) - return false; + Msg("! [Upscale] DLSS-RR Evaluate failed 0x%08x — falling back to SR next frame", (u32)res); + g_featForceSR = true; } } } - if (!ok) - ok = EvaluateDLSSSR(vkCmd, inputs, color, depth, mv, out, hasExposure ? &exposure : nullptr); + if (!ok && g_dlssHandle) + ok = EvaluateDLSSSR(vkCmd, evalIn, color, depth, mv, out, hasExposure ? &exposure : nullptr); return ok; } @@ -684,10 +728,23 @@ bool Streamline_EvaluateDLSSG(nvrhi::ICommandList* cmd, const DlssFgInputs& inpu const bool hasHudless = inputs.hudless && MakeResourceVK(inputs.hudless, false, hudless); + cmd->setTextureState(inputs.backbuffer, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(inputs.depth, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(inputs.motionVectors, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + if (hasHudless) + cmd->setTextureState(inputs.hudless, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + cmd->setTextureState(g_fgInterp, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->setTextureState(g_fgReal, nvrhi::AllSubresources, nvrhi::ResourceStates::UnorderedAccess); + cmd->commitBarriers(); + auto* vkCmd = (VkCommandBuffer)cmd->getNativeObject(nvrhi::ObjectTypes::VK_CommandBuffer).integer; if (!vkCmd || !g_fgHandle) return false; + NVSDK_NGX_Parameter_SetUI(g_params, "DLSSG.EnableInterp", 1); + NVSDK_NGX_Parameter_SetI(g_params, "DLSSG.NumFrames", 1); + NVSDK_NGX_Parameter_SetUI(g_params, "DLSSG.IsRecording", 1); + NVSDK_NGX_VK_DLSSG_Eval_Params eval{}; eval.pBackbuffer = &color; eval.pDepth = &depth; @@ -727,8 +784,13 @@ bool Streamline_EvaluateDLSSG(nvrhi::ICommandList* cmd, const DlssFgInputs& inpu opt.cameraFar = inputs.cameraFar; opt.cameraFOV = inputs.cameraFOV; opt.cameraAspectRatio = inputs.cameraAspect; - opt.colorBuffersHDR = false; - opt.depthInverted = false; + { + const auto fmt = inputs.backbuffer->getDesc().format; + opt.colorBuffersHDR = + fmt == nvrhi::Format::RGBA16_FLOAT || fmt == nvrhi::Format::RGBA32_FLOAT || + fmt == nvrhi::Format::RGB32_FLOAT || fmt == nvrhi::Format::RG16_FLOAT; + } + opt.depthInverted = true; opt.cameraMotionIncluded = true; opt.reset = inputs.reset; opt.motionVectorsDilated = false; @@ -739,33 +801,13 @@ bool Streamline_EvaluateDLSSG(nvrhi::ICommandList* cmd, const DlssFgInputs& inpu opt.hudLessSubrectSize = { inputs.displayWidth, inputs.displayHeight }; const NVSDK_NGX_Result res = NGX_VK_EVALUATE_DLSSG(vkCmd, g_fgHandle, g_params, &eval, &opt); + cmd->clearState(); if (NVSDK_NGX_FAILED(res)) { Msg("! [Upscale] DLSS-FG Evaluate failed 0x%08x", (u32)res); return false; } - if (auto* bb = GEnv.Backend ? GEnv.Backend->GetBackBuffer() : nullptr) - { - if (bb->getDesc().width == g_fgW && bb->getDesc().height == g_fgH && - bb->getDesc().format == g_fgReal->getDesc().format) - { - nvrhi::TextureSlice slice; - cmd->copyTexture(bb, slice, g_fgReal, slice); - } - else - { - static bool s_fmtLogged = false; - if (!s_fmtLogged) - { - s_fmtLogged = true; - Msg("! [Upscale] DLSS-FG blit skipped — BB %ux%u fmt=%u vs FG %ux%u fmt=%u", - bb->getDesc().width, bb->getDesc().height, (u32)bb->getDesc().format, - g_fgW, g_fgH, (u32)g_fgReal->getDesc().format); - } - } - } - g_fgPresentPending = true; static bool s_fgOk = false; if (!s_fgOk) @@ -800,6 +842,8 @@ void Streamline_Shutdown() {} bool Streamline_IsDLSSAvailable() { return false; } bool Streamline_IsFGAvailable() { return false; } bool Streamline_IsRRAvailable() { return false; } +bool Streamline_ConsumeFeatureReset() { return false; } +void Streamline_ReleaseFeatures() {} bool Streamline_EvaluateDLSS(nvrhi::ICommandList*, const UpscaleInputs&) { return false; } bool Streamline_EvaluateDLSSRR(nvrhi::ICommandList*, const UpscaleInputs&) { return false; } bool Streamline_EvaluateDLSSG(nvrhi::ICommandList*, const DlssFgInputs&) { return false; } diff --git a/src/Layers/xrRender/Upscaling/StreamlineDLSS.h b/src/Layers/xrRender/Upscaling/StreamlineDLSS.h index 2b2d8e0e4d4..da3109ba238 100644 --- a/src/Layers/xrRender/Upscaling/StreamlineDLSS.h +++ b/src/Layers/xrRender/Upscaling/StreamlineDLSS.h @@ -54,6 +54,8 @@ void Streamline_Shutdown(); bool Streamline_IsDLSSAvailable(); bool Streamline_IsFGAvailable(); bool Streamline_IsRRAvailable(); +bool Streamline_ConsumeFeatureReset(); +void Streamline_ReleaseFeatures(); bool Streamline_EvaluateDLSS(nvrhi::ICommandList* cmd, const UpscaleInputs& inputs); bool Streamline_EvaluateDLSSRR(nvrhi::ICommandList* cmd, const UpscaleInputs& inputs); diff --git a/src/Layers/xrRender/Upscaling/UpscalePassSetup.cpp b/src/Layers/xrRender/Upscaling/UpscalePassSetup.cpp index 0e46402a047..2ba6fb903ec 100644 --- a/src/Layers/xrRender/Upscaling/UpscalePassSetup.cpp +++ b/src/Layers/xrRender/Upscaling/UpscalePassSetup.cpp @@ -343,6 +343,11 @@ framegraph::VirtualResourceHandle setupUpscaleOrResolvePass( nvrhi::TextureSlice slice; cmd->copyTexture(dst, slice, src, slice); } + else + { + cmd->clearTextureFloat(dst, nvrhi::TextureSubresourceSet(0, 1, 0, 1), + nvrhi::Color(0.f)); + } } }); diff --git a/src/Layers/xrRender/Upscaling/UpscaleState.cpp b/src/Layers/xrRender/Upscaling/UpscaleState.cpp index ff4b5ffd01d..e44fa1fb313 100644 --- a/src/Layers/xrRender/Upscaling/UpscaleState.cpp +++ b/src/Layers/xrRender/Upscaling/UpscaleState.cpp @@ -26,9 +26,9 @@ float DlssQualityScale(int quality) } -void UpdateUpscaleState(UpscaleState& state, u32 displayW, u32 displayH) +void UpdateUpscaleState(UpscaleState& state, u32 displayW, u32 displayH, bool backendAvailable) { - if (ps_r_dlss != 0 && ps_r_upscale == 0) + if (backendAvailable && ps_r_dlss != 0 && ps_r_upscale == 0) ps_r_upscale = 2; state.displayWidth = std::max(1u, displayW); @@ -39,15 +39,17 @@ void UpdateUpscaleState(UpscaleState& state, u32 displayW, u32 displayH) scale = 1.0f; scale = std::clamp(scale, 0.25f, 1.0f); - if (ps_r_upscale == 0) + if (ps_r_upscale == 0 || !backendAvailable) scale = 1.0f; - else if (ps_r_upscale == 2 && scale >= 0.999f && ps_r_dlss_quality != 5) + else if (ps_r_upscale == 2 && ps_r_dlss_quality == 5) + scale = 1.0f; + else if (ps_r_upscale == 2 && scale >= 0.999f) scale = DlssQualityScale(ps_r_dlss_quality); state.renderScale = scale; state.renderWidth = std::max(1u, (u32)std::lround(float(state.displayWidth) * scale)); state.renderHeight = std::max(1u, (u32)std::lround(float(state.displayHeight) * scale)); - state.upscaleActive = ps_r_upscale != 0 && + state.upscaleActive = backendAvailable && ps_r_upscale != 0 && (state.renderWidth != state.displayWidth || state.renderHeight != state.displayHeight || ps_r_upscale == 2); diff --git a/src/Layers/xrRender/Upscaling/UpscaleState.h b/src/Layers/xrRender/Upscaling/UpscaleState.h index 9e43e6730b2..a08709764e2 100644 --- a/src/Layers/xrRender/Upscaling/UpscaleState.h +++ b/src/Layers/xrRender/Upscaling/UpscaleState.h @@ -19,7 +19,7 @@ struct UpscaleState bool resetHistory = false; }; -void UpdateUpscaleState(UpscaleState& state, u32 displayW, u32 displayH); +void UpdateUpscaleState(UpscaleState& state, u32 displayW, u32 displayH, bool backendAvailable = true); inline bool NeedsResolveToDisplay(const UpscaleState& state) { From 2e5a43513d0aabe30febe4b24fcb9133def6ba0a Mon Sep 17 00:00:00 2001 From: FoxConED Date: Thu, 30 Jul 2026 00:01:28 +0300 Subject: [PATCH 257/261] xrRender: add skinned velocity shaders and every-frame BLAS --- .../r5/bindless_skinned_2w_mdi_velocity.vs | 58 +++ .../r5/bindless_skinned_2w_velocity.vs | 26 + .../r5/bindless_skinned_3w_mdi_velocity.vs | 66 +++ .../r5/bindless_skinned_3w_velocity.vs | 30 ++ .../r5/bindless_skinned_4w_mdi_velocity.vs | 71 +++ .../r5/bindless_skinned_4w_velocity.vs | 35 ++ .../r5/bindless_skinned_hq_mdi_velocity.vs | 54 ++ .../r5/bindless_skinned_hq_velocity.vs | 26 + .../r5/bindless_skinned_mdi_velocity.vs | 55 ++ .../shaders/r5/bindless_skinned_velocity.ps | Bin 0 -> 172 bytes .../shaders/r5/bindless_skinned_velocity.vs | 32 ++ res/gamedata/shaders/r5/rt_skin_vertices.cs | 4 +- .../shaders/r5/skinned_velocity_common.h | 63 +++ .../MotionVectorPassSetup.cpp | 14 +- .../FrameGraphPasses/SkinningPassSetup.cpp | 484 +++++++++++++++++- .../FrameGraphPasses/SkinningPassSetup.h | 28 + .../RayTracing/RTAccelStructManager.cpp | 230 ++++++++- .../RayTracing/RTAccelStructManager.h | 32 ++ src/Layers/xrRender/SkeletonCustom.h | 6 + 19 files changed, 1259 insertions(+), 55 deletions(-) create mode 100644 res/gamedata/shaders/r5/bindless_skinned_2w_mdi_velocity.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_2w_velocity.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_3w_mdi_velocity.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_3w_velocity.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_4w_mdi_velocity.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_4w_velocity.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_hq_mdi_velocity.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_hq_velocity.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_mdi_velocity.vs create mode 100644 res/gamedata/shaders/r5/bindless_skinned_velocity.ps create mode 100644 res/gamedata/shaders/r5/bindless_skinned_velocity.vs create mode 100644 res/gamedata/shaders/r5/skinned_velocity_common.h diff --git a/res/gamedata/shaders/r5/bindless_skinned_2w_mdi_velocity.vs b/res/gamedata/shaders/r5/bindless_skinned_2w_mdi_velocity.vs new file mode 100644 index 00000000000..fb03bfdc102 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_2w_mdi_velocity.vs @@ -0,0 +1,58 @@ +#define SM_6_0 + + +#include "skinned_velocity_common.h" + +struct SkinnedDrawRecord +{ + float4x4 world; + uint boneOffset; + uint splatOffset; + uint splatCount; + uint pad; + float4x4 prevWorld; + uint prevBoneOffset; + uint prevValid; + uint pad1; + uint pad2; +}; + +StructuredBuffer g_SkinnedRecords : register(t14); +StructuredBuffer g_SkinnedCompactIndices : register(t15); + +struct VS_INPUT +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float4 tc : TEXCOORD0; + uint drawIndex : DRAWINDEX; +}; + +float4x4 mdi_curr_bone(uint boneOffset, int legacy_index) +{ + return g_BoneMatrices[boneOffset + (legacy_index / 3)]; +} + +float4x4 mdi_prev_bone(SkinnedDrawRecord rec, int legacy_index) +{ + if (rec.prevValid) + return g_PrevBoneMatrices[rec.prevBoneOffset + (legacy_index / 3)]; + return g_BoneMatrices[rec.boneOffset + (legacy_index / 3)]; +} + +VelocityVSOut main(VS_INPUT v) +{ + SkinnedDrawRecord rec = g_SkinnedRecords[g_SkinnedCompactIndices[v.drawIndex]]; + int id_0 = int(v.tc.z); + int id_1 = int(v.tc.w); + float w = v.N.w; + float4x4 bone = lerp(mdi_curr_bone(rec.boneOffset, id_0), mdi_curr_bone(rec.boneOffset, id_1), w); + float4x4 prevBone = lerp(mdi_prev_bone(rec, id_0), mdi_prev_bone(rec, id_1), w); + + float3 currWorld = mul(rec.world, skinning_pos(v.P, bone)).xyz; + float4x4 prevW = rec.prevValid ? rec.prevWorld : rec.world; + float3 prevWorld = mul(prevW, skinning_pos(v.P, prevBone)).xyz; + return EmitVelocity(currWorld, prevWorld); +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_2w_velocity.vs b/res/gamedata/shaders/r5/bindless_skinned_2w_velocity.vs new file mode 100644 index 00000000000..3a17d336836 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_2w_velocity.vs @@ -0,0 +1,26 @@ +#define SM_6_0 + + +#include "skinned_velocity_common.h" + +struct VS_INPUT +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float4 tc : TEXCOORD0; +}; + +VelocityVSOut main(VS_INPUT v) +{ + int id_0 = int(v.tc.z); + int id_1 = int(v.tc.w); + float w = v.N.w; + float4x4 bone = lerp(get_curr_bone(id_0), get_curr_bone(id_1), w); + float4x4 prevBone = lerp(get_prev_bone(id_0), get_prev_bone(id_1), w); + + float3 currWorld = mul(vel_m_W, skinning_pos(v.P, bone)).xyz; + float3 prevWorld = mul(vel_m_W_prev, skinning_pos(v.P, prevBone)).xyz; + return EmitVelocity(currWorld, prevWorld); +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_3w_mdi_velocity.vs b/res/gamedata/shaders/r5/bindless_skinned_3w_mdi_velocity.vs new file mode 100644 index 00000000000..398f8b8ac3a --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_3w_mdi_velocity.vs @@ -0,0 +1,66 @@ +#define SM_6_0 + + +#include "skinned_velocity_common.h" + +struct SkinnedDrawRecord +{ + float4x4 world; + uint boneOffset; + uint splatOffset; + uint splatCount; + uint pad; + float4x4 prevWorld; + uint prevBoneOffset; + uint prevValid; + uint pad1; + uint pad2; +}; + +StructuredBuffer g_SkinnedRecords : register(t14); +StructuredBuffer g_SkinnedCompactIndices : register(t15); + +struct VS_INPUT +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float4 tc : TEXCOORD0; + uint drawIndex : DRAWINDEX; +}; + +float4x4 mdi_curr_bone(uint boneOffset, int legacy_index) +{ + return g_BoneMatrices[boneOffset + (legacy_index / 3)]; +} + +float4x4 mdi_prev_bone(SkinnedDrawRecord rec, int legacy_index) +{ + if (rec.prevValid) + return g_PrevBoneMatrices[rec.prevBoneOffset + (legacy_index / 3)]; + return g_BoneMatrices[rec.boneOffset + (legacy_index / 3)]; +} + +VelocityVSOut main(VS_INPUT v) +{ + SkinnedDrawRecord rec = g_SkinnedRecords[g_SkinnedCompactIndices[v.drawIndex]]; + int id_0 = int(v.tc.z); + int id_1 = int(v.tc.w); + int id_2 = int(v.B.w * 255.0 + 0.3); + float w0 = v.N.w; + float w1 = v.T.w; + float w2 = 1.0 - w0 - w1; + + float4x4 bone = mdi_curr_bone(rec.boneOffset, id_0) * w0 + + mdi_curr_bone(rec.boneOffset, id_1) * w1 + + mdi_curr_bone(rec.boneOffset, id_2) * w2; + float4x4 prevBone = mdi_prev_bone(rec, id_0) * w0 + + mdi_prev_bone(rec, id_1) * w1 + + mdi_prev_bone(rec, id_2) * w2; + + float3 currWorld = mul(rec.world, skinning_pos(v.P, bone)).xyz; + float4x4 prevW = rec.prevValid ? rec.prevWorld : rec.world; + float3 prevWorld = mul(prevW, skinning_pos(v.P, prevBone)).xyz; + return EmitVelocity(currWorld, prevWorld); +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_3w_velocity.vs b/res/gamedata/shaders/r5/bindless_skinned_3w_velocity.vs new file mode 100644 index 00000000000..88095809891 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_3w_velocity.vs @@ -0,0 +1,30 @@ +#define SM_6_0 + + +#include "skinned_velocity_common.h" + +struct VS_INPUT +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float4 tc : TEXCOORD0; +}; + +VelocityVSOut main(VS_INPUT v) +{ + int id_0 = int(v.tc.z); + int id_1 = int(v.tc.w); + int id_2 = int(v.B.w * 255.0 + 0.3); + float w0 = v.N.w; + float w1 = v.T.w; + float w2 = 1.0 - w0 - w1; + + float4x4 bone = get_curr_bone(id_0) * w0 + get_curr_bone(id_1) * w1 + get_curr_bone(id_2) * w2; + float4x4 prevBone = get_prev_bone(id_0) * w0 + get_prev_bone(id_1) * w1 + get_prev_bone(id_2) * w2; + + float3 currWorld = mul(vel_m_W, skinning_pos(v.P, bone)).xyz; + float3 prevWorld = mul(vel_m_W_prev, skinning_pos(v.P, prevBone)).xyz; + return EmitVelocity(currWorld, prevWorld); +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_4w_mdi_velocity.vs b/res/gamedata/shaders/r5/bindless_skinned_4w_mdi_velocity.vs new file mode 100644 index 00000000000..d9444fdecda --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_4w_mdi_velocity.vs @@ -0,0 +1,71 @@ +#define SM_6_0 + + +#include "skinned_velocity_common.h" + +struct SkinnedDrawRecord +{ + float4x4 world; + uint boneOffset; + uint splatOffset; + uint splatCount; + uint pad; + float4x4 prevWorld; + uint prevBoneOffset; + uint prevValid; + uint pad1; + uint pad2; +}; + +StructuredBuffer g_SkinnedRecords : register(t14); +StructuredBuffer g_SkinnedCompactIndices : register(t15); + +struct VS_INPUT +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; + float4 ind : BLENDINDICES; + uint drawIndex : DRAWINDEX; +}; + +float4x4 mdi_curr_bone(uint boneOffset, int legacy_index) +{ + return g_BoneMatrices[boneOffset + (legacy_index / 3)]; +} + +float4x4 mdi_prev_bone(SkinnedDrawRecord rec, int legacy_index) +{ + if (rec.prevValid) + return g_PrevBoneMatrices[rec.prevBoneOffset + (legacy_index / 3)]; + return g_BoneMatrices[rec.boneOffset + (legacy_index / 3)]; +} + +VelocityVSOut main(VS_INPUT v) +{ + SkinnedDrawRecord rec = g_SkinnedRecords[g_SkinnedCompactIndices[v.drawIndex]]; + float w0 = v.N.w; + float w1 = v.T.w; + float w2 = v.B.w; + float w3 = 1.0 - w0 - w1 - w2; + int id0 = int(v.ind.x * 255.0 + 0.3); + int id1 = int(v.ind.y * 255.0 + 0.3); + int id2 = int(v.ind.z * 255.0 + 0.3); + int id3 = int(v.ind.w * 255.0 + 0.3); + + float4x4 bone = mdi_curr_bone(rec.boneOffset, id0) * w0 + + mdi_curr_bone(rec.boneOffset, id1) * w1 + + mdi_curr_bone(rec.boneOffset, id2) * w2 + + mdi_curr_bone(rec.boneOffset, id3) * w3; + float4x4 prevBone = mdi_prev_bone(rec, id0) * w0 + + mdi_prev_bone(rec, id1) * w1 + + mdi_prev_bone(rec, id2) * w2 + + mdi_prev_bone(rec, id3) * w3; + + float3 currWorld = mul(rec.world, skinning_pos(v.P, bone)).xyz; + float4x4 prevW = rec.prevValid ? rec.prevWorld : rec.world; + float3 prevWorld = mul(prevW, skinning_pos(v.P, prevBone)).xyz; + return EmitVelocity(currWorld, prevWorld); +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_4w_velocity.vs b/res/gamedata/shaders/r5/bindless_skinned_4w_velocity.vs new file mode 100644 index 00000000000..33ddcbb3060 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_4w_velocity.vs @@ -0,0 +1,35 @@ +#define SM_6_0 + + +#include "skinned_velocity_common.h" + +struct VS_INPUT +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; + float4 ind : BLENDINDICES; +}; + +VelocityVSOut main(VS_INPUT v) +{ + float w0 = v.N.w; + float w1 = v.T.w; + float w2 = v.B.w; + float w3 = 1.0 - w0 - w1 - w2; + int id0 = int(v.ind.x * 255.0 + 0.3); + int id1 = int(v.ind.y * 255.0 + 0.3); + int id2 = int(v.ind.z * 255.0 + 0.3); + int id3 = int(v.ind.w * 255.0 + 0.3); + + float4x4 bone = get_curr_bone(id0) * w0 + get_curr_bone(id1) * w1 + + get_curr_bone(id2) * w2 + get_curr_bone(id3) * w3; + float4x4 prevBone = get_prev_bone(id0) * w0 + get_prev_bone(id1) * w1 + + get_prev_bone(id2) * w2 + get_prev_bone(id3) * w3; + + float3 currWorld = mul(vel_m_W, skinning_pos(v.P, bone)).xyz; + float3 prevWorld = mul(vel_m_W_prev, skinning_pos(v.P, prevBone)).xyz; + return EmitVelocity(currWorld, prevWorld); +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_hq_mdi_velocity.vs b/res/gamedata/shaders/r5/bindless_skinned_hq_mdi_velocity.vs new file mode 100644 index 00000000000..3b496dd12b3 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_hq_mdi_velocity.vs @@ -0,0 +1,54 @@ +#define SM_6_0 + + +#include "skinned_velocity_common.h" + +struct SkinnedDrawRecord +{ + float4x4 world; + uint boneOffset; + uint splatOffset; + uint splatCount; + uint pad; + float4x4 prevWorld; + uint prevBoneOffset; + uint prevValid; + uint pad1; + uint pad2; +}; + +StructuredBuffer g_SkinnedRecords : register(t14); +StructuredBuffer g_SkinnedCompactIndices : register(t15); + +struct VS_INPUT +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; + uint drawIndex : DRAWINDEX; +}; + +float4x4 mdi_curr_bone(uint boneOffset, int legacy_index) +{ + return g_BoneMatrices[boneOffset + (legacy_index / 3)]; +} + +float4x4 mdi_prev_bone(SkinnedDrawRecord rec, int legacy_index) +{ + if (rec.prevValid) + return g_PrevBoneMatrices[rec.prevBoneOffset + (legacy_index / 3)]; + return g_BoneMatrices[rec.boneOffset + (legacy_index / 3)]; +} + +VelocityVSOut main(VS_INPUT v) +{ + SkinnedDrawRecord rec = g_SkinnedRecords[g_SkinnedCompactIndices[v.drawIndex]]; + int boneIdx = int(v.N.w * 255.0 + 0.3); + + float3 currWorld = mul(rec.world, skinning_pos(v.P, mdi_curr_bone(rec.boneOffset, boneIdx))).xyz; + float4x4 prevW = rec.prevValid ? rec.prevWorld : rec.world; + float3 prevWorld = mul(prevW, skinning_pos(v.P, mdi_prev_bone(rec, boneIdx))).xyz; + return EmitVelocity(currWorld, prevWorld); +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_hq_velocity.vs b/res/gamedata/shaders/r5/bindless_skinned_hq_velocity.vs new file mode 100644 index 00000000000..379cbb8c597 --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_hq_velocity.vs @@ -0,0 +1,26 @@ +#define SM_6_0 + + +#include "skinned_velocity_common.h" + +struct VS_INPUT +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; +}; + +VelocityVSOut main(VS_INPUT input) +{ + int boneIdx = int(input.N.w * 255.0 + 0.3); + + float4 currSkinned = skinning_pos(input.P, get_curr_bone(boneIdx)); + float3 currWorld = mul(vel_m_W, currSkinned).xyz; + + float4 prevSkinned = skinning_pos(input.P, get_prev_bone(boneIdx)); + float3 prevWorld = mul(vel_m_W_prev, prevSkinned).xyz; + + return EmitVelocity(currWorld, prevWorld); +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_mdi_velocity.vs b/res/gamedata/shaders/r5/bindless_skinned_mdi_velocity.vs new file mode 100644 index 00000000000..3dec78b629b --- /dev/null +++ b/res/gamedata/shaders/r5/bindless_skinned_mdi_velocity.vs @@ -0,0 +1,55 @@ +#define SM_6_0 + + +#include "skinned_velocity_common.h" + +struct SkinnedDrawRecord +{ + float4x4 world; + uint boneOffset; + uint splatOffset; + uint splatCount; + uint pad; + float4x4 prevWorld; + uint prevBoneOffset; + uint prevValid; + uint pad1; + uint pad2; +}; + +StructuredBuffer g_SkinnedRecords : register(t14); +StructuredBuffer g_SkinnedCompactIndices : register(t15); + +struct VS_INPUT +{ + float4 P : POSITION; + float4 N : NORMAL; + float4 T : TANGENT; + float4 B : BINORMAL; + float2 tc : TEXCOORD0; + uint drawIndex : DRAWINDEX; +}; + +float4x4 mdi_curr_bone(uint boneOffset, int legacy_index) +{ + return g_BoneMatrices[boneOffset + (legacy_index / 3)]; +} + +float4x4 mdi_prev_bone(SkinnedDrawRecord rec, int legacy_index) +{ + if (rec.prevValid) + return g_PrevBoneMatrices[rec.prevBoneOffset + (legacy_index / 3)]; + return g_BoneMatrices[rec.boneOffset + (legacy_index / 3)]; +} + +VelocityVSOut main(VS_INPUT v) +{ + SkinnedDrawRecord rec = g_SkinnedRecords[g_SkinnedCompactIndices[v.drawIndex]]; + float4 localPos = float4(v.P.xyz * 12.0, 1.0); + int boneIdx = int(v.N.w * 255.0 + 0.3); + + float3 currWorld = mul(rec.world, skinning_pos(localPos, mdi_curr_bone(rec.boneOffset, boneIdx))).xyz; + float4x4 prevW = rec.prevValid ? rec.prevWorld : rec.world; + float3 prevWorld = mul(prevW, skinning_pos(localPos, mdi_prev_bone(rec, boneIdx))).xyz; + return EmitVelocity(currWorld, prevWorld); +} diff --git a/res/gamedata/shaders/r5/bindless_skinned_velocity.ps b/res/gamedata/shaders/r5/bindless_skinned_velocity.ps new file mode 100644 index 0000000000000000000000000000000000000000..b1c387278d120f115f63e0e6073509d61b768922 GIT binary patch literal 172 zcmZ|GF$=;l5C!1 g_BoneMatrices : register(t3); +StructuredBuffer g_PrevBoneMatrices : register(t4); + +cbuffer VelocityTransforms : register(b0) +{ + float4x4 vel_m_W; + float4x4 vel_m_W_prev; + float4x4 vel_m_VP; + float4x4 vel_m_VP_prev; +}; + +cbuffer VelocitySkinCB : register(b4) +{ + uint g_VelBoneOffset; + uint g_VelPrevBoneOffset; + uint g_VelHasPrev; + uint g_VelPad; +}; + +float4 skinning_pos(float4 pos, float4x4 bone) +{ + return mul(bone, pos); +} + +float4x4 get_curr_bone(int legacy_index) +{ + return g_BoneMatrices[g_VelBoneOffset + (legacy_index / 3)]; +} + +float4x4 get_prev_bone(int legacy_index) +{ + if (g_VelHasPrev) + return g_PrevBoneMatrices[g_VelPrevBoneOffset + (legacy_index / 3)]; + return g_BoneMatrices[g_VelBoneOffset + (legacy_index / 3)]; +} + +float2 VelocityUvFromClip(float4 clip) +{ + float2 ndc = clip.xy / max(clip.w, 1e-5); + ndc.y = -ndc.y; + return ndc * 0.5 + 0.5; +} + +struct VelocityVSOut +{ + float4 position : SV_Position; + float4 currClip : TEXCOORD0; + float4 prevClip : TEXCOORD1; +}; + +VelocityVSOut EmitVelocity(float3 currWorld, float3 prevWorld) +{ + VelocityVSOut o; + o.currClip = mul(vel_m_VP, float4(currWorld, 1.0)); + o.prevClip = mul(vel_m_VP_prev, float4(prevWorld, 1.0)); + o.position = o.currClip; + return o; +} + +#endif diff --git a/src/Layers/xrRender/FrameGraphPasses/MotionVectorPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/MotionVectorPassSetup.cpp index bb0cbb5b8b0..1ec7be8774e 100644 --- a/src/Layers/xrRender/FrameGraphPasses/MotionVectorPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/MotionVectorPassSetup.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "MotionVectorPassSetup.h" +#include "TAAPassSetup.h" #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/PassResourceCache.h" @@ -13,7 +14,7 @@ using namespace framegraph; namespace { -constexpr u32 kMotionVectorPipeVersion = 6; +constexpr u32 kMotionVectorPipeVersion = 10; } static void InitializeResources(fg::RenderDevice* device, MotionVectorPassState& state) @@ -36,14 +37,14 @@ static void InitializeResources(fg::RenderDevice* device, MotionVectorPassState& return; state.layout = cache.GetOrCreateBindingLayoutFromReflection( - "MotionVector_v6", *csResult.reflection, nvDevice); + "MotionVector_v10", *csResult.reflection, nvDevice); nvrhi::ComputePipelineDesc pipeDesc; pipeDesc.CS = csResult.handle; pipeDesc.bindingLayouts = { state.layout }; - state.pipeline = cache.GetOrCreateComputePipeline("MotionVector_v6", pipeDesc, nvDevice); + state.pipeline = cache.GetOrCreateComputePipeline("MotionVector_v10", pipeDesc, nvDevice); - state.cb = cache.GetOrCreateVolatileCB("MotionVector", "MotionVectorCB_v6", 256, device); + state.cb = cache.GetOrCreateVolatileCB("MotionVector", "MotionVectorCB_v10", 256, device); state.initialized = true; state.pipeVersion = kMotionVectorPipeVersion; @@ -71,6 +72,7 @@ MotionVectorOutput setupMotionVectorPass( mvDesc.height = height; mvDesc.format = nvrhi::Format::RG16_FLOAT; mvDesc.isUAV = true; + mvDesc.isRenderTarget = true; mvDesc.isTransient = true; auto mvHandle = fg.CreateTexture("rt_MotionVectors", mvDesc); @@ -88,6 +90,7 @@ MotionVectorOutput setupMotionVectorPass( MotionVectorPassState* state; Fmatrix viewProj; Fmatrix prevViewProj; + Fmatrix invViewProj; Fvector cameraPos; Fvector prevCameraPos; u32 width, height; @@ -105,6 +108,7 @@ MotionVectorOutput setupMotionVectorPass( data.state = &state; data.viewProj = viewProj; data.prevViewProj = prevViewProj; + data.invViewProj = g_taa_unjittered_inv_full_transform; data.cameraPos = cameraPos; data.prevCameraPos = prevCameraPos; data.width = width; @@ -130,7 +134,7 @@ MotionVectorOutput setupMotionVectorPass( } cb; cb.viewProj = data.viewProj; cb.prevViewProj = data.prevViewProj; - cb.invViewProj.invert(data.viewProj); + cb.invViewProj = data.invViewProj; cb.screenW = (float)data.width; cb.screenH = (float)data.height; cb.invScreenW = 1.0f / data.width; diff --git a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp index 1b0cb19a12a..354840f5a05 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.cpp @@ -124,7 +124,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; pipeDesc.renderState.depthStencilState.depthTestEnable = true; pipeDesc.renderState.depthStencilState.depthWriteEnable = true; - pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; pipeDesc.renderState.rasterState.frontCounterClockwise = false; pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; return pipeDesc; @@ -178,7 +178,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf auto mdiVsForReflection = shaderLoader->LoadVertexShader("bindless_skinned_mdi", "main"); if (mdiPsResult.handle && mdiVsForReflection.handle) { state.mdiPS = mdiPsResult.handle; - state.mdiLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_MDI_IBL_CSMLadder_NoMask", + state.mdiLayout = cache.GetOrCreateBindingLayoutFromReflection("SkinningPass_MDI_FullLit_v3", *mdiVsForReflection.reflection, *mdiPsResult.reflection, nvDevice); } @@ -214,7 +214,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG16_SNORM).setOffset(20).setElementStride(stride), }; initVariant(state.nonHQ, "bindless_skinned", "SkinningPass_nonHQ_IBL_CSMLadder_v2", attribs, 5); - initMDIVariant(state.mdiNonHQ, "bindless_skinned_mdi", "SkinningPass_mdi_nonHQ_IBL_CSMLadder_v2", attribs, 5); + initMDIVariant(state.mdiNonHQ, "bindless_skinned_mdi", "SkinningPass_mdi_nonHQ_FullLit_v3", attribs, 5); } { @@ -227,7 +227,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), }; initVariant(state.hq1w, "bindless_skinned_hq", "SkinningPass_hq1w_IBL_CSMLadder_v2", attribs, 5); - initMDIVariant(state.mdiHQ1w, "bindless_skinned_hq_mdi", "SkinningPass_mdi_hq1w_IBL_CSMLadder_v2", attribs, 5); + initMDIVariant(state.mdiHQ1w, "bindless_skinned_hq_mdi", "SkinningPass_mdi_hq1w_FullLit_v3", attribs, 5); } { @@ -241,7 +241,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("BLENDINDICES").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(36).setElementStride(stride), }; initVariant(state.hq4w, "bindless_skinned_4w", "SkinningPass_hq4w_IBL_CSMLadder_v2", attribs, 6); - initMDIVariant(state.mdiHQ4w, "bindless_skinned_4w_mdi", "SkinningPass_mdi_hq4w_IBL_CSMLadder_v2", attribs, 6); + initMDIVariant(state.mdiHQ4w, "bindless_skinned_4w_mdi", "SkinningPass_mdi_hq4w_FullLit_v3", attribs, 6); } { @@ -254,7 +254,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), }; initVariant(state.hq2w, "bindless_skinned_2w", "SkinningPass_hq2w_IBL_CSMLadder_v2", attribs, 5); - initMDIVariant(state.mdiHQ2w, "bindless_skinned_2w_mdi", "SkinningPass_mdi_hq2w_IBL_CSMLadder_v2", attribs, 5); + initMDIVariant(state.mdiHQ2w, "bindless_skinned_2w_mdi", "SkinningPass_mdi_hq2w_FullLit_v3", attribs, 5); } { @@ -267,7 +267,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), }; initVariant(state.hq3w, "bindless_skinned_3w", "SkinningPass_hq3w_IBL_CSMLadder_v2", attribs, 5); - initMDIVariant(state.mdiHQ3w, "bindless_skinned_3w_mdi", "SkinningPass_mdi_hq3w_IBL_CSMLadder_v2", attribs, 5); + initMDIVariant(state.mdiHQ3w, "bindless_skinned_3w_mdi", "SkinningPass_mdi_hq3w_FullLit_v3", attribs, 5); } initHudVariant(state.hudNonHQ, state.nonHQ, "SkinningPass_hud_nonHQ_Opaque_v3"); @@ -360,7 +360,7 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; pipeDesc.renderState.depthStencilState.depthTestEnable = true; pipeDesc.renderState.depthStencilState.depthWriteEnable = true; - pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; pipeDesc.renderState.rasterState.frontCounterClockwise = false; pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; depthVar.pipeline = cache.GetOrCreatePipeline(cacheName, pipeDesc, depthFb, nvDevice); @@ -382,6 +382,124 @@ void InitializeSkinningResources(fg::RenderDevice* device, const nvrhi::Framebuf } } + { + auto velPs = shaderLoader->LoadPixelShader("bindless_skinned_velocity", "main"); + if (velPs.handle) + { + state.velocityPS = velPs.handle; + nvrhi::FramebufferInfoEx velFb; + velFb.colorFormats = { nvrhi::Format::RG16_FLOAT }; + velFb.depthFormat = nvrhi::Format::D32; + + auto initVelVariant = [&](SkinningPipelineVariant& var, const char* vsName, + const nvrhi::VertexAttributeDesc* attribs, u32 attrCount, + const char* cacheName, bool mdi) { + auto vsResult = shaderLoader->LoadVertexShader(vsName, "main"); + if (!vsResult.handle || !vsResult.reflection || !velPs.reflection) + return; + if (!state.velocityLayout && !mdi) + state.velocityLayout = cache.GetOrCreateBindingLayoutFromReflection( + "SkinningVelocity_v1", *vsResult.reflection, *velPs.reflection, nvDevice); + if (!state.velocityMdiLayout && mdi) + state.velocityMdiLayout = cache.GetOrCreateBindingLayoutFromReflection( + "SkinningVelocityMDI_v1", *vsResult.reflection, *velPs.reflection, nvDevice); + nvrhi::IBindingLayout* layout = mdi ? state.velocityMdiLayout.Get() : state.velocityLayout.Get(); + if (!layout) + return; + + nvrhi::VertexAttributeDesc localAttribs[8]; + u32 totalAttrs = attrCount; + for (u32 i = 0; i < attrCount; ++i) + localAttribs[i] = attribs[i]; + if (mdi) { + localAttribs[attrCount] = nvrhi::VertexAttributeDesc() + .setName("DRAWINDEX").setFormat(nvrhi::Format::R32_UINT) + .setBufferIndex(1).setOffset(0).setElementStride(4).setIsInstanced(true); + totalAttrs = attrCount + 1; + } + + var.vs = vsResult.handle; + var.inputLayout = nvDevice->createInputLayout(localAttribs, totalAttrs, var.vs); + nvrhi::GraphicsPipelineDesc pipeDesc; + pipeDesc.VS = var.vs; + pipeDesc.PS = state.velocityPS; + pipeDesc.inputLayout = var.inputLayout; + pipeDesc.bindingLayouts = { layout }; + pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; + pipeDesc.renderState.depthStencilState.depthTestEnable = true; + pipeDesc.renderState.depthStencilState.depthWriteEnable = false; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; + pipeDesc.renderState.rasterState.frontCounterClockwise = false; + pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; + pipeDesc.renderState.blendState.targets[0] + .setBlendEnable(false) + .setColorWriteMask(nvrhi::ColorMask::Red | nvrhi::ColorMask::Green); + var.pipeline = cache.GetOrCreatePipeline(cacheName, pipeDesc, velFb, nvDevice); + }; + + { + constexpr u32 stride = 24; + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA16_SNORM).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(8).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(12).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG16_SNORM).setOffset(20).setElementStride(stride), + }; + initVelVariant(state.velNonHQ, "bindless_skinned_velocity", attribs, 5, "SkinningVelocity_nonHQ_v1", false); + initVelVariant(state.velMdiNonHQ, "bindless_skinned_mdi_velocity", attribs, 5, "SkinningVelocity_mdi_nonHQ_v1", true); + } + { + constexpr u32 stride = 36; + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(20).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), + }; + initVelVariant(state.velHQ1w, "bindless_skinned_hq_velocity", attribs, 5, "SkinningVelocity_hq1w_v1", false); + initVelVariant(state.velMdiHQ1w, "bindless_skinned_hq_mdi_velocity", attribs, 5, "SkinningVelocity_mdi_hq1w_v1", true); + } + { + constexpr u32 stride = 40; + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(20).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setOffset(28).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BLENDINDICES").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(36).setElementStride(stride), + }; + initVelVariant(state.velHQ4w, "bindless_skinned_4w_velocity", attribs, 6, "SkinningVelocity_hq4w_v1", false); + initVelVariant(state.velMdiHQ4w, "bindless_skinned_4w_mdi_velocity", attribs, 6, "SkinningVelocity_mdi_hq4w_v1", true); + } + { + constexpr u32 stride = 44; + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("NORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TANGENT").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(20).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("BINORMAL").setFormat(nvrhi::Format::BGRA8_UNORM).setOffset(24).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RGBA32_FLOAT).setOffset(28).setElementStride(stride), + }; + initVelVariant(state.velHQ2w, "bindless_skinned_2w_velocity", attribs, 5, "SkinningVelocity_hq2w_v1", false); + initVelVariant(state.velMdiHQ2w, "bindless_skinned_2w_mdi_velocity", attribs, 5, "SkinningVelocity_mdi_hq2w_v1", true); + initVelVariant(state.velHQ3w, "bindless_skinned_3w_velocity", attribs, 5, "SkinningVelocity_hq3w_v1", false); + initVelVariant(state.velMdiHQ3w, "bindless_skinned_3w_mdi_velocity", attribs, 5, "SkinningVelocity_mdi_hq3w_v1", true); + } + + if (state.velNonHQ.pipeline || state.velHQ1w.pipeline || state.velHQ4w.pipeline) + Msg("* [SkinningPass] Skinned velocity pipelines ready"); + else + Msg("! [SkinningPass] Skinned velocity PSO create failed"); + } + else + { + Msg("! [SkinningPass] bindless_skinned_velocity missing — skinned velocity disabled"); + } + } + state.initialized = true; Msg("* [SkinningPass] Pipeline initialization complete"); } @@ -1063,10 +1181,8 @@ framegraph::DefaultOutputLayout setupSkinningPass( data.color = passBuilder.readWrite(inputs.albedo, ResourceState::RenderTarget); data.normal = passBuilder.readWrite(inputs.normal, ResourceState::RenderTarget); - if (inputs.baseColor.is_valid()) - data.baseColor = passBuilder.readWrite(inputs.baseColor, ResourceState::RenderTarget); - if (inputs.worldPos.is_valid()) - data.worldPos = passBuilder.readWrite(inputs.worldPos, ResourceState::RenderTarget); + data.baseColor = passBuilder.readWrite(inputs.baseColor, ResourceState::RenderTarget); + data.worldPos = passBuilder.readWrite(inputs.worldPos, ResourceState::RenderTarget); data.depth = passBuilder.readWrite(inputs.depth, ResourceState::DepthStencilWrite); data.outputs.albedo = data.color; @@ -1107,7 +1223,7 @@ framegraph::DefaultOutputLayout setupSkinningPass( auto* baseColorRT = data.baseColor.is_valid() ? fg.GetPhysicalTexture(data.baseColor) : nullptr; auto* worldPosRT = data.worldPos.is_valid() ? fg.GetPhysicalTexture(data.worldPos) : nullptr; auto* depthRT = fg.GetPhysicalTexture(data.depth); - if (!colorRT || !depthRT) + if (!colorRT || !depthRT || !normalRT || !baseColorRT || !worldPosRT) return; nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); @@ -1117,12 +1233,9 @@ framegraph::DefaultOutputLayout setupSkinningPass( nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(colorRT); - if (normalRT) - fbDesc.addColorAttachment(normalRT); - if (baseColorRT) - fbDesc.addColorAttachment(baseColorRT); - if (worldPosRT) - fbDesc.addColorAttachment(worldPosRT); + fbDesc.addColorAttachment(normalRT); + fbDesc.addColorAttachment(baseColorRT); + fbDesc.addColorAttachment(worldPosRT); fbDesc.setDepthAttachment(depthRT); auto framebuffer = framegraph::GetPassResourceCache().GetOrCreateFramebuffer("SkinningPass", fbDesc, nvDevice); if (!framebuffer) @@ -1145,10 +1258,15 @@ framegraph::DefaultOutputLayout setupSkinningPass( auto& cache = framegraph::GetPassResourceCache(); auto dynTransformsCB = cache.GetOrCreateVolatileCB("SkinningPass", "DynTransforms", sizeof(DynamicTransforms), data.device, 1024 * 8); - auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + auto staticGlobalsCB = cache.GetOrCreateVolatileCB("SkinningPass", "StaticGlobals", sizeof(StaticGlobals), data.device); auto shaderParamsCB = cache.GetOrCreateVolatileCB("SkinningPass", "ShaderParams", sizeof(ShaderParams), data.device, 512); auto materialIdCB = cache.GetOrCreateVolatileCB("SkinningPass", "MaterialId", sizeof(SkinnedMaterialCB), data.device, 1024 * 8); + { + StaticGlobals sg = BuildStaticGlobals(); + cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); + } + ShaderParams shaderParams = {}; shaderParams.m_AlphaRef = 0.5f; shaderParams.dt_params.set(1.0f, 0.0f, 1.0f, 50.0f); @@ -1280,6 +1398,15 @@ framegraph::DefaultOutputLayout setupSkinningPass( u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); auto sr = GetSplatRange(batch, data.overlayMgr); + { + CKinematics* parent = nullptr; + u32 visualType = batch.visual ? batch.visual->getType() : 0; + if (visualType == MT_SKELETON_GEOMDEF_ST) + parent = static_cast(batch.visual)->GetParent(); + else if (visualType == MT_SKELETON_GEOMDEF_PM) + parent = static_cast(batch.visual)->GetParent(); + gpuCullMgr->SetSkeletonCurrWorld(parent, batch.worldMatrix); + } DrawSkinnedBatch(*data.passState, cmdList, nvDevice, worldCtx, batch, batch.worldMatrix, boneOffset, sr, @@ -1428,12 +1555,9 @@ framegraph::VirtualResourceHandle setupHudOverlayPass( nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(colorRT); - if (normalRT) - fbDesc.addColorAttachment(normalRT); - if (baseColorRT) - fbDesc.addColorAttachment(baseColorRT); - if (worldPosRT) - fbDesc.addColorAttachment(worldPosRT); + fbDesc.addColorAttachment(normalRT); + fbDesc.addColorAttachment(baseColorRT); + fbDesc.addColorAttachment(worldPosRT); fbDesc.setDepthAttachment(depthRT); auto framebuffer = framegraph::GetPassResourceCache().GetOrCreateFramebuffer( data.rtgiGuidePass ? "HudRtgiGuidePass" : "HudOverlayPass", fbDesc, nvDevice); @@ -1448,7 +1572,7 @@ framegraph::VirtualResourceHandle setupHudOverlayPass( auto materialIdCB = cache.GetOrCreateVolatileCB("HudOverlay", "MaterialId", sizeof(SkinnedMaterialCB), data.device, 1024 * 8); { - StaticGlobals sg = BuildStaticGlobals(2.0f, data.rtgiGuidePass); + StaticGlobals sg = BuildStaticGlobals(2.0f, false); cmdList->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); } @@ -1464,7 +1588,7 @@ framegraph::VirtualResourceHandle setupHudOverlayPass( nvrhi::Viewport hudViewport( 0.0f, static_cast(rtDesc.width), 0.0f, static_cast(rtDesc.height), - 0.0f, 0.1f); + 0.9f, 1.0f); nvrhi::Rect scissor(rtDesc.width, rtDesc.height); bool hasScopeBatch = false; @@ -1544,6 +1668,15 @@ framegraph::VirtualResourceHandle setupHudOverlayPass( for (const auto& batch : *data.hudBatches) { Fmatrix adjustedWorldMatrix = ApplyHUDFOVAdjustment(batch.worldMatrix); u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); + { + CKinematics* parent = nullptr; + u32 visualType = batch.visual ? batch.visual->getType() : 0; + if (visualType == MT_SKELETON_GEOMDEF_ST) + parent = static_cast(batch.visual)->GetParent(); + else if (visualType == MT_SKELETON_GEOMDEF_PM) + parent = static_cast(batch.visual)->GetParent(); + gpuCullMgr->SetSkeletonCurrWorld(parent, adjustedWorldMatrix); + } const SkinnedPhaseContext& drawCtx = (IsScopeBatch(batch) && scopeCtx.bindingSet) ? scopeCtx : hudCtx; @@ -1556,4 +1689,299 @@ framegraph::VirtualResourceHandle setupHudOverlayPass( return passData.color; } +framegraph::VirtualResourceHandle setupSkinnedVelocityPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle motionVectors, + framegraph::VirtualResourceHandle depth, + const GeometryCollector* geometry, + const xr_vector* hudBatches, + fg::GPUCullingManager* gpuCulling, + SkinningPassState* state, + const Fmatrix& viewProj, + const Fmatrix& prevViewProj, + u32 width, + u32 height) +{ + using namespace framegraph; + + if (!device || !state || !gpuCulling || !motionVectors.is_valid() || !depth.is_valid()) + return motionVectors; + + if (!state->initialized || !state->velocityPS || !state->velocityLayout) + { + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.colorFormats = { + nvrhi::Format::RGBA16_FLOAT, nvrhi::Format::RGBA16_FLOAT, + nvrhi::Format::RGBA8_UNORM, nvrhi::Format::RGBA32_FLOAT + }; + fbInfo.depthFormat = nvrhi::Format::D32; + InitializeSkinningResources(device, fbInfo, *state); + } + if (!state->velocityPS || !state->velocityLayout) + return motionVectors; + + struct alignas(16) VelocityTransformsCB { + Fmatrix m_W; + Fmatrix m_W_prev; + Fmatrix m_VP; + Fmatrix m_VP_prev; + }; + struct alignas(16) VelocitySkinCB { + u32 boneOffset; + u32 prevBoneOffset; + u32 hasPrev; + u32 pad; + }; + + struct PassData { + VirtualResourceHandle motionVectors; + VirtualResourceHandle depth; + fg::RenderDevice* device = nullptr; + const GeometryCollector* geometry = nullptr; + const xr_vector* hudBatches = nullptr; + fg::GPUCullingManager* gpuCulling = nullptr; + SkinningPassState* passState = nullptr; + Fmatrix viewProj; + Fmatrix prevViewProj; + u32 width = 0; + u32 height = 0; + }; + + auto& passData = fg.addCallbackPass( + "Skinned Velocity", + [&](FrameGraph& builder, PassHandle pass, PassData& data) { + RenderPassBuilder pb(builder, pass); + data.motionVectors = pb.readWrite(motionVectors, ResourceState::RenderTarget); + data.depth = pb.read(depth, ResourceState::DepthStencilRead); + data.device = device; + data.geometry = geometry; + data.hudBatches = hudBatches; + data.gpuCulling = gpuCulling; + data.passState = state; + data.viewProj = viewProj; + data.prevViewProj = prevViewProj; + data.width = width; + data.height = height; + }, + [](const PassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + auto* mvRT = fgGraph.GetPhysicalTexture(data.motionVectors); + auto* depthRT = fgGraph.GetPhysicalTexture(data.depth); + if (!mvRT || !depthRT || !data.gpuCulling || !data.passState) + return; + + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + if (!nvDevice || !cmdList) + return; + + GPUCullingManager* gpuCullMgr = data.gpuCulling; + nvrhi::IBuffer* currBones = gpuCullMgr->GetGlobalBoneBuffer(); + nvrhi::IBuffer* prevBones = gpuCullMgr->GetPrevBoneBuffer(); + if (!currBones || !prevBones) + return; + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(mvRT); + fbDesc.setDepthAttachment(depthRT); + auto framebuffer = framegraph::GetPassResourceCache().GetOrCreateFramebuffer( + "SkinnedVelocityPass", fbDesc, nvDevice); + if (!framebuffer) + return; + + auto& cache = framegraph::GetPassResourceCache(); + auto velCB = cache.GetOrCreateVolatileCB("SkinnedVelocity", "VelocityTransforms", sizeof(VelocityTransformsCB), data.device, 1024 * 8); + auto skinCB = cache.GetOrCreateVolatileCB("SkinnedVelocity", "VelocitySkin", sizeof(VelocitySkinCB), data.device, 1024 * 8); + if (!velCB || !skinCB) + return; + + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* velPsRefl = shaderLoader->GetCachedReflection("bindless_skinned_velocity", ".ps"); + if (!velPsRefl) + return; + + nvrhi::Viewport viewport(0.f, float(data.width), 0.f, float(data.height), 0.f, 1.f); + nvrhi::Rect scissor(data.width, data.height); + + auto selectVel = [&](u32 stride, u16 mode) -> const SkinningPipelineVariant* { + if (mode == RM_SKINNING_3B || mode == RM_SKINNING_3B_HQ) return &data.passState->velHQ3w; + if (mode == RM_SKINNING_2B || mode == RM_SKINNING_2B_HQ) return &data.passState->velHQ2w; + if (mode == RM_SKINNING_4B || mode == RM_SKINNING_4B_HQ) return &data.passState->velHQ4w; + if (mode == RM_SKINNING_1B_HQ || mode == RM_SINGLE_HQ) return &data.passState->velHQ1w; + if (mode == RM_SKINNING_1B || mode == RM_SINGLE) return &data.passState->velNonHQ; + if (stride == 36) return &data.passState->velHQ1w; + if (stride == 40) return &data.passState->velHQ4w; + if (stride == 44) return &data.passState->velHQ2w; + return &data.passState->velNonHQ; + }; + auto selectVelMdi = [&](u32 fmt) -> const SkinningPipelineVariant* { + switch (fmt) { + case VF_SKINNED_NONHQ: return &data.passState->velMdiNonHQ; + case VF_SKINNED_HQ1W: return &data.passState->velMdiHQ1w; + case VF_SKINNED_HQ2W: return &data.passState->velMdiHQ2w; + case VF_SKINNED_HQ3W: return &data.passState->velMdiHQ3w; + case VF_SKINNED_HQ4W: return &data.passState->velMdiHQ4w; + default: return nullptr; + } + }; + + auto drawResidual = [&](const GeometryBatch& batch, const Fmatrix& world, bool hudDepth) { + CKinematics* parent = nullptr; + u32 visualType = batch.visual ? batch.visual->getType() : 0; + if (visualType == MT_SKELETON_GEOMDEF_ST) + parent = static_cast(batch.visual)->GetParent(); + else if (visualType == MT_SKELETON_GEOMDEF_PM) + parent = static_cast(batch.visual)->GetParent(); + + u32 boneOffset = GetSkeletonBoneOffset(cmdList, *gpuCullMgr, batch); + const bool hasPrev = gpuCullMgr->HasPrevBones(parent) && + parent && parent->fg_prev_world_frame + 1 == gpuCullMgr->GetBoneUploadFrameId(); + + const SkinningPipelineVariant* variant = selectVel(batch.vertexStride, batch.skinningRenderMode); + if (!variant || !variant->pipeline || !batch.vertexBuffer || !batch.indexBuffer) + return; + + auto* vsRefl = shaderLoader->GetCachedReflection( + batch.vertexStride == 40 ? "bindless_skinned_4w_velocity" : + batch.vertexStride == 44 ? (batch.skinningRenderMode == RM_SKINNING_3B || batch.skinningRenderMode == RM_SKINNING_3B_HQ + ? "bindless_skinned_3w_velocity" : "bindless_skinned_2w_velocity") : + batch.vertexStride == 36 ? "bindless_skinned_hq_velocity" : + "bindless_skinned_velocity", ".vs"); + if (!vsRefl) + vsRefl = shaderLoader->GetCachedReflection("bindless_skinned_velocity", ".vs"); + if (!vsRefl) + return; + + VelocityTransformsCB vcb{}; + vcb.m_W = world; + vcb.m_W_prev = hasPrev ? parent->fg_prev_world : world; + vcb.m_VP = data.viewProj; + vcb.m_VP_prev = data.prevViewProj; + cmdList->writeBuffer(velCB, &vcb, sizeof(vcb)); + + VelocitySkinCB scb{}; + scb.boneOffset = boneOffset; + scb.prevBoneOffset = hasPrev ? gpuCullMgr->GetPrevBoneOffset(parent) : boneOffset; + scb.hasPrev = hasPrev ? 1u : 0u; + scb.pad = 0; + cmdList->writeBuffer(skinCB, &scb, sizeof(scb)); + + framegraph::BindingSetBuilder bsb(*vsRefl, *velPsRefl, nvDevice, "SkinnedVelocity"); + bsb.ConstantBuffer("VelocityTransforms", velCB); + bsb.BufferSRV("g_BoneMatrices", currBones); + bsb.BufferSRV("g_PrevBoneMatrices", prevBones); + bsb.ConstantBuffer("VelocitySkinCB", skinCB); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.passState->velocityLayout, nvDevice); + if (!bindingSet) + return; + + nvrhi::GraphicsState gfxState; + gfxState.pipeline = variant->pipeline; + gfxState.framebuffer = framebuffer; + gfxState.bindings = { bindingSet }; + gfxState.vertexBuffers = { {batch.vertexBuffer, 0, 0} }; + gfxState.indexBuffer = { batch.indexBuffer, nvrhi::Format::R16_UINT, 0 }; + if (hudDepth) { + nvrhi::Viewport hudVp(0.f, float(data.width), 0.f, float(data.height), 0.9f, 1.f); + gfxState.viewport.addViewport(hudVp); + } else { + gfxState.viewport.addViewport(viewport); + } + gfxState.viewport.addScissorRect(scissor); + cmdList->setGraphicsState(gfxState); + cmdList->drawIndexed( + nvrhi::DrawArguments() + .setVertexCount(batch.indexCount) + .setStartIndexLocation(batch.startIndex) + .setStartVertexLocation(batch.baseVertex)); + }; + + if (data.geometry) { + const bool mdiActive = gpuCullMgr->IsSkinnedMDIEnabled() && gpuCullMgr->GetSkinnedObjectCount() > 0; + if (mdiActive && data.passState->velocityMdiLayout) { + nvrhi::IBuffer* drawIndexBuffer = GetOrCreateDrawIndexBuffer("SkinnedVelocity", nvDevice); + auto& pools = gpuCullMgr->GetSkinnedPools(); + VelocityTransformsCB vcb{}; + vcb.m_W = Fidentity; + vcb.m_W_prev = Fidentity; + vcb.m_VP = data.viewProj; + vcb.m_VP_prev = data.prevViewProj; + cmdList->writeBuffer(velCB, &vcb, sizeof(vcb)); + VelocitySkinCB scb{}; + cmdList->writeBuffer(skinCB, &scb, sizeof(scb)); + + for (u32 f = SkinnedGeometryPools::FIRST_FORMAT; f < SkinnedGeometryPools::FORMAT_COUNT; ++f) { + const auto& bucket = gpuCullMgr->GetSkinnedBucket(f); + if (bucket.count == 0) + continue; + const SkinningPipelineVariant* variant = selectVelMdi(f); + nvrhi::IBuffer* poolVB = pools.GetVertexBuffer(f); + nvrhi::IBuffer* poolIB = pools.GetIndexBuffer(f); + if (!variant || !variant->pipeline || !poolVB || !poolIB || !drawIndexBuffer) + continue; + + const char* vsName = + f == VF_SKINNED_HQ4W ? "bindless_skinned_4w_mdi_velocity" : + f == VF_SKINNED_HQ3W ? "bindless_skinned_3w_mdi_velocity" : + f == VF_SKINNED_HQ2W ? "bindless_skinned_2w_mdi_velocity" : + f == VF_SKINNED_HQ1W ? "bindless_skinned_hq_mdi_velocity" : + "bindless_skinned_mdi_velocity"; + auto* vsRefl = shaderLoader->GetCachedReflection(vsName, ".vs"); + if (!vsRefl) + continue; + + framegraph::BindingSetBuilder bsb(*vsRefl, *velPsRefl, nvDevice, "SkinnedVelocity.MDI"); + bsb.ConstantBuffer("VelocityTransforms", velCB); + bsb.BufferSRV("g_BoneMatrices", currBones); + bsb.BufferSRV("g_PrevBoneMatrices", prevBones); + bsb.ConstantBuffer("VelocitySkinCB", skinCB); + bsb.BufferSRV("g_SkinnedRecords", bucket.recordsBuffer); + bsb.BufferSRV("g_SkinnedCompactIndices", bucket.compactBatchIndicesBuffer); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.passState->velocityMdiLayout, nvDevice); + if (!bindingSet) + continue; + + nvrhi::GraphicsState gfxState; + gfxState.pipeline = variant->pipeline; + gfxState.framebuffer = framebuffer; + gfxState.bindings = { bindingSet }; + gfxState.vertexBuffers = { {poolVB, 0, 0}, {drawIndexBuffer, 1, 0} }; + gfxState.indexBuffer = { poolIB, nvrhi::Format::R16_UINT, 0 }; + gfxState.viewport.addViewport(viewport); + gfxState.viewport.addScissorRect(scissor); + gfxState.indirectParams = bucket.compactDrawArgsBuffer; + gfxState.indirectCountBuffer = bucket.compactCountBuffer; + cmdList->setGraphicsState(gfxState); + DrawIndexedIndirectCountOrFallback(cmdList, 0, 0, bucket.count); + } + } + + for (const auto& batch : data.geometry->GetBatches()) { + if (!batch.isSkinned) + continue; + if (mdiActive) { + const u32 variantIdx = MaterialBuffer::Instance().GetShaderVariant(batch.bindlessMaterialID); + const bool pooled = variantIdx == 0 + && batch.skinnedPoolFormat >= SkinnedGeometryPools::FIRST_FORMAT + && batch.skinnedPoolFormat < SkinnedGeometryPools::FORMAT_COUNT; + if (pooled) + continue; + } + drawResidual(batch, batch.worldMatrix, false); + } + } + + if (data.hudBatches) { + for (const auto& batch : *data.hudBatches) { + if (IsScopeBatch(batch)) + continue; + drawResidual(batch, ApplyHUDFOVAdjustment(batch.worldMatrix), true); + } + } + } + ); + + return passData.motionVectors; +} + } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h index e4258b217e2..599ec3ef96f 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/SkinningPassSetup.h @@ -80,6 +80,19 @@ struct SkinningPassState { SkinningPipelineVariant depthHQ4w; nvrhi::BindingLayoutHandle depthLayout; nvrhi::ShaderHandle depthPS; + SkinningPipelineVariant velNonHQ; + SkinningPipelineVariant velHQ1w; + SkinningPipelineVariant velHQ2w; + SkinningPipelineVariant velHQ3w; + SkinningPipelineVariant velHQ4w; + SkinningPipelineVariant velMdiNonHQ; + SkinningPipelineVariant velMdiHQ1w; + SkinningPipelineVariant velMdiHQ2w; + SkinningPipelineVariant velMdiHQ3w; + SkinningPipelineVariant velMdiHQ4w; + nvrhi::BindingLayoutHandle velocityLayout; + nvrhi::BindingLayoutHandle velocityMdiLayout; + nvrhi::ShaderHandle velocityPS; nvrhi::TextureHandle secondVP; nvrhi::SamplerHandle linearSampler; bool initialized = false; @@ -185,4 +198,19 @@ framegraph::VirtualResourceHandle setupHudOverlayPass( bool rtgiGuidePass = false ); +framegraph::VirtualResourceHandle setupSkinnedVelocityPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle motionVectors, + framegraph::VirtualResourceHandle depth, + const GeometryCollector* geometry, + const xr_vector* hudBatches, + fg::GPUCullingManager* gpuCulling, + SkinningPassState* state, + const Fmatrix& viewProj, + const Fmatrix& prevViewProj, + u32 width, + u32 height +); + } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/RayTracing/RTAccelStructManager.cpp b/src/Layers/xrRender/RayTracing/RTAccelStructManager.cpp index 7ed016785a5..c0799fd7156 100644 --- a/src/Layers/xrRender/RayTracing/RTAccelStructManager.cpp +++ b/src/Layers/xrRender/RayTracing/RTAccelStructManager.cpp @@ -11,6 +11,7 @@ #include "Layers/xrRender/SkeletonCustom.h" #include "Layers/xrRender/ShaderVariant/VariantPSOCache.h" #include "Layers/xrRender/FGDetailManager.h" +#include "Layers/xrRender/FrameGraphPasses/ParticlePassSetup.h" #include "Layers/xrRender/FrameGraph/ShaderLoader.h" #include "xrEngine/IGame_Persistent.h" #include "xrEngine/Environment.h" @@ -19,6 +20,7 @@ extern ENGINE_API float ps_r3_grass_blade_width; extern ENGINE_API float ps_r3_grass_blade_height; extern ENGINE_API float ps_r3_grass_wind_displacement; +extern ENGINE_API float ps_r_rt_detail_dist; namespace xray::render::fg { extern xray::render::FrameGraphRenderer RImplementation; @@ -75,9 +77,12 @@ static_assert(sizeof(GrassRTCB) == 96, "GrassRTCB must be 96 bytes"); struct BillboardRTCB { u32 maxVertsPerBillboard; + float maxDist; + float camX, camY; + float camZ; u32 pad[3]; }; -static_assert(sizeof(BillboardRTCB) == 16, "BillboardRTCB must be 16 bytes"); +static_assert(sizeof(BillboardRTCB) == 32, "BillboardRTCB must be 32 bytes"); static void FmatrixToRTTransform(const Fmatrix& m, nvrhi::rt::AffineTransform& out) { @@ -100,6 +105,22 @@ void RTAccelStructManager::Initialize(fg::RenderDevice* device) Msg("* [RT] Ray tracing NOT supported on this device"); } +void RTAccelStructManager::RetireAccelStruct(nvrhi::rt::AccelStructHandle as) +{ + if (!as) + return; + m_retiredAccelStructs.push_back(as); +} + +void RTAccelStructManager::PumpRetiredAccelStructs() +{ + constexpr size_t kKeepAlive = 4; + if (m_retiredAccelStructs.size() > kKeepAlive) + m_retiredAccelStructs.erase( + m_retiredAccelStructs.begin(), + m_retiredAccelStructs.end() - static_cast(kKeepAlive)); +} + void RTAccelStructManager::Shutdown() { m_staticBlas = nullptr; @@ -108,6 +129,7 @@ void RTAccelStructManager::Shutdown() m_tlasMaxInstances = 0; m_tlasBuiltInstances = 0; m_batchInfoBuffer = nullptr; + m_retiredAccelStructs.clear(); m_skinnedOutputVB = nullptr; m_skinnedIB = nullptr; m_skinnedBlas = nullptr; @@ -122,6 +144,15 @@ void RTAccelStructManager::Shutdown() m_grassTotalVerts = 0; m_grassTotalIndices = 0; m_grassReady = false; + m_particleOutputVB = nullptr; + m_particleIB = nullptr; + m_particleBlas = nullptr; + m_particleTotalVerts = 0; + m_particleTotalIndices = 0; + m_particleTopoHash = 0; + m_particleReady = false; + m_particleAllocFailed = false; + m_particleBatchData.clear(); m_isReady = false; m_batchCount = 0; m_batchCounts = {}; @@ -164,9 +195,27 @@ void RTAccelStructManager::InvalidateShaderPipelines() Msg("* [RTAccel] Shader pipelines invalidated for hot-reload"); } +void RTAccelStructManager::ReleaseAccelerationStructures() +{ + m_staticBlas = nullptr; + m_uniqueGeometries.clear(); + m_tlas = nullptr; + m_tlasMaxInstances = 0; + m_tlasBuiltInstances = 0; + m_batchInfoBuffer = nullptr; + InvalidateSkinned(); + InvalidateGrass(); + InvalidateParticles(); + m_grassAllocFailed = false; + m_particleAllocFailed = false; + m_isReady = false; + m_batchCount = 0; + m_batchCounts = {}; +} + void RTAccelStructManager::BuildIfNeeded(nvrhi::ICommandList* cmdList, GPUCullingManager* gpuCulling) { - if (m_isReady || !m_rtSupported || !gpuCulling) + if (!m_rtSupported || !gpuCulling) return; if (!gpuCulling->IsMegaDataUploaded()) @@ -176,6 +225,35 @@ void RTAccelStructManager::BuildIfNeeded(nvrhi::ICommandList* cmdList, GPUCullin if (drawArgs.empty()) return; + u32 terrainAvail = 0; + for (const auto& a : gpuCulling->GetTerrainDrawArgsData()) + { + if (a.indexCountPerInstance > 0) + terrainAvail++; + } + + if (m_isReady) + { + if (m_batchCounts.terrain == 0 && terrainAvail > 0) + { + Msg("* [RT] Terrain batches appeared (%u) — rebuilding static AS", terrainAvail); + m_staticBlas = nullptr; + m_uniqueGeometries.clear(); + m_tlas = nullptr; + m_batchInfoBuffer = nullptr; + m_isReady = false; + m_batchCount = 0; + m_batchCounts = {}; + } + else + { + return; + } + } + + if (gpuCulling->GetTerrainObjectCount() > 0 && terrainAvail == 0) + return; + m_megaVB = gpuCulling->GetMegaVertexBuffer(); m_megaIB = gpuCulling->GetMegaIndexBuffer(); @@ -365,7 +443,9 @@ void RTAccelStructManager::BuildTLAS(nvrhi::ICommandList* cmdList) u32 skinnedCount = m_skinnedReady ? (u32)m_skinnedBatchData.size() : 0; u32 grassCount = m_grassReady ? 1 : 0; - u32 instanceCount = (m_staticBlas ? 1 : 0) + totalInstances + (m_skinnedBlas ? 1 : 0) + (m_grassBlas ? 1 : 0); + u32 particleCount = m_particleReady ? 1 : 0; + u32 instanceCount = (m_staticBlas ? 1 : 0) + totalInstances + (m_skinnedBlas ? 1 : 0) + + (m_grassBlas ? 1 : 0) + (m_particleBlas ? 1 : 0); if (instanceCount == 0) { Msg("! [RT] No BLAS to build TLAS from"); return; @@ -383,6 +463,7 @@ void RTAccelStructManager::BuildTLAS(nvrhi::ICommandList* cmdList) tlasDesc.buildFlags = nvrhi::rt::AccelStructBuildFlags::PreferFastTrace | nvrhi::rt::AccelStructBuildFlags::AllowUpdate; + RetireAccelStruct(m_tlas); m_tlas = nvDevice->createAccelStruct(tlasDesc); if (!m_tlas) { Msg("! [RT] Failed to create TLAS (instances=%u capacity=%u)", instanceCount, m_tlasMaxInstances); @@ -390,6 +471,7 @@ void RTAccelStructManager::BuildTLAS(nvrhi::ICommandList* cmdList) return; } Msg("* [RT] TLAS created (capacity=%u)", m_tlasMaxInstances); + PumpRetiredAccelStructs(); } xr_vector instances; @@ -452,7 +534,19 @@ void RTAccelStructManager::BuildTLAS(nvrhi::ICommandList* cmdList) instances.push_back(inst); } - m_batchCount = grassBatchOffset + grassCount; + u32 particleBatchOffset = grassBatchOffset + grassCount; + if (m_particleBlas && particleCount > 0) { + nvrhi::rt::InstanceDesc inst; + inst.setTransform(nvrhi::rt::c_IdentityTransform) + .setInstanceID(particleBatchOffset) + .setInstanceMask(0x04) + .setFlags(nvrhi::rt::InstanceFlags::TriangleCullDisable | + nvrhi::rt::InstanceFlags::ForceOpaque) + .setBLAS(m_particleBlas); + instances.push_back(inst); + } + + m_batchCount = particleBatchOffset + particleCount; const u32 builtCount = (u32)instances.size(); auto buildFlags = nvrhi::rt::AccelStructBuildFlags::PreferFastTrace | nvrhi::rt::AccelStructBuildFlags::AllowUpdate; @@ -531,6 +625,15 @@ void RTAccelStructManager::CreateBatchInfoBuffer(nvrhi::ICommandList* cmdList, G batchInfos.push_back(info); } + if (m_particleReady && m_particleTotalIndices > 0 && !m_particleBatchData.empty()) { + RTBatchInfo info; + info.materialID = m_particleBatchData[0].materialID; + info.startIndex = 0; + info.baseVertex = 0; + info.indexCount = m_particleTotalIndices; + batchInfos.push_back(info); + } + if (batchInfos.empty()) return; const u64 neededBytes = batchInfos.size() * sizeof(RTBatchInfo); @@ -724,16 +827,37 @@ void RTAccelStructManager::BuildSkinnedBLAS( nvrhi::ComputeState state; state.pipeline = s_skinPipeline; + if (!boneBuffer) { + Msg("! [RT] Skinned skinning skipped — bone buffer missing"); + InvalidateSkinned(); + return; + } + + cmdList->setBufferState(m_skinnedOutputVB, nvrhi::ResourceStates::UnorderedAccess); + for (const auto& sb : m_skinnedBatchData) { + if (!sb.srcVB) + continue; auto it = bindingSetCache.find(sb.srcVB); if (it == bindingSetCache.end()) { auto* skinRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("rt_skin_vertices", ".cs"); + if (!skinRefl) { + Msg("! [RT] rt_skin_vertices reflection missing"); + InvalidateSkinned(); + return; + } framegraph::BindingSetBuilder bsb(*skinRefl, nvDevice, "RT.SkinVertices"); bsb.BufferSRV("g_SrcVB", sb.srcVB) .BufferSRV("g_BoneMatrices", boneBuffer) .BufferUAV("g_Output", m_skinnedOutputVB) .ConstantBuffer("RTSkinningCB", m_device->GetNativeBuffer(s_skinCB)); - it = bindingSetCache.emplace(sb.srcVB, nvDevice->createBindingSet(bsb.Build(), s_skinLayout)).first; + auto set = nvDevice->createBindingSet(bsb.Build(), s_skinLayout); + if (!set) { + Msg("! [RT] Skinned binding set failed (srcVB)"); + InvalidateSkinned(); + return; + } + it = bindingSetCache.emplace(sb.srcVB, set).first; } RTSkinningCB cb; @@ -753,6 +877,8 @@ void RTAccelStructManager::BuildSkinnedBLAS( cmdList->dispatch((sb.vertexCount + 255) / 256, 1, 1); } + cmdList->setBufferState(m_skinnedOutputVB, nvrhi::ResourceStates::AccelStructBuildInput); + u64 topoHash = m_skinnedBatchData.size(); for (const auto& sb : m_skinnedBatchData) { topoHash = topoHash * 131ull + sb.vertexCount; @@ -786,6 +912,7 @@ void RTAccelStructManager::BuildSkinnedBLAS( const bool needCreate = !m_skinnedBlas || m_skinnedTopoHash != topoHash || m_skinnedTotalVerts != totalVerts || m_skinnedTotalIndices != totalIndices; if (needCreate) { + RetireAccelStruct(m_skinnedBlas); m_skinnedBlas = nvDevice->createAccelStruct(blasDesc); if (!m_skinnedBlas) { Msg("! [RT] Skinned BLAS create failed"); @@ -793,8 +920,12 @@ void RTAccelStructManager::BuildSkinnedBLAS( return; } nvrhi::utils::BuildBottomLevelAccelStruct(cmdList, m_skinnedBlas, blasDesc); - Msg("* [RT] Built %u skinned BLAS (%u verts, %u indices)", - (u32)m_skinnedBatchData.size(), totalVerts, totalIndices); + static u32 s_lastLogFrame = 0; + if (Device.dwFrame - s_lastLogFrame > 60u) { + s_lastLogFrame = Device.dwFrame; + Msg("* [RT] Built %u skinned BLAS (%u verts, %u indices)", + (u32)m_skinnedBatchData.size(), totalVerts, totalIndices); + } } else if (m_skinnedBlas) { blasDesc.buildFlags = nvrhi::rt::AccelStructBuildFlags::PreferFastBuild | nvrhi::rt::AccelStructBuildFlags::AllowUpdate | @@ -807,10 +938,12 @@ void RTAccelStructManager::BuildSkinnedBLAS( m_skinnedTopoHash = topoHash; m_batchCounts.skinned = (u32)m_skinnedBatchData.size(); m_skinnedReady = true; + PumpRetiredAccelStructs(); } void RTAccelStructManager::InvalidateSkinned() { + RetireAccelStruct(m_skinnedBlas); m_skinnedBlas = nullptr; m_skinnedBatchData.clear(); m_skinnedTotalVerts = 0; @@ -818,10 +951,12 @@ void RTAccelStructManager::InvalidateSkinned() m_skinnedTopoHash = 0; m_batchCounts.skinned = 0; m_skinnedReady = false; + PumpRetiredAccelStructs(); } void RTAccelStructManager::InvalidateGrass() { + RetireAccelStruct(m_grassBlas); m_grassBlas = nullptr; m_grassTotalVerts = 0; m_grassTotalIndices = 0; @@ -829,6 +964,20 @@ void RTAccelStructManager::InvalidateGrass() m_grassReady = false; m_grassBillboardMode = false; m_detailAtlasIndex = 0; + PumpRetiredAccelStructs(); +} + +void RTAccelStructManager::InvalidateParticles() +{ + RetireAccelStruct(m_particleBlas); + m_particleBlas = nullptr; + m_particleTotalVerts = 0; + m_particleTotalIndices = 0; + m_particleTopoHash = 0; + m_batchCounts.particles = 0; + m_particleReady = false; + m_particleBatchData.clear(); + PumpRetiredAccelStructs(); } void RTAccelStructManager::RebuildDynamic(nvrhi::ICommandList* cmdList, GPUCullingManager* gpuCulling) @@ -912,7 +1061,10 @@ void RTAccelStructManager::InitBillboardPipeline() void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetailManager* detailMgr) { - if (!m_rtSupported || !m_isReady || !detailMgr) + if (!m_rtSupported || !m_isReady || !detailMgr || m_grassAllocFailed) + return; + + if (m_grassReady && (Device.dwFrame & 3u) != 0u) return; const bool billboardMode = !ps_r__detail_gpu; @@ -939,7 +1091,7 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail return; } - constexpr u32 kMaxRtBillboardVerts = 1500000u; + constexpr u32 kMaxRtBillboardVerts = 500000u; u32 capacity = detailMgr->visibleBufferCapacity; const u32 maxCapacity = kMaxRtBillboardVerts / maxVPB; if (capacity > maxCapacity) @@ -949,7 +1101,13 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail return; } - totalVerts = capacity * maxVPB; + const u32 actualCount = std::min(stats.visibleBillboardCount, capacity); + if (actualCount == 0) { + InvalidateGrass(); + return; + } + + totalVerts = actualCount * maxVPB; totalIndices = totalVerts; u64 vbSize = static_cast(totalVerts) * GRASS_VERTEX_STRIDE; u64 ibSize = static_cast(totalIndices) * sizeof(u32); @@ -965,7 +1123,8 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail desc.canHaveUAVs = true; m_grassOutputVB = nvDevice->createBuffer(desc); if (!m_grassOutputVB) { - Msg("! [RT] Grass VB alloc failed (%.1f MB)", float(vbSize) / (1024.f * 1024.f)); + Msg("! [RT] Grass VB alloc failed (%.1f MB) — grass excluded from RT", float(vbSize) / (1024.f * 1024.f)); + m_grassAllocFailed = true; InvalidateGrass(); return; } @@ -982,7 +1141,8 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail desc.keepInitialState = true; m_grassIB = nvDevice->createBuffer(desc); if (!m_grassIB) { - Msg("! [RT] Grass IB alloc failed (%.1f MB)", float(ibSize) / (1024.f * 1024.f)); + Msg("! [RT] Grass IB alloc failed (%.1f MB) — grass excluded from RT", float(ibSize) / (1024.f * 1024.f)); + m_grassAllocFailed = true; InvalidateGrass(); return; } @@ -990,6 +1150,10 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail BillboardRTCB cb; cb.maxVertsPerBillboard = maxVPB; + cb.maxDist = ps_r_rt_detail_dist; + cb.camX = Device.vCameraPosition.x; + cb.camY = Device.vCameraPosition.y; + cb.camZ = Device.vCameraPosition.z; cb.pad[0] = cb.pad[1] = cb.pad[2] = 0; auto* billboardRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("rt_grass_billboard", ".cs"); @@ -1011,7 +1175,7 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail state.pipeline = s_billboardPipeline; state.bindings = { bindingSet }; cmdList->setComputeState(state); - cmdList->dispatch((capacity + 255) / 256, 1, 1); + cmdList->dispatch((actualCount + 255) / 256, 1, 1); } else { InitGrassPipeline(); if (!s_grassInitialized) return; @@ -1019,14 +1183,27 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail u32 lodCounts[FGDetailManager::LOD_COUNT] = { stats.visibleLOD0Count, stats.visibleLOD1Count, stats.visibleLOD2Count }; + const float distFactor = std::min(1.f, ps_r_rt_detail_dist / 40.f); + for (u32 lod = 0; lod < FGDetailManager::LOD_COUNT; lod++) + lodCounts[lod] = (u32)((float)lodCounts[lod] * distFactor); + u32 totalBlades = lodCounts[0] + lodCounts[1] + lodCounts[2]; if (totalBlades == 0) { InvalidateGrass(); return; } constexpr u32 kMaxRtGrassBlades = 500000u; - if (totalBlades > kMaxRtGrassBlades) { - const float scale = (float)kMaxRtGrassBlades / (float)totalBlades; - for (u32 lod = 0; lod < FGDetailManager::LOD_COUNT; lod++) - lodCounts[lod] = (u32)((float)lodCounts[lod] * scale); + const u32 maxBlades = std::max(1u, (u32)((float)kMaxRtGrassBlades * distFactor)); + if (totalBlades > maxBlades) { + u32 excess = totalBlades - maxBlades; + const u32 drop0 = std::min(lodCounts[0], excess); + lodCounts[0] -= drop0; + excess -= drop0; + if (excess > 0) { + const u32 drop1 = std::min(lodCounts[1], excess); + lodCounts[1] -= drop1; + excess -= drop1; + } + if (excess > 0) + lodCounts[2] = (lodCounts[2] > excess) ? (lodCounts[2] - excess) : 0; totalBlades = lodCounts[0] + lodCounts[1] + lodCounts[2]; } @@ -1053,7 +1230,7 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail desc.keepInitialState = true; desc.canHaveUAVs = true; m_grassOutputVB = nvDevice->createBuffer(desc); - if (!m_grassOutputVB) { InvalidateGrass(); return; } + if (!m_grassOutputVB) { m_grassAllocFailed = true; InvalidateGrass(); return; } } if (!m_grassIB || m_grassIB->getDesc().byteSize < ibSize) { @@ -1066,7 +1243,7 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail desc.initialState = nvrhi::ResourceStates::ShaderResource; desc.keepInitialState = true; m_grassIB = nvDevice->createBuffer(desc); - if (!m_grassIB) { InvalidateGrass(); return; } + if (!m_grassIB) { m_grassAllocFailed = true; InvalidateGrass(); return; } } float windAngleDeg = 0.0f; @@ -1148,8 +1325,11 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail geom.setTriangles(tri).setFlags(geomFlags); blasDesc.addBottomLevelGeometry(geom); - if (!m_grassBlas || m_grassTotalVerts != totalVerts || m_grassTotalIndices != totalIndices) + if (!m_grassBlas || m_grassTotalVerts != totalVerts || m_grassTotalIndices != totalIndices) { + RetireAccelStruct(m_grassBlas); m_grassBlas = nvDevice->createAccelStruct(blasDesc); + PumpRetiredAccelStructs(); + } if (!m_grassBlas) { Msg("! [RT] Grass BLAS create failed (verts=%u)", totalVerts); @@ -1178,4 +1358,14 @@ void RTAccelStructManager::BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetail } } +void RTAccelStructManager::BuildParticleBLAS( + nvrhi::ICommandList* cmdList, + const xr_vector& worldBatches) +{ + (void)cmdList; + (void)worldBatches; + if (m_particleReady || m_particleBlas) + InvalidateParticles(); +} + } diff --git a/src/Layers/xrRender/RayTracing/RTAccelStructManager.h b/src/Layers/xrRender/RayTracing/RTAccelStructManager.h index ff56d1f8eb4..682ce995322 100644 --- a/src/Layers/xrRender/RayTracing/RTAccelStructManager.h +++ b/src/Layers/xrRender/RayTracing/RTAccelStructManager.h @@ -11,6 +11,10 @@ namespace xray::render::fg { class FGDetailManager; } +namespace xray::render::fg::passes { + struct ParticleBatch; +} + namespace xray::render::fg { class RenderDevice; class RenderContext; @@ -34,6 +38,7 @@ struct RTBatchCounts { u32 instancedTotal = 0; u32 skinned = 0; u32 grass = 0; + u32 particles = 0; }; class RTAccelStructManager { @@ -42,14 +47,17 @@ class RTAccelStructManager { void Shutdown(); void BuildIfNeeded(nvrhi::ICommandList* cmdList, GPUCullingManager* gpuCulling); + void ReleaseAccelerationStructures(); void BuildSkinnedBLAS(nvrhi::ICommandList* cmdList, GPUCullingManager* gpuCulling, const xr_vector& worldBatches, const xr_vector& hudBatches); void BuildGrassBLAS(nvrhi::ICommandList* cmdList, FGDetailManager* detailMgr); + void BuildParticleBLAS(nvrhi::ICommandList* cmdList, const xr_vector& worldBatches); void RebuildDynamic(nvrhi::ICommandList* cmdList, GPUCullingManager* gpuCulling); void InvalidateSkinned(); void InvalidateGrass(); + void InvalidateParticles(); static void InvalidateShaderPipelines(); bool IsReady() const { return m_isReady; } @@ -65,6 +73,8 @@ class RTAccelStructManager { nvrhi::IBuffer* GetSkinnedIB() const { return m_skinnedIB.Get(); } nvrhi::IBuffer* GetGrassOutputVB() const { return m_grassOutputVB.Get(); } nvrhi::IBuffer* GetGrassIB() const { return m_grassIB.Get(); } + nvrhi::IBuffer* GetParticleOutputVB() const { return m_particleOutputVB.Get(); } + nvrhi::IBuffer* GetParticleIB() const { return m_particleIB.Get(); } u32 GetBatchCount() const { return m_batchCount; } const RTBatchCounts& GetBatchCounts() const { return m_batchCounts; } u32 GetDetailAtlasIndex() const { return m_detailAtlasIndex; } @@ -120,6 +130,8 @@ class RTAccelStructManager { void InitGrassPipeline(); void InitBillboardPipeline(); u32 GetSkinningFormatID(u16 renderMode, u32 stride); + void RetireAccelStruct(nvrhi::rt::AccelStructHandle as); + void PumpRetiredAccelStructs(); fg::RenderDevice* m_device = nullptr; bool m_rtSupported = false; @@ -133,6 +145,7 @@ class RTAccelStructManager { u32 m_tlasMaxInstances = 0; u32 m_tlasBuiltInstances = 0; nvrhi::BufferHandle m_batchInfoBuffer; + xr_vector m_retiredAccelStructs; nvrhi::IBuffer* m_megaVB = nullptr; nvrhi::IBuffer* m_megaIB = nullptr; @@ -155,8 +168,27 @@ class RTAccelStructManager { u32 m_grassTotalIndices = 0; bool m_grassReady = false; bool m_grassBillboardMode = false; + bool m_grassAllocFailed = false; u32 m_detailAtlasIndex = 0; + nvrhi::BufferHandle m_particleOutputVB; + nvrhi::BufferHandle m_particleIB; + nvrhi::rt::AccelStructHandle m_particleBlas; + u32 m_particleTotalVerts = 0; + u32 m_particleTotalIndices = 0; + u64 m_particleTopoHash = 0; + bool m_particleReady = false; + bool m_particleAllocFailed = false; + + struct ParticleBatchRT { + u32 vertexOffset; + u32 vertexCount; + u32 indexOffset; + u32 indexCount; + u32 materialID; + }; + xr_vector m_particleBatchData; + static nvrhi::ComputePipelineHandle s_skinPipeline; static nvrhi::BindingLayoutHandle s_skinLayout; static fg::BufferHandle s_skinCB; diff --git a/src/Layers/xrRender/SkeletonCustom.h b/src/Layers/xrRender/SkeletonCustom.h index f125d629c9c..08a46506377 100644 --- a/src/Layers/xrRender/SkeletonCustom.h +++ b/src/Layers/xrRender/SkeletonCustom.h @@ -136,6 +136,12 @@ class CKinematics : public FHierrarhyVisual, public IKinematics u32 fg_bone_upload_frame{0}; u32 fg_bone_upload_offset{0}; + u32 fg_prev_bone_upload_offset{0}; + u32 fg_prev_bone_valid{0}; + Fmatrix fg_curr_world{}; + u32 fg_curr_world_frame{0}; + Fmatrix fg_prev_world{}; + u32 fg_prev_world_frame{0}; protected: SkeletonWMVec wallmarks; From 7d6c4a7c3bd115e72ffc2ea1b3651fa074f020d0 Mon Sep 17 00:00:00 2001 From: FoxConED Date: Thu, 30 Jul 2026 00:01:36 +0300 Subject: [PATCH 258/261] xrRender: harden particle occlusion and glow forward paths --- res/gamedata/shaders/r5/bindless_particle.ps | Bin 1552 -> 1784 bytes .../shaders/r5/bindless_particle_distort.ps | Bin 782 -> 782 bytes .../shaders/r5/bindless_particle_lit.ps | Bin 4735 -> 4435 bytes .../shaders/r5/bindless_particle_sixway.ps | Bin 4202 -> 4282 bytes res/gamedata/shaders/r5/glow_forward.ps | Bin 0 -> 757 bytes res/gamedata/shaders/r5/glow_forward.vs | 21 + res/gamedata/shaders/r5/particle_cull.cs | 4 - .../shaders/r5/particle_cull_debug.cs | 51 ++- res/gamedata/shaders/r5/particle_occlusion.ps | Bin 0 -> 1603 bytes res/gamedata/shaders/r5/rt_particle_alpha.h | 44 ++ .../FrameGraphPasses/GlowPassSetup.cpp | 19 +- .../FrameGraphPasses/ParticlePassSetup.cpp | 416 ++++++++++++++++-- .../FrameGraphPasses/ParticlePassSetup.h | 37 +- 13 files changed, 537 insertions(+), 55 deletions(-) create mode 100644 res/gamedata/shaders/r5/glow_forward.ps create mode 100644 res/gamedata/shaders/r5/glow_forward.vs create mode 100644 res/gamedata/shaders/r5/particle_occlusion.ps create mode 100644 res/gamedata/shaders/r5/rt_particle_alpha.h diff --git a/res/gamedata/shaders/r5/bindless_particle.ps b/res/gamedata/shaders/r5/bindless_particle.ps index a3298c35f38a6d8ff8ac7d5df0c0925e44b79992..de7af793cac239566ce1e84a8a0b18d45423264a 100644 GIT binary patch delta 563 zcmZ8e!AiqG6a*VDK~LU1JzFS=)U}F5uvQILZLwmh1#gCJvT1^86L&YXD$-vl><4)B zuR8bAeWmq$mm>1$9gECxLefzXyo!MQh*KZ$pPVAP|Yno=d;W7OxGg)F_Y22>d z_b)o?MA$WgF&Cu*3=~7zXtP%0UdbXaWQj&jMw47!q115$=3we(snrif{Q+mjGe2sW zc@i&$UGZKhYG``@AifGEv8r7PqMeIZttLLS(xJlU-LO#w1d+$}zT1AQ{6`~Uy| delta 340 zcmeytJAr3{gB({#YDGzDQL2%PO*W{ec0*#{7 z;?kTFy+j3F1q}l|149LU1tU{aJp)ZmYc4JYAOP!DD28iQuthOx@&{(k!)kYB8knO9I+qL%?=>Qz+g zAlV6Y%j9Yn^U2#-QJ(yt#e1?JtNY}ctcH`%vC3OP-I$+N0yNt>Kd+=HvA9H| zI1v~Ci6yBT#RZAU5PvIZDH!S*Yw9Q%0gVK?Y_blUlq`q=4TqxiBn2&7g<_yl5Cyf9 IBiZHy0RBpCZ~y=R diff --git a/res/gamedata/shaders/r5/bindless_particle_distort.ps b/res/gamedata/shaders/r5/bindless_particle_distort.ps index ef5f6cab6e684210473371cfffae6e1ec412c5c4..ecb9a41238936648d2d23e49021d161efac512cf 100644 GIT binary patch delta 12 TcmeBU>tmac#b~@SdodFL8gK*{ delta 12 TcmeBU>tmac#b~-QdodFL8hQj8 diff --git a/res/gamedata/shaders/r5/bindless_particle_lit.ps b/res/gamedata/shaders/r5/bindless_particle_lit.ps index e8a60191993d1972f4d8e65a6d3d948c17aeacdf..71554f3f94305a2b65df46bde9259bbc96f43837 100644 GIT binary patch delta 232 zcmeyba#?9ZBxikEPJUvELUK-Kfun-0LT+M(Mq*AuMxqXsnUtE6uUCY^OV?3I%mIrh zX=-X(b147;NWTWe5M7|o5K*fu5 z^YcqGic3-pGz|0%bb#uNOm!5B3lfu4T~Z55GBh_w^Za7gEJ`gd%>i1Wpk=E7(g`sO PDq57D1Q(xtSa1&j2SrKw delta 562 zcma)&u};G<5QZg~8Zqzy3@60kgkm)f8_EPL3mc>BUPvW_E&DQ6RaSQ1fei_%Pl9*{ zUIXj|r6CbgEe~g({eS=cpQqnv@As1vsaA#wWl8}T@F<8k^&J-RRkZz>tPko!X+hw0 zL6F0>ej+OzF(;_9f{-~QB;KH*5)w#x7EfXB|3YuI@=4x^3|k9aY8dr4jld>~VEcUZ zby%+oV8I=sbGJyPUP7Kj8h0Fvu2R8aWA~CNw1zcx#?mQ-?VYu+xP4f3weZ&|pZ!`! zGp diff --git a/res/gamedata/shaders/r5/bindless_particle_sixway.ps b/res/gamedata/shaders/r5/bindless_particle_sixway.ps index e3141cdafc093ef5d37d378da2bd38a93dc654c4..12a412891f3bda6a16c191f661b603be40343f0b 100644 GIT binary patch delta 185 zcmaE*uuE~nEUx;roczQRh2)&f0!IZ~h1|pnjl`USj6@wMGbuGCU#|#-m#(9bm;)A1 z($v(n=28FxkbVt_A-X`FC3;{(f>P6f@?ZrJYl`#JN`ST)>KQ<^Wu|T3%T>ap4izuX n&Cf5%C@x7Y&@j+5&;hD5GSyKiE=WvHbxAEK$=IC2%f<`<0U9~( delta 98 zcmdm`_)1~JEH0VkoXi4^#GHbRLwC;-P zB$1DNkB>?cFrzB!ULR3c@nBezzwsXh?d2 zZMIMIG~3+HA;Az2S!Ive?q-%KsOW_}T?D2ab;IqX z#Vysl@n$#CXvPoCUd$EJ+++iM4;I9sS7f@OkV0|P*O11xbY?iM#CAt3Kfhl%L=Wqo t*WhP>muf@%;b* literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/glow_forward.vs b/res/gamedata/shaders/r5/glow_forward.vs new file mode 100644 index 00000000000..c20c9d3c97c --- /dev/null +++ b/res/gamedata/shaders/r5/glow_forward.vs @@ -0,0 +1,21 @@ +#include "shared/common.h" + +struct VS_INPUT { + float4 position : POSITION; + float4 color : COLOR0; + float2 tc : TEXCOORD0; +}; + +struct VS_OUTPUT { + float4 hpos : SV_POSITION; + float4 color : COLOR0; + float2 tc : TEXCOORD0; +}; + +VS_OUTPUT main(VS_INPUT v) { + VS_OUTPUT o; + o.hpos = mul(m_WVP, float4(v.position.xyz, 1.0)); + o.color = v.color; + o.tc = v.tc; + return o; +} diff --git a/res/gamedata/shaders/r5/particle_cull.cs b/res/gamedata/shaders/r5/particle_cull.cs index 144829acf34..2f4c5ccab73 100644 --- a/res/gamedata/shaders/r5/particle_cull.cs +++ b/res/gamedata/shaders/r5/particle_cull.cs @@ -48,10 +48,6 @@ void main(uint3 dispatchThreadID : SV_DispatchThreadID) bool visible = FrustumTestSphere(p.position, p.radius, g_FrustumPlanes); - if (visible && g_HiZMipLevels > 0) - visible = HiZTestSphere(p.position, p.radius, g_CameraPos.xyz, g_PrevViewProj, - g_HiZPyramid, smp_nofilter, g_HiZWidth, g_HiZHeight, g_HiZMipLevels); - if (visible) { uint unused; diff --git a/res/gamedata/shaders/r5/particle_cull_debug.cs b/res/gamedata/shaders/r5/particle_cull_debug.cs index 2e919dc6408..be54a71a337 100644 --- a/res/gamedata/shaders/r5/particle_cull_debug.cs +++ b/res/gamedata/shaders/r5/particle_cull_debug.cs @@ -15,7 +15,6 @@ struct GPUParticleData cbuffer CullDebugParams : register(b5) { float4x4 g_ViewProj; - float4x4 g_PrevViewProj; float3 g_CameraPos; float g_MaxDistance; float4 g_FrustumPlanes[6]; @@ -36,9 +35,53 @@ struct GPUParticleData float3 OcclusionTestSphereDebug(float3 center, float radius) { - HiZTestResult r = HiZTestSphereEx(center, radius, g_CameraPos, g_PrevViewProj, - g_HiZPyramid, smp_nofilter, g_HiZWidth, g_HiZHeight, g_HiZMipLevels); - return float3(r.frontDepth, r.hiZDepth, r.visible ? 1.0 : 0.0); + float3 result = float3(0.0, 0.0, 1.0); + + float4 clipPos = mul(g_ViewProj, float4(center, 1.0)); + + if (clipPos.w <= 0.001) + return float3(0.0, 1.0, 1.0); + + float3 ndc = clipPos.xyz / clipPos.w; + + float projScale = max(abs(g_ViewProj[0][0]), abs(g_ViewProj[1][1])); + float2 ndcSize = float2(radius, radius) * projScale / clipPos.w; + float2 minNDC = ndc.xy - ndcSize; + float2 maxNDC = ndc.xy + ndcSize; + + if (any(minNDC > 1.0) || any(maxNDC < -1.0)) + return float3(1.0, 0.0, 0.0); + + float2 minUV = minNDC * 0.5 + 0.5; + float2 maxUV = maxNDC * 0.5 + 0.5; + minUV.y = 1.0 - minUV.y; + maxUV.y = 1.0 - maxUV.y; + float4 boxUV = float4(min(minUV, maxUV), max(minUV, maxUV)); + + float boxWidth = (boxUV.z - boxUV.x) * float(g_HiZWidth); + float boxHeight = (boxUV.w - boxUV.y) * float(g_HiZHeight); + float mipLevel = floor(log2(max(1.0, max(boxWidth, boxHeight) * 0.5))); + mipLevel = clamp(mipLevel, 0.0, float(g_HiZMipLevels - 1)); + + float d1 = g_HiZPyramid.SampleLevel(smp_nofilter, float2(boxUV.x, boxUV.y), mipLevel); + float d2 = g_HiZPyramid.SampleLevel(smp_nofilter, float2(boxUV.z, boxUV.y), mipLevel); + float d3 = g_HiZPyramid.SampleLevel(smp_nofilter, float2(boxUV.x, boxUV.w), mipLevel); + float d4 = g_HiZPyramid.SampleLevel(smp_nofilter, float2(boxUV.z, boxUV.w), mipLevel); + float hiZDepth = min(min(d1, d2), min(d3, d4)); + + float3 viewDir = normalize(center - g_CameraPos); + float3 frontPoint = center - viewDir * radius; + float4 frontClip = mul(g_ViewProj, float4(frontPoint, 1.0)); + + if (frontClip.w <= 0.001) + return float3(0.0, hiZDepth, 1.0); + + float objectDepth = saturate(frontClip.z / frontClip.w); + + float depthBias = 0.0001; + bool visible = objectDepth + depthBias >= hiZDepth; + + return float3(objectDepth, hiZDepth, visible ? 1.0 : 0.0); } [numthreads(64, 1, 1)] diff --git a/res/gamedata/shaders/r5/particle_occlusion.ps b/res/gamedata/shaders/r5/particle_occlusion.ps new file mode 100644 index 0000000000000000000000000000000000000000..2bc96bd893ae27001943e95c5b31ff83c23a1c87 GIT binary patch literal 1603 zcmb7EYj4^x6#brGao0*!s37CH4{cWoHEczK76E%`(-gUh0c%Md*-n90{P$hQd9M31 z3n6UIJ@=mDdwm|z4T~wP=H3UdYn`*$57U6cna`t$$IkAocJ{>LAf!@y|N2wzh@ioH znTieZsTUFPTk3FYO%Ey+wC~<*LQd517Pj8Xr!oD;MHno&gewqq%cP>hR=w7>wIWdx zQn6A*Q3TJhKqBr!Jgq}z7opa9F2J2E*7x3Iv6{{vAE4{>EK4ep`U(~+Z~CzKvbJ6= zz-5YnU4n`Ax`NeL5368`@%Xxe^g+>s&$$Q?vYvdudwg8F-5OvBJ`cGt90j0P0}Nna zDJ%f}1|a4lB4JiV0T>uS%vp?vBs?U#Fi3F=5gH;G38!wYrs1`9LL+ir5n-`iNCp-s zscIRGYa+H(b#oi$Ih9Kk(Y){xoh}aL&n(cX^8KonJ%oU5HmRiMo-oqN%L{tzjV1oMk0-QbEBFzXcF5sP(=xjo= zm2d&`@!I<|8-Ml|8GWXq-I$g(W^02U6V0QSRyLLQX|TdEo#TkwIjytL z3pO~wtC2ntK{K=j8!wDR9Z|{DyX=|=@XYAmWdbXHW9H%bQZbhyM&nlu`+-3LS7mjG zhYrvq_0Y3yQriW6O*{6Lrsa= z`x?;=bozMJjeyEfEk&<4*PsuJ?!ClfBF@zgpU%YYI^8z(oc^c{RhpVRr>qN3ro{^i iWFYuD<6}_Hgr1gBF)f1Pzi~de1q6F4`{=j&Wc>kCGUUAg literal 0 HcmV?d00001 diff --git a/res/gamedata/shaders/r5/rt_particle_alpha.h b/res/gamedata/shaders/r5/rt_particle_alpha.h new file mode 100644 index 00000000000..d5aadad1e57 --- /dev/null +++ b/res/gamedata/shaders/r5/rt_particle_alpha.h @@ -0,0 +1,44 @@ +#ifndef RT_PARTICLE_ALPHA_H +#define RT_PARTICLE_ALPHA_H + +#include "bindless_common.h" +#include "rt_common.h" + +float2 GetParticleHitUV(ByteAddressBuffer particleVB, ByteAddressBuffer particleIB, + RTBatchInfo info, uint primitiveIndex, float2 barycentrics) +{ + uint triBase = (info.startIndex + primitiveIndex * 3); + uint i0 = particleIB.Load(triBase * 4 + 0) + info.baseVertex; + uint i1 = particleIB.Load(triBase * 4 + 4) + info.baseVertex; + uint i2 = particleIB.Load(triBase * 4 + 8) + info.baseVertex; + + float2 uv0 = asfloat(particleVB.Load2(i0 * 32 + 16)); + float2 uv1 = asfloat(particleVB.Load2(i1 * 32 + 16)); + float2 uv2 = asfloat(particleVB.Load2(i2 * 32 + 16)); + + float w0 = 1.0 - barycentrics.x - barycentrics.y; + return uv0 * w0 + uv1 * barycentrics.x + uv2 * barycentrics.y; +} + +bool ParticleTexelOpaque( + ByteAddressBuffer particleVB, + ByteAddressBuffer particleIB, + StructuredBuffer batchInfo, + uint batchIdx, + uint primIdx, + float2 bary) +{ + RTBatchInfo info = batchInfo[batchIdx]; + MaterialData mat = g_Materials[info.materialID]; + if (mat.flags & MAT_FLAG_EMISSIVE) + return false; + float2 uv = GetParticleHitUV(particleVB, particleIB, info, primIdx, bary); + float4 diffuse = SampleDiffuseLevel(mat, uv); + if (mat.flags & MAT_FLAG_ALPHA_TEST) + return diffuse.a >= mat.alphaRef; + if (mat.flags & MAT_FLAG_ALPHA_BLEND) + return diffuse.a >= 0.5; + return diffuse.a >= 0.5; +} + +#endif diff --git a/src/Layers/xrRender/FrameGraphPasses/GlowPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/GlowPassSetup.cpp index d484eeeb714..a6bac352ecb 100644 --- a/src/Layers/xrRender/FrameGraphPasses/GlowPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/GlowPassSetup.cpp @@ -26,8 +26,8 @@ void EnsureGlowResources(nvrhi::IDevice* nv, GlowPassState& state) auto* shaderLoader = RImplementation.GetShaderLoader(); if (!shaderLoader) return; - auto vsResult = shaderLoader->LoadVertexShader("sun_forward"); - auto psResult = shaderLoader->LoadPixelShader("sun_forward"); + auto vsResult = shaderLoader->LoadVertexShader("glow_forward"); + auto psResult = shaderLoader->LoadPixelShader("glow_forward"); if (!vsResult.handle || !psResult.handle) return; state.vs = vsResult.handle; @@ -35,7 +35,7 @@ void EnsureGlowResources(nvrhi::IDevice* nv, GlowPassState& state) auto& cache = GetPassResourceCache(); state.bindingLayout = cache.GetOrCreateBindingLayoutFromReflection( - "GlowBillboard", *vsResult.reflection, *psResult.reflection, nv); + "GlowBillboard_v3", *vsResult.reflection, *psResult.reflection, nv); if (!state.bindingLayout) return; @@ -65,7 +65,7 @@ void EnsureGlowResources(nvrhi::IDevice* nv, GlowPassState& state) rs.blendState.targets[0].setBlendOp(nvrhi::BlendOp::Add); rs.depthStencilState.setDepthTestEnable(true); rs.depthStencilState.setDepthWriteEnable(false); - rs.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + rs.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::GreaterOrEqual); rs.rasterState.setCullMode(nvrhi::RasterCullMode::None); nvrhi::GraphicsPipelineDesc pso; @@ -79,7 +79,7 @@ void EnsureGlowResources(nvrhi::IDevice* nv, GlowPassState& state) nvrhi::FramebufferInfoEx fbInfo; fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); fbInfo.depthFormat = nvrhi::Format::D32; - state.pipeline = cache.GetOrCreatePipeline("GlowBillboard", pso, fbInfo, nv); + state.pipeline = cache.GetOrCreatePipeline("GlowBillboard_v3", pso, fbInfo, nv); u16 indices[6] = { 0, 1, 2, 2, 1, 3 }; nvrhi::BufferDesc ibDesc; @@ -229,13 +229,16 @@ framegraph::VirtualResourceHandle setupGlowBillboardPass( return; auto& cache = GetPassResourceCache(); - auto* vsRefl = RImplementation.GetShaderLoader()->GetCachedReflection("sun_forward", ".vs"); - auto* psRefl = RImplementation.GetShaderLoader()->GetCachedReflection("sun_forward", ".ps"); + auto* vsRefl = RImplementation.GetShaderLoader()->GetCachedReflection("glow_forward", ".vs"); + auto* psRefl = RImplementation.GetShaderLoader()->GetCachedReflection("glow_forward", ".ps"); if (!vsRefl || !psRefl) return; auto dynamicCBBuffer = cache.GetOrCreateVolatileCB( - "Frame", "DynamicTransforms", sizeof(DynamicTransforms), renderDevice); + "GlowBillboard", "DynamicTransforms", sizeof(DynamicTransforms), renderDevice); + DynamicTransforms dynCB{}; + FillDynamicTransforms(dynCB, Fidentity); + cmd->writeBuffer(dynamicCBBuffer, &dynCB, sizeof(dynCB)); Fvector right = Device.vCameraRight; Fvector up = Device.vCameraTop; diff --git a/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp index 29cb9907637..ce6b3b55358 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.cpp @@ -22,6 +22,7 @@ #include "Layers/xrRender/ClusteredLightManager.h" #include "xrParticles/psystem.h" #include "xrCDB/Frustum.h" // For CFrustum (frustum plane extraction) +#include extern ENGINE_API float psHUD_FOV; @@ -205,12 +206,52 @@ static Fmatrix BuildHUDFOVMatrix() return result; } -static u32 GenerateParticleVertices( +void FilterRTParticleBatches(const xr_vector& src, xr_vector& out) +{ + constexpr float kRTParticleMaxDist = 20.f; + constexpr u32 kRTParticleMaxBatches = 8; + + struct Cand { + const ParticleBatch* batch; + float dist; + }; + xr_vector cands; + cands.reserve(src.size()); + + const Fvector& cam = Device.vCameraPosition; + for (const auto& b : src) { + if (b.isHUDMode) + continue; + if (b.shaderVariant == ParticleShaderVariant::Distort) + continue; + if (!b.visual) + continue; + const float dist = cam.distance_to(b.visual->vis.sphere.P) - b.visual->vis.sphere.R; + if (dist > kRTParticleMaxDist) + continue; + cands.push_back({&b, dist > 0.f ? dist : 0.f}); + } + + std::sort(cands.begin(), cands.end(), [](const Cand& a, const Cand& b) { + return a.dist < b.dist; + }); + + out.clear(); + const u32 n = std::min((u32)cands.size(), kRTParticleMaxBatches); + out.reserve(n); + for (u32 i = 0; i < n; i++) + out.push_back(*cands[i].batch); +} + +u32 GenerateParticleVertices( const xr_vector& batches, xr_vector& vertices, - xr_vector* outCounts = nullptr) + xr_vector* outCounts, + float maxDistance) { u32 totalParticles = 0; + const float maxDistSq = maxDistance > 0.f ? maxDistance * maxDistance : 0.f; + const Fvector& cam = Device.vCameraPosition; for (const auto& batch : batches) { if (outCounts) @@ -248,10 +289,19 @@ static u32 GenerateParticleVertices( float sina = 0.0f, cosa = 0.0f; float angle = float(0xFFFFFFFF); + u32 written = 0; for (u32 i = 0; i < particleCount; i++) { auto& m = particles[i]; + if (maxDistSq > 0.f) { + Fvector wp = m.pos; + if (hasXForm) + xform.transform_tiny(wp, m.pos); + if (cam.distance_to_sqr(wp) > maxDistSq) + continue; + } + if (angle != m.rot.x) { angle = m.rot.x; sina = _sin(angle); @@ -346,11 +396,13 @@ static u32 GenerateParticleVertices( v->p = tmp; } } + written++; } + vertices.resize(baseVertex + written * 4); if (outCounts) - outCounts->back() = particleCount; - totalParticles += particleCount; + outCounts->back() = written; + totalParticles += written; } return totalParticles; @@ -587,7 +639,7 @@ static void InitializeDistortionPipeline(fg::RenderDevice* device, const nvrhi:: pipeDesc.renderState.blendState.targets[0].srcBlendAlpha = nvrhi::BlendFactor::One; pipeDesc.renderState.blendState.targets[0].destBlendAlpha = nvrhi::BlendFactor::One; - state.distortPipeline = cache.GetOrCreatePipeline("ParticlePass_distort", pipeDesc, fbInfo, nvDevice); + state.distortPipeline = cache.GetOrCreatePipeline("ParticlePass_distort_v2", pipeDesc, fbInfo, nvDevice); if (state.distortPipeline) { const auto& actualDesc = state.distortPipeline->getDesc(); @@ -613,7 +665,8 @@ ParticlePassOutput setupParticlePass( u32 hiZMipLevels, const Fmatrix* prevViewProj, VirtualResourceHandle prevDepth, - ParticlePassState* state) + ParticlePassState* state, + VirtualResourceHandle seedDistortion) { if (state) { nvrhi::FramebufferInfoEx fbInfo; @@ -631,7 +684,7 @@ ParticlePassOutput setupParticlePass( auto& passData = fg.addCallbackPass( "Particles", - [&, width, height, hiZPyramid, hiZWidth, hiZHeight, hiZMipLevels, state](FrameGraph& builder, PassHandle passHandle, ParticlePassData& data) { + [&, width, height, hiZPyramid, hiZWidth, hiZHeight, hiZMipLevels, state, seedDistortion](FrameGraph& builder, PassHandle passHandle, ParticlePassData& data) { RenderPassBuilder passBuilder(builder, passHandle); data.width = width; @@ -657,19 +710,29 @@ ParticlePassOutput setupParticlePass( if (b.shaderVariant == ParticleShaderVariant::Distort) return true; return false; }; - data.hasDistortion = hasDistortBatch(worldParticleBatches) || hasDistortBatch(hudParticleBatches); + const bool particleDistort = + hasDistortBatch(worldParticleBatches) || hasDistortBatch(hudParticleBatches); + data.importedDistortion = seedDistortion.is_valid(); + data.hasDistortion = particleDistort || data.importedDistortion; + data.seedDistortion = {}; if (data.hasDistortion) { - framegraph::ResourceDesc distDesc; - distDesc.type = framegraph::ResourceDesc::Type::Texture2D; - distDesc.width = width; - distDesc.height = height; - distDesc.format = nvrhi::Format::RGBA16_FLOAT; - distDesc.isRenderTarget = true; - distDesc.isTransient = true; - distDesc.isUAV = true; - distDesc.debugName = "rt_Distortion"; - data.distortionRT = passBuilder.createTexture("rt_Distortion", distDesc); + if (data.importedDistortion && !particleDistort) { + data.distortionRT = passBuilder.readWrite(seedDistortion, ResourceState::RenderTarget); + } else { + framegraph::ResourceDesc distDesc; + distDesc.type = framegraph::ResourceDesc::Type::Texture2D; + distDesc.width = width; + distDesc.height = height; + distDesc.format = nvrhi::Format::RGBA16_FLOAT; + distDesc.isRenderTarget = true; + distDesc.isTransient = true; + distDesc.isUAV = true; + distDesc.debugName = "rt_Distortion"; + data.distortionRT = passBuilder.createTexture("rt_Distortion", distDesc); + if (data.importedDistortion) + data.seedDistortion = passBuilder.read(seedDistortion, ResourceState::CopySource); + } } if (hiZPyramid.is_valid()) @@ -681,12 +744,15 @@ ParticlePassOutput setupParticlePass( data.depth = passBuilder.readWrite(forwardInputs.depth, ResourceState::DepthStencilWrite); if (forwardInputs.baseColor.is_valid()) data.baseColor = passBuilder.readWrite(forwardInputs.baseColor, ResourceState::RenderTarget); + if (forwardInputs.worldPos.is_valid()) + data.worldPos = passBuilder.read(forwardInputs.worldPos, ResourceState::ShaderResource); if (prevDepth.is_valid()) data.prevDepth = passBuilder.read(prevDepth, ResourceState::ShaderResource); data.outputs.albedo = data.outputColor; data.outputs.normal = data.outputNormal; data.outputs.baseColor = data.baseColor; + data.outputs.worldPos = forwardInputs.worldPos; data.outputs.depth = data.depth; }, [](const ParticlePassData& data, const FrameGraph& fg, fg::RenderContext* ctx) { @@ -716,14 +782,14 @@ ParticlePassOutput setupParticlePass( matBuffer.Upload(ctx); auto* baseColorRT = data.baseColor.is_valid() ? fg.GetPhysicalTexture(data.baseColor) : nullptr; - auto* prevDepthTex = data.prevDepth.is_valid() ? fg.GetPhysicalTexture(data.prevDepth) : depthRT; + auto* worldPosTex = data.worldPos.is_valid() ? fg.GetPhysicalTexture(data.worldPos) : nullptr; + if (!normalRT || !baseColorRT) + return; nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(colorRT); - if (normalRT) - fbDesc.addColorAttachment(normalRT); - if (baseColorRT) - fbDesc.addColorAttachment(baseColorRT); + fbDesc.addColorAttachment(normalRT); + fbDesc.addColorAttachment(baseColorRT); fbDesc.setDepthAttachment(depthRT); auto& cache = framegraph::GetPassResourceCache(); auto framebuffer = cache.GetOrCreateFramebuffer("ParticlePass", fbDesc, nvDevice); @@ -736,14 +802,16 @@ ParticlePassOutput setupParticlePass( const auto& rtDesc = colorRT->getDesc(); auto staticGlobalsCB = cache.GetOrCreateVolatileCB("Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + if (!worldPosTex) + worldPosTex = cache.GetDummyContactHistory(nvDevice); auto* shaderLoader = GEnv.Render->GetShaderLoader(); auto* vsReflection = shaderLoader->GetCachedReflection("bindless_particle", ".vs"); auto* psReflection = shaderLoader->GetCachedReflection("bindless_particle", ".ps"); BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "Particle"); bsb.ConstantBuffer("static_globals", staticGlobalsCB) - .BufferSRV("g_Materials", matBuffer.GetBuffer()) - .Texture("g_SceneDepth", prevDepthTex); + .Texture("g_SceneWorldPos", worldPosTex); + BindBindlessMaterialTables(bsb); auto bindDesc = bsb.Build(); auto bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bindDesc, data.passState->layout, nvDevice); @@ -755,8 +823,8 @@ ParticlePassOutput setupParticlePass( return nullptr; BindingSetBuilder lbsb(*vsReflection, *litPsReflection, nvDevice, psName); lbsb.ConstantBuffer("static_globals", staticGlobalsCB) - .BufferSRV("g_Materials", matBuffer.GetBuffer()) - .Texture("g_SceneDepth", prevDepthTex); + .Texture("g_SceneWorldPos", worldPosTex); + BindBindlessMaterialTables(lbsb); auto& clm = ClusteredLightManager::Instance(); if (clm.GetLightDataBuffer()) lbsb.BufferSRV("g_LightData", clm.GetLightDataBuffer()); @@ -789,9 +857,11 @@ ParticlePassOutput setupParticlePass( { xr_vector filtered; for (const auto& b : allBatches) { - if (b.blendMode == blendMode && b.shaderVariant == ParticleShaderVariant::Standard && - b.lightingMode == PS::ParticleLightingMode::Emissive) - filtered.push_back(b); + if (b.blendMode != blendMode || b.shaderVariant == ParticleShaderVariant::Distort) + continue; + if (b.lightingMode != PS::ParticleLightingMode::Emissive) + continue; + filtered.push_back(b); } if (filtered.empty()) return; @@ -931,9 +1001,11 @@ ParticlePassOutput setupParticlePass( for (u8 mode : s_renderOrder) { xr_vector filtered; for (const auto& b : *data.worldParticleBatches) { - if (b.blendMode == mode && b.shaderVariant == ParticleShaderVariant::Standard && - b.lightingMode == PS::ParticleLightingMode::Emissive) - filtered.push_back(b); + if (b.blendMode != mode || b.shaderVariant == ParticleShaderVariant::Distort) + continue; + if (b.lightingMode != PS::ParticleLightingMode::Emissive) + continue; + filtered.push_back(b); } if (filtered.empty()) continue; @@ -1137,6 +1209,26 @@ ParticlePassOutput setupParticlePass( if (!distortRT) return; + auto* seedTex = data.seedDistortion.is_valid() + ? fg.GetPhysicalTexture(data.seedDistortion) + : nullptr; + if (seedTex && seedTex != distortRT) + cmdList->copyTexture(distortRT, nvrhi::TextureSlice(), seedTex, nvrhi::TextureSlice()); + else if (!data.importedDistortion) + cmdList->clearTextureFloat(distortRT, nvrhi::AllSubresources, nvrhi::Color(0.f, 0.f, 0.f, 0.f)); + + bool hasParticleDistort = false; + auto checkDistort = [&](const xr_vector* batches) { + if (!batches) return; + for (const auto& b : *batches) + if (b.shaderVariant == ParticleShaderVariant::Distort) + hasParticleDistort = true; + }; + checkDistort(data.worldParticleBatches); + checkDistort(data.hudParticleBatches); + if (!hasParticleDistort) + return; + nvrhi::FramebufferDesc distortFbDesc; distortFbDesc.addColorAttachment(distortRT); distortFbDesc.setDepthAttachment(depthRT); @@ -1147,13 +1239,11 @@ ParticlePassOutput setupParticlePass( if (!data.passState->distortPipeline) return; - cmdList->clearTextureFloat(distortRT, nvrhi::AllSubresources, nvrhi::Color(0.f, 0.f, 0.f, 0.f)); - auto* distortVsReflection = shaderLoader->GetCachedReflection("bindless_particle", ".vs"); auto* distortPsReflection = shaderLoader->GetCachedReflection("bindless_particle_distort", ".ps"); BindingSetBuilder distortBsb(*distortVsReflection, *distortPsReflection, nvDevice, "Particle.Distort"); - distortBsb.ConstantBuffer("static_globals", staticGlobalsCB) - .BufferSRV("g_Materials", matBuffer.GetBuffer()); + distortBsb.ConstantBuffer("static_globals", staticGlobalsCB); + BindBindlessMaterialTables(distortBsb); auto distortBindDesc = distortBsb.Build(); auto distortBindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet( distortBindDesc, data.passState->distortLayout, nvDevice); @@ -1222,4 +1312,256 @@ ParticlePassOutput setupParticlePass( return output; } +static void InitParticleOcclusionPipeline( + fg::RenderDevice* device, + ParticleOcclusionPassState& state, + u32 width, + u32 height) +{ + if (state.initialized) + return; + + nvrhi::IDevice* nvDevice = device->GetNVRHIDevice(); + if (!nvDevice) + return; + + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + if (!shaderLoader) + return; + + auto* backend = device->GetBackend(); + nvrhi::IBindingLayout* bindlessLayout = backend ? backend->GetBindlessLayout() : nullptr; + auto& cache = framegraph::GetPassResourceCache(); + + auto vsResult = shaderLoader->LoadVertexShader("bindless_particle", "main"); + auto psResult = shaderLoader->LoadPixelShader("particle_occlusion", "main"); + if (!vsResult.handle || !psResult.handle || !vsResult.reflection || !psResult.reflection) + return; + + state.vs = vsResult.handle; + state.ps = psResult.handle; + state.layout = cache.GetOrCreateBindingLayoutFromReflection( + "ParticleOcclusion", *vsResult.reflection, *psResult.reflection, nvDevice); + + nvrhi::SamplerDesc samplerDesc; + samplerDesc.setAllAddressModes(nvrhi::SamplerAddressMode::Clamp); + samplerDesc.setAllFilters(true); + state.sampler = nvDevice->createSampler(samplerDesc); + + constexpr u32 stride = sizeof(ParticleVertex); + nvrhi::VertexAttributeDesc attribs[] = { + nvrhi::VertexAttributeDesc().setName("POSITION").setFormat(nvrhi::Format::RGB32_FLOAT).setBufferIndex(0).setOffset(0).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("COLOR").setFormat(nvrhi::Format::BGRA8_UNORM).setBufferIndex(0).setOffset(12).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("TEXCOORD").setFormat(nvrhi::Format::RG32_FLOAT).setBufferIndex(0).setOffset(16).setElementStride(stride), + nvrhi::VertexAttributeDesc().setName("MATERIALID").setFormat(nvrhi::Format::R32_UINT).setBufferIndex(0).setOffset(24).setElementStride(stride), + }; + state.inputLayout = nvDevice->createInputLayout(attribs, 4, state.vs); + + nvrhi::FramebufferInfoEx fbInfo; + fbInfo.width = width; + fbInfo.height = height; + fbInfo.colorFormats = { nvrhi::Format::R16_FLOAT }; + + nvrhi::GraphicsPipelineDesc pipeDesc; + pipeDesc.VS = state.vs; + pipeDesc.PS = state.ps; + pipeDesc.inputLayout = state.inputLayout; + pipeDesc.bindingLayouts.push_back(state.layout); + if (bindlessLayout) + pipeDesc.bindingLayouts.push_back(bindlessLayout); + pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; + pipeDesc.renderState.depthStencilState.depthTestEnable = false; + pipeDesc.renderState.depthStencilState.depthWriteEnable = false; + pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; + pipeDesc.renderState.blendState.targets[0].enableBlend(); + pipeDesc.renderState.blendState.targets[0].srcBlend = nvrhi::BlendFactor::One; + pipeDesc.renderState.blendState.targets[0].destBlend = nvrhi::BlendFactor::One; + pipeDesc.renderState.blendState.targets[0].srcBlendAlpha = nvrhi::BlendFactor::One; + pipeDesc.renderState.blendState.targets[0].destBlendAlpha = nvrhi::BlendFactor::One; + + state.pipeline = cache.GetOrCreatePipeline("ParticleOcclusion", pipeDesc, fbInfo, nvDevice); + if (state.pipeline && !state.pipeline->getDesc().bindingLayouts.empty()) + state.layout = state.pipeline->getDesc().bindingLayouts[0]; + + state.initialized = state.pipeline != nullptr; + if (state.initialized) + Msg("* [ParticleOcclusion] Half-res opacity pass ready"); +} + +framegraph::VirtualResourceHandle setupParticleOcclusionPass( + FrameGraph& fg, + fg::RenderDevice* device, + VirtualResourceHandle worldPos, + const xr_vector* worldParticleBatches, + u32 width, + u32 height, + ParticleOcclusionPassState& state) +{ + const u32 genW = std::max(1u, (width + 1) / 2); + const u32 genH = std::max(1u, (height + 1) / 2); + InitParticleOcclusionPipeline(device, state, genW, genH); + + ResourceDesc opacityDesc; + opacityDesc.type = ResourceDesc::Type::Texture2D; + opacityDesc.width = genW; + opacityDesc.height = genH; + opacityDesc.format = nvrhi::Format::R16_FLOAT; + opacityDesc.isRenderTarget = true; + opacityDesc.isTransient = true; + opacityDesc.debugName = "rt_ParticleOpacity"; + auto opacityRT = fg.CreateTexture("rt_ParticleOpacity", opacityDesc); + + struct OccPassData { + fg::RenderDevice* device = nullptr; + ParticleOcclusionPassState* passState = nullptr; + const xr_vector* worldBatches = nullptr; + VirtualResourceHandle worldPos; + VirtualResourceHandle opacity; + u32 genW = 0; + u32 genH = 0; + }; + + fg.addCallbackPass( + "Particle Occlusion SS", + [&, worldPos, opacityRT, worldParticleBatches, genW, genH](FrameGraph& builder, PassHandle passHandle, OccPassData& data) { + RenderPassBuilder pb(builder, passHandle); + data.device = device; + data.passState = &state; + data.worldBatches = worldParticleBatches; + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); + data.opacity = pb.write(opacityRT, ResourceState::RenderTarget); + data.genW = genW; + data.genH = genH; + }, + [](const OccPassData& data, const FrameGraph& fgGraph, fg::RenderContext* ctx) { + auto* ps = data.passState; + if (!ps || !ps->initialized || !ps->pipeline) + return; + + auto* opacityTex = fgGraph.GetPhysicalTexture(data.opacity); + auto* worldPosTex = data.worldPos.is_valid() ? fgGraph.GetPhysicalTexture(data.worldPos) : nullptr; + if (!opacityTex) + return; + + nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); + auto& cache = framegraph::GetPassResourceCache(); + if (!worldPosTex) + worldPosTex = cache.GetDummyContactHistory(nvDevice); + + nvrhi::FramebufferDesc fbDesc; + fbDesc.addColorAttachment(opacityTex); + auto framebuffer = cache.GetOrCreateFramebuffer("ParticleOcclusion", fbDesc, nvDevice); + if (!framebuffer) + return; + + nvrhi::utils::ClearColorAttachment(cmdList, framebuffer, 0, nvrhi::Color(0.f)); + + if (!data.worldBatches || data.worldBatches->empty()) + return; + + xr_vector filtered; + FilterRTParticleBatches(*data.worldBatches, filtered); + if (filtered.empty()) + return; + + constexpr float kMaxDist = 20.f; + constexpr u32 kMaxQuads = 256u; + xr_vector vertices; + GenerateParticleVertices(filtered, vertices, nullptr, kMaxDist); + if (vertices.empty()) + return; + + u32 totalParticles = (u32)vertices.size() / 4; + if (totalParticles > kMaxQuads) { + totalParticles = kMaxQuads; + vertices.resize(totalParticles * 4); + } + + const u32 vbBytes = (u32)(vertices.size() * sizeof(ParticleVertex)); + if (!ps->particleVB || ps->particleVBSize < vbBytes) { + nvrhi::BufferDesc vbDesc; + vbDesc.byteSize = std::max(vbBytes, 64u * 1024u); + vbDesc.isVertexBuffer = true; + vbDesc.debugName = "ParticleOcclusionVB"; + vbDesc.initialState = nvrhi::ResourceStates::VertexBuffer; + vbDesc.keepInitialState = true; + ps->particleVB = nvDevice->createBuffer(vbDesc); + ps->particleVBSize = ps->particleVB ? (u32)vbDesc.byteSize : 0; + } + if (!ps->quadIB || ps->maxQuads < totalParticles) { + u32 numIndices = totalParticles * 6; + xr_vector indices(numIndices); + for (u32 i = 0; i < totalParticles; i++) { + u16 b = (u16)(i * 4); + indices[i * 6 + 0] = b; + indices[i * 6 + 1] = b + 1; + indices[i * 6 + 2] = b + 2; + indices[i * 6 + 3] = b; + indices[i * 6 + 4] = b + 2; + indices[i * 6 + 5] = b + 3; + } + nvrhi::BufferDesc ibDesc; + ibDesc.byteSize = numIndices * sizeof(u16); + ibDesc.isIndexBuffer = true; + ibDesc.debugName = "ParticleOcclusionIB"; + ibDesc.initialState = nvrhi::ResourceStates::IndexBuffer; + ibDesc.keepInitialState = true; + ps->quadIB = nvDevice->createBuffer(ibDesc); + if (ps->quadIB) { + cmdList->writeBuffer(ps->quadIB, indices.data(), indices.size() * sizeof(u16)); + ps->maxQuads = totalParticles; + } + } + if (!ps->particleVB || !ps->quadIB) + return; + + cmdList->writeBuffer(ps->particleVB, vertices.data(), vbBytes); + + auto& matBuffer = MaterialBuffer::Instance(); + matBuffer.Upload(ctx); + + auto staticGlobalsCB = cache.GetOrCreateVolatileCB( + "Frame", "StaticGlobals", sizeof(StaticGlobals), data.device); + + auto* shaderLoader = GEnv.Render->GetShaderLoader(); + auto* vsReflection = shaderLoader->GetCachedReflection("bindless_particle", ".vs"); + auto* psReflection = shaderLoader->GetCachedReflection("particle_occlusion", ".ps"); + if (!vsReflection || !psReflection) + return; + + BindingSetBuilder bsb(*vsReflection, *psReflection, nvDevice, "ParticleOcclusion"); + bsb.ConstantBuffer("static_globals", staticGlobalsCB) + .Texture("g_SceneWorldPos", worldPosTex); + BindBindlessMaterialTables(bsb); + auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), ps->layout, nvDevice); + if (!bindingSet) + return; + + auto* backend = data.device->GetBackend(); + nvrhi::IDescriptorTable* bindlessTable = backend ? backend->GetBindlessDescriptorTable() : nullptr; + + nvrhi::GraphicsState gfxState; + gfxState.pipeline = ps->pipeline; + gfxState.framebuffer = framebuffer; + gfxState.viewport.addViewport(nvrhi::Viewport((float)data.genW, (float)data.genH)); + gfxState.viewport.addScissorRect(nvrhi::Rect(data.genW, data.genH)); + gfxState.bindings = { bindingSet }; + if (bindlessTable) + gfxState.addBindingSet(bindlessTable); + gfxState.vertexBuffers = { { ps->particleVB, 0, 0 } }; + gfxState.indexBuffer = { ps->quadIB, nvrhi::Format::R16_UINT, 0 }; + + cmdList->setGraphicsState(gfxState); + cmdList->drawIndexed( + nvrhi::DrawArguments() + .setVertexCount(totalParticles * 6) + .setStartIndexLocation(0) + .setStartVertexLocation(0)); + } + ); + + return opacityRT; +} + } // namespace xray::render::fg::passes diff --git a/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.h index dc11e7927c7..9ddc96284c6 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/ParticlePassSetup.h @@ -110,8 +110,10 @@ struct ParticlePassData { framegraph::VirtualResourceHandle outputColor; framegraph::VirtualResourceHandle outputNormal; framegraph::VirtualResourceHandle baseColor; + framegraph::VirtualResourceHandle worldPos; framegraph::VirtualResourceHandle hiZPyramid; framegraph::VirtualResourceHandle distortionRT; + framegraph::VirtualResourceHandle seedDistortion; framegraph::VirtualResourceHandle prevDepth; fg::RenderDevice* device; const xr_vector* worldParticleBatches; @@ -145,6 +147,36 @@ void InitializeParticleResources(fg::RenderDevice* device, const nvrhi::Framebuf // Renders AFTER forward color and skinning passes (particles on top of world+HUD) // Supports both world and HUD particles with proper FOV handling // When hiZPyramid is valid, uses GPU frustum + occlusion culling +void FilterRTParticleBatches(const xr_vector& src, xr_vector& out); +u32 GenerateParticleVertices( + const xr_vector& batches, + xr_vector& vertices, + xr_vector* outCounts = nullptr, + float maxDistance = 0.f); + +struct ParticleOcclusionPassState { + nvrhi::GraphicsPipelineHandle pipeline; + nvrhi::BindingLayoutHandle layout; + nvrhi::InputLayoutHandle inputLayout; + nvrhi::ShaderHandle vs; + nvrhi::ShaderHandle ps; + nvrhi::SamplerHandle sampler; + nvrhi::BufferHandle particleVB; + u32 particleVBSize = 0; + nvrhi::BufferHandle quadIB; + u32 maxQuads = 0; + bool initialized = false; +}; + +framegraph::VirtualResourceHandle setupParticleOcclusionPass( + framegraph::FrameGraph& fg, + fg::RenderDevice* device, + framegraph::VirtualResourceHandle worldPos, + const xr_vector* worldParticleBatches, + u32 width, + u32 height, + ParticleOcclusionPassState& state); + ParticlePassOutput setupParticlePass( framegraph::FrameGraph& fg, fg::RenderDevice* device, @@ -154,13 +186,14 @@ ParticlePassOutput setupParticlePass( MaterialCache* materialCache, u32 width, u32 height, - framegraph::VirtualResourceHandle hiZPyramid = {}, // Hi-Z pyramid for GPU culling + framegraph::VirtualResourceHandle hiZPyramid = {}, u32 hiZWidth = 0, u32 hiZHeight = 0, u32 hiZMipLevels = 0, const Fmatrix* prevViewProj = nullptr, framegraph::VirtualResourceHandle prevDepth = {}, - ParticlePassState* state = nullptr + ParticlePassState* state = nullptr, + framegraph::VirtualResourceHandle seedDistortion = {} ); } // namespace xray::render::fg::passes From d5f8f09c7b8d2bd715904ab1554961b3b969a222 Mon Sep 17 00:00:00 2001 From: FoxConED Date: Thu, 30 Jul 2026 00:01:36 +0300 Subject: [PATCH 259/261] xrRender: extend ReSTIR GI stack and RT quality console knobs --- res/gamedata/shaders/r5/nrd_composite.cs | 2 +- res/gamedata/shaders/r5/nrd_pack_inputs.cs | 2 +- res/gamedata/shaders/r5/restir_gi_blur.cs | 64 ++-- .../shaders/r5/restir_gi_composite.cs | 41 ++- res/gamedata/shaders/r5/rt_grass_billboard.cs | 31 +- res/gamedata/shaders/r5/rt_volumetric.cs | 17 +- res/gamedata/shaders/r5/shadow_mask.cs | 2 +- .../FrameGraphPasses/ReSTIRGIPassSetup.cpp | 312 +++++++++++++----- .../FrameGraphPasses/ReSTIRGIPassSetup.h | 6 +- .../FrameGraphPasses/ShaderConstants.h | 53 ++- src/Layers/xrRender/xrRender_console.cpp | 85 ++++- src/Layers/xrRender/xrRender_console.h | 2 +- src/xrEngine/xr_ioc_cmd.cpp | 13 +- src/xrEngine/xr_ioc_cmd.h | 5 + 14 files changed, 490 insertions(+), 145 deletions(-) diff --git a/res/gamedata/shaders/r5/nrd_composite.cs b/res/gamedata/shaders/r5/nrd_composite.cs index ee49f98561a..116d7fba55e 100644 --- a/res/gamedata/shaders/r5/nrd_composite.cs +++ b/res/gamedata/shaders/r5/nrd_composite.cs @@ -36,7 +36,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float depth = t_Depth.Load(int3(pixel, 0)); float guideMark = t_WorldPos.Load(int3(pixel, 0)).w; float classifyMark = t_ClassifyWorldPos.Load(int3(pixel, 0)).w; - if (depth >= 1.0 || SkipRtSurfLighting(classifyMark, guideMark)) { + if (depth <= 0.0 || SkipRtSurfLighting(classifyMark, guideMark)) { u_SceneColor[pixel] = t_SceneColorIn.Load(int3(pixel, 0)); u_OutDiffuse[pixel] = 0; u_OutSpecular[pixel] = 0; diff --git a/res/gamedata/shaders/r5/nrd_pack_inputs.cs b/res/gamedata/shaders/r5/nrd_pack_inputs.cs index b8cecb912e5..3d602eae4ad 100644 --- a/res/gamedata/shaders/r5/nrd_pack_inputs.cs +++ b/res/gamedata/shaders/r5/nrd_pack_inputs.cs @@ -55,7 +55,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float3 cameraPos = g_CameraPos_Range.xyz; float depth = t_Depth.Load(int3(pixel, 0)); - if (depth >= 1.0) { + if (depth <= 0.0) { u_DiffRadianceHitDist[pixel] = 0; u_SpecRadianceHitDist[pixel] = 0; u_NormalRoughness[pixel] = float4(0.5, 0.5, 1.0, 1.0); diff --git a/res/gamedata/shaders/r5/restir_gi_blur.cs b/res/gamedata/shaders/r5/restir_gi_blur.cs index cee1ea40409..30691a4f11f 100644 --- a/res/gamedata/shaders/r5/restir_gi_blur.cs +++ b/res/gamedata/shaders/r5/restir_gi_blur.cs @@ -54,18 +54,23 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float guideMark = t_WorldPos.Load(int3(pixel, 0)).w; float classifyMark = t_ClassifyWorldPos.Load(int3(pixel, 0)).w; - if (depth >= 1.0 || SkipRtSurfLighting(classifyMark, guideMark)) { + if (depth <= 0.0 || SkipRtSurfLighting(classifyMark, guideMark)) { u_SceneColor[pixel] = t_SceneColorIn.Load(int3(pixel, 0)); u_FilteredDiffuse[pixel] = 0; u_FilteredSpecular[pixel] = 0; return; } - float3 N = normalize(t_Normal.Load(int3(pixel, 0)).xyz); + float4 nPack = t_Normal.Load(int3(pixel, 0)); + float3 N = normalize(nPack.xyz); + float roughness = saturate(nPack.w); float centerLumaD = Luma(noisyD); float centerLumaS = Luma(noisyS); float directLuma = max(Luma(direct), 0.02); int step = (int)max(g_Step, 1u); + float hitBlur = saturate(specHitDist * 0.04); + float roughBlur = saturate(roughness * roughness * 2.5 + hitBlur); + int stepS = max(step, (int)ceil((float)step * (1.0 + roughBlur * 2.5))); float3 sumD = 0; float3 sumS = 0; @@ -78,37 +83,46 @@ void main(uint3 dispatchID : SV_DispatchThreadID) [unroll] for (int iy = -2; iy <= 2; ++iy) { [unroll] for (int ix = -2; ix <= 2; ++ix) { - int2 np = int2(pixel) + int2(ix, iy) * step; - if (np.x < 0 || np.y < 0 || np.x >= (int)g_ScreenSize.x || np.y >= (int)g_ScreenSize.y) + int2 npD = int2(pixel) + int2(ix, iy) * step; + int2 npS = int2(pixel) + int2(ix, iy) * stepS; + if (npD.x >= 0 && npD.y >= 0 && npD.x < (int)g_ScreenSize.x && npD.y < (int)g_ScreenSize.y) + { + float nd = t_Depth.Load(int3(npD, 0)); + if (nd > 0.0 && SameHudSurfClass(guideMark, t_WorldPos.Load(int3(npD, 0)).w)) + { + float3 nN = normalize(t_Normal.Load(int3(npD, 0)).xyz); + float3 nD = SoftClampFirefly(t_NoisyDiffuse.Load(int3(npD, 0)).rgb, directLuma, 5.0); + nbMinD = min(nbMinD, nD); + nbMaxD = max(nbMaxD, nD); + float w = kKernel[ix + 2] * kKernel[iy + 2]; + float wGeo = exp(-abs(depth - nd) * g_PhiDepth); + wGeo *= pow(saturate(dot(N, nN)), g_PhiNormal); + w *= max(wGeo, 0.05); + float wD = w * exp(-abs(Luma(nD) - centerLumaD) * 1.25); + sumD += nD * wD; + wSumD += wD; + } + } + + if (npS.x < 0 || npS.y < 0 || npS.x >= (int)g_ScreenSize.x || npS.y >= (int)g_ScreenSize.y) continue; - - float nd = t_Depth.Load(int3(np, 0)); - if (nd >= 1.0) + float ndS = t_Depth.Load(int3(npS, 0)); + if (ndS <= 0.0) continue; - if (!SameHudSurfClass(guideMark, t_WorldPos.Load(int3(np, 0)).w)) + if (!SameHudSurfClass(guideMark, t_WorldPos.Load(int3(npS, 0)).w)) continue; - float3 nN = normalize(t_Normal.Load(int3(np, 0)).xyz); - float3 nD = t_NoisyDiffuse.Load(int3(np, 0)).rgb; - float3 nS = t_NoisySpecular.Load(int3(np, 0)).rgb; - nD = SoftClampFirefly(nD, directLuma, 5.0); - nS = SoftClampFirefly(nS, directLuma, 6.5); - - nbMinD = min(nbMinD, nD); - nbMaxD = max(nbMaxD, nD); + float3 nNs = normalize(t_Normal.Load(int3(npS, 0)).xyz); + float3 nS = SoftClampFirefly(t_NoisySpecular.Load(int3(npS, 0)).rgb, directLuma, 6.5); nbMinS = min(nbMinS, nS); nbMaxS = max(nbMaxS, nS); - float w = kKernel[ix + 2] * kKernel[iy + 2]; - float wGeo = exp(-abs(depth - nd) * g_PhiDepth); - wGeo *= pow(saturate(dot(N, nN)), g_PhiNormal); - w *= max(wGeo, 0.05); - - float wD = w * exp(-abs(Luma(nD) - centerLumaD) * 1.25); - float wS = w * exp(-abs(Luma(nS) - centerLumaS) * 0.75); - sumD += nD * wD; + float wS0 = kKernel[ix + 2] * kKernel[iy + 2]; + float wGeoS = exp(-abs(depth - ndS) * (g_PhiDepth * lerp(1.0, 0.45, roughBlur))); + wGeoS *= pow(saturate(dot(N, nNs)), g_PhiNormal * lerp(1.0, 0.35, roughBlur)); + wS0 *= max(wGeoS, 0.08); + float wS = wS0 * exp(-abs(Luma(nS) - centerLumaS) * lerp(0.75, 0.2, roughBlur)); sumS += nS * wS; - wSumD += wD; wSumS += wS; } } diff --git a/res/gamedata/shaders/r5/restir_gi_composite.cs b/res/gamedata/shaders/r5/restir_gi_composite.cs index 2c0057affbc..e17e4e68b45 100644 --- a/res/gamedata/shaders/r5/restir_gi_composite.cs +++ b/res/gamedata/shaders/r5/restir_gi_composite.cs @@ -4,15 +4,20 @@ #include "shared/nrd_helpers.h" #include "shared/surface_marks.h" #include "restir_gi_common.h" +#include "rt_irradiance_cache.h" cbuffer CompositeParams : register(b5) { float4x4 g_InvViewProj; float4 g_CameraPos; float2 g_ScreenSize; float g_GIIntensity; - uint g_Pad; + uint g_CacheSize; float4 g_FogParams; float4 g_FogColor; + float g_CacheCellSize; + uint g_CacheMaxAge; + uint g_FrameIndex; + float g_Pad; }; Texture2D t_DirectLighting : register(t0); @@ -27,10 +32,13 @@ Texture2D t_ClassifyWorldPos : register(t10); Texture2D t_SpecReservoirA : register(t11); Texture2D t_SpecReservoirB : register(t12); +Texture2D t_ParticleOpacity : register(t13); +SamplerState s_LinearClamp : register(s0); RWTexture2D u_SceneColor : register(u0); RWTexture2D u_NoisyDiffuse : register(u1); RWTexture2D u_HitDist : register(u2); +RWStructuredBuffer u_IrradianceCache : register(u3); [numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) @@ -40,7 +48,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) return; float depth = t_Depth.Load(int3(pixel, 0)); - if (depth >= 1.0) { + if (depth <= 0.0) { u_SceneColor[pixel] = t_SceneColorIn.Load(int3(pixel, 0)); u_NoisyDiffuse[pixel] = 0; u_HitDist[pixel] = 0; @@ -49,7 +57,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float guideMark = t_WorldPos.Load(int3(pixel, 0)).w; float classifyMark = t_ClassifyWorldPos.Load(int3(pixel, 0)).w; - if (IsWaterSurfMark(classifyMark) && !IsHudSurfMark(guideMark)) { + if (SkipRtSurfLighting(classifyMark, guideMark)) { u_SceneColor[pixel] = t_SceneColorIn.Load(int3(pixel, 0)); u_NoisyDiffuse[pixel] = 0; u_HitDist[pixel] = 0; @@ -81,7 +89,8 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float3 diffIrradiance = 0; float hitDist = 0; - if (IsReservoirValid(r) && r.W > 0 && metallic < 0.999) { + bool weakRes = !(IsReservoirValid(r) && r.W > 1e-5 && metallic < 0.999); + if (!weakRes) { float3 wi = normalize(r.samplePos - worldPos); float cosTheta = max(dot(N, wi), 0); float3 kD = (1.0 - F_Schlick(cosTheta, F0)) * (1.0 - metallic); @@ -90,6 +99,17 @@ void main(uint3 dispatchID : SV_DispatchThreadID) diffIrradiance = r.Lo * brdfCos * r.W; diffIrradiance = min(diffIrradiance, RESTIR_MAX_RADIANCE); hitDist = length(r.samplePos - worldPos); + if (Luminance(diffIrradiance) < 1e-4) + weakRes = true; + } + if (weakRes && metallic < 0.999) + { + float3 cached = QueryIrradianceCacheRW( + u_IrradianceCache, worldPos, g_CacheCellSize, g_CacheSize, g_FrameIndex, g_CacheMaxAge); + float3 kD = (1.0 - metallic); + float3 floorGi = cached * kD * albedo / PI; + float mixW = IsReservoirValid(r) && r.W > 0 ? 0.55 : 1.0; + diffIrradiance = max(diffIrradiance, floorGi * mixW); } if (hitDist < 1e-3) hitDist = 0; @@ -107,11 +127,24 @@ void main(uint3 dispatchID : SV_DispatchThreadID) diffIrradiance = min(diffIrradiance, RESTIR_MAX_RADIANCE); specIrradiance = min(specIrradiance, RESTIR_MAX_RADIANCE); + { + float dLum = max(Luminance(direct), 0.04); + float maxDiff = dLum * 10.0 + 0.35; + float diffLum = Luminance(diffIrradiance); + if (diffLum > maxDiff) + diffIrradiance *= maxDiff / diffLum; + } float giAo = 1.0; if (hitDist > 0.35 && hitDist < 3.0) giAo = saturate(0.72 + hitDist * 0.09); + float2 opacityUV = (float2(pixel) + 0.5) / g_ScreenSize; + float particleOcc = saturate(t_ParticleOpacity.SampleLevel(s_LinearClamp, opacityUV, 0)); + float particleAtten = 1.0 - 0.7 * particleOcc; + giAo *= particleAtten; + direct *= lerp(1.0, particleAtten, 0.45); + u_NoisyDiffuse[pixel] = float4(diffIrradiance * giAo, 1.0); u_HitDist[pixel] = hitDist; diff --git a/res/gamedata/shaders/r5/rt_grass_billboard.cs b/res/gamedata/shaders/r5/rt_grass_billboard.cs index 09e982d06b1..4249e699892 100644 --- a/res/gamedata/shaders/r5/rt_grass_billboard.cs +++ b/res/gamedata/shaders/r5/rt_grass_billboard.cs @@ -33,7 +33,13 @@ struct PulledVertex { cbuffer BillboardRTCB : register(b5) { uint maxVertsPerBillboard; - uint3 pad; + float maxDist; + float camX; + float camY; + float camZ; + uint pad0; + uint pad1; + uint pad2; }; static const float TWO_PI = 6.28318530718; @@ -45,6 +51,16 @@ uint pack_normal(float3 n) return (u.x << 16) | (u.y << 8) | u.z; } +void zero_billboard(uint vertBase) +{ + for (uint v = 0; v < maxVertsPerBillboard; v++) { + uint vi = vertBase + v; + g_Output.Store4(vi * 24, uint4(0, 0, 0, 0)); + g_Output.Store2(vi * 24 + 16, uint2(0, 0)); + g_OutputIB.Store(vi * 4, vi); + } +} + [numthreads(256, 1, 1)] void main(uint3 dtid : SV_DispatchThreadID) { @@ -54,18 +70,19 @@ void main(uint3 dtid : SV_DispatchThreadID) uint vertBase = bb_idx * maxVertsPerBillboard; if (bb_idx >= actualCount) { - for (uint v = 0; v < maxVertsPerBillboard; v++) { - uint vi = vertBase + v; - g_Output.Store4(vi * 24, uint4(0, 0, 0, 0)); - g_Output.Store2(vi * 24 + 16, uint2(0, 0)); - g_OutputIB.Store(vi * 4, vi); - } + zero_billboard(vertBase); return; } uint src_idx = g_VisibleIndices[bb_idx]; InstanceData inst = g_AllInstances[src_idx]; + float3 camera = float3(camX, camY, camZ); + if (length(inst.pos - camera) > maxDist) { + zero_billboard(vertBase); + return; + } + uint object_id = inst.packed & 0x3F; float rotation = float((inst.packed >> 8) & 0x3FF) / 1023.0 * TWO_PI; float scale = float((inst.packed >> 18) & 0x3FF) / 1023.0 * PACK_MAX_SCALE; diff --git a/res/gamedata/shaders/r5/rt_volumetric.cs b/res/gamedata/shaders/r5/rt_volumetric.cs index 33d888132d8..9d8623db472 100644 --- a/res/gamedata/shaders/r5/rt_volumetric.cs +++ b/res/gamedata/shaders/r5/rt_volumetric.cs @@ -81,7 +81,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID) float2 ndc = float2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0); float3 cam = g_CameraPos.xyz; - float4 farClip = mul(g_InvViewProj, float4(ndc, 1.0, 1.0)); + float4 farClip = mul(g_InvViewProj, float4(ndc, 0.0, 1.0)); float3 farPos = farClip.xyz / max(farClip.w, 1e-6); float3 viewDir = normalize(farPos - cam); @@ -92,18 +92,21 @@ void main(uint3 dispatchID : SV_DispatchThreadID) clamp(uint(uv.y * float(max(depthH, 1u))), 0u, max(depthH, 1u) - 1u)); float depth = t_Depth.Load(int3(depthPixel, 0)); - const bool isSky = depth >= 0.9995; + const bool isSky = IsSkyDepth(depth); float3 surfacePos; if (isSky) { float marchFar = min(max(g_FogFar, 900.0), 1600.0); surfacePos = cam + viewDir * marchFar; } else { - float3 worldPos = t_WorldPos.Load(int3(depthPixel, 0)).xyz; - if (any(abs(worldPos - cam) > 1e-3)) - surfacePos = worldPos; - else - surfacePos = cam + viewDir * min(max(g_FogFar, 900.0), 1600.0); + float4 wp = t_WorldPos.Load(int3(depthPixel, 0)); + if (wp.w >= 0.5) + surfacePos = wp.xyz; + else { + float4 clip = float4(ndc, depth, 1.0); + float4 worldH = mul(g_InvViewProj, clip); + surfacePos = worldH.xyz / max(worldH.w, 1e-6); + } } float3 toEye = cam - surfacePos; diff --git a/res/gamedata/shaders/r5/shadow_mask.cs b/res/gamedata/shaders/r5/shadow_mask.cs index 5f9a1851757..0e36119cd06 100644 --- a/res/gamedata/shaders/r5/shadow_mask.cs +++ b/res/gamedata/shaders/r5/shadow_mask.cs @@ -80,7 +80,7 @@ void main(uint3 id : SV_DispatchThreadID) float2 uv = (float2(id.xy) + 0.5) / float2(g_maskSize); float rawD = g_SceneDepth.SampleLevel(smp_nofilter, uv, 0).x; - if (rawD >= 0.9995) + if (IsSkyDepth(rawD)) { g_OutMask[id.xy] = float4(1, 1, 1, 1); return; diff --git a/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.cpp index b614140c6a7..3241fa149e8 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.cpp @@ -38,6 +38,10 @@ extern ENGINE_API int ps_r_rt_gi_bounces; extern ENGINE_API int ps_r_rt_gi_cache_size; extern ENGINE_API float ps_r_rt_gi_cache_cell; extern ENGINE_API int ps_r_rt_vol_steps; +extern ENGINE_API float ps_r_rt_detail_dist; +extern ENGINE_API float ps_r_rt_gi_lod_dist; +extern ENGINE_API int ps_r_rt_sun_soft_samples; +extern ENGINE_API float ps_r_rt_sun_angular; namespace fg { @@ -77,9 +81,42 @@ struct ReSTIRGICB { u32 cacheSize; float cacheCellSize; u32 cacheMaxAge; - u32 pad1; + u32 particleBatchStart; + float lodDist; + float sunAngular; + u32 sunSoftSamples; + float cameraMotion; +}; +static_assert(sizeof(ReSTIRGICB) == 384, "ReSTIRGICB must be 384 bytes"); + +struct RTBatchStartIndices { + u32 skinned = 0xFFFFFFFFu; + u32 grass = 0xFFFFFFFFu; + u32 particle = 0xFFFFFFFFu; }; -static_assert(sizeof(ReSTIRGICB) == 368, "ReSTIRGICB must be 368 bytes"); + +static RTBatchStartIndices ComputeRTBatchStarts(const RTBatchCounts& bc) +{ + RTBatchStartIndices r; + u32 base = bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal; + if (bc.skinned > 0) + r.skinned = base; + base += bc.skinned; + if (bc.grass > 0) + r.grass = base; + base += bc.grass; + if (bc.particles > 0) + r.particle = base; + return r; +} + +static void FillRTBatchCBFields(u32& skinnedStart, u32& grassStart, u32& particleStart, const RTBatchCounts& bc) +{ + const auto idx = ComputeRTBatchStarts(bc); + skinnedStart = idx.skinned; + grassStart = idx.grass; + particleStart = idx.particle; +} struct TemporalCB { Fmatrix invViewProj; @@ -94,7 +131,8 @@ struct TemporalCB { u32 skinnedBatchStart; u32 grassBatchStart; u32 detailAtlasIndex; - u32 pad[2]; + u32 particleBatchStart; + float cameraMotion; }; static_assert(sizeof(TemporalCB) == 128, "TemporalCB must be 128 bytes"); @@ -114,7 +152,9 @@ struct SpatialCB { u32 skinnedBatchStart; u32 grassBatchStart; u32 detailAtlasIndex; - u32 pad[3]; + u32 particleBatchStart; + float lodDist; + float cameraMotion; }; static_assert(sizeof(SpatialCB) == 144, "SpatialCB must be 144 bytes"); @@ -151,7 +191,7 @@ struct DISpatialCB { u32 identityStaticCount; u32 terrainBatchCount; u32 skinnedBatchStart; - u32 pad0; + u32 particleBatchStart; u32 pad1; u32 pad2; }; @@ -167,7 +207,7 @@ struct DIShadeCB { u32 identityStaticCount; u32 terrainBatchCount; u32 skinnedBatchStart; - u32 pad; + u32 particleBatchStart; }; static_assert(sizeof(DIShadeCB) == 64, "DIShadeCB must be 64 bytes"); @@ -194,11 +234,15 @@ struct CompositeCB { float screenWidth; float screenHeight; float giIntensity; - u32 pad; + u32 cacheSize; Fvector4 fogParams; Fvector4 fogColor; + float cacheCellSize; + u32 cacheMaxAge; + u32 frameIndex; + float pad; }; -static_assert(sizeof(CompositeCB) == 128, "CompositeCB must be 128 bytes"); +static_assert(sizeof(CompositeCB) == 144, "CompositeCB must be 144 bytes"); struct BlurCB { float screenWidth; @@ -244,9 +288,16 @@ struct RTVolCB { }; static_assert(sizeof(RTVolCB) == 160, "RTVolCB must be 160 bytes"); +static bool s_rtgiAllocFailed = false; + bool IsRTGIActive(const RTAccelStructManager* accelMgr) { - return ::ps_r_rt_gi && accelMgr && accelMgr->IsSupported() && accelMgr->IsReady(); + return ::ps_r_rt_gi && !s_rtgiAllocFailed && accelMgr && accelMgr->IsSupported() && accelMgr->IsReady(); +} + +bool IsRTGIAllocFailed() +{ + return s_rtgiAllocFailed; } static void CreatePlaceholders(nvrhi::IDevice* nvDevice) @@ -276,7 +327,7 @@ static void CreatePlaceholders(nvrhi::IDevice* nvDevice) static void EnsureVolumetricPipeline(fg::RenderDevice* device, ReSTIRGIPassState& state) { - constexpr u32 kVolPipeVersion = 4; + constexpr u32 kVolPipeVersion = 5; static u32 s_volPipeVersion = 0; if (state.volPipeline && state.volLayout && s_volPipeVersion == kVolPipeVersion) return; @@ -313,7 +364,7 @@ static void EnsureVolumetricPipeline(fg::RenderDevice* device, ReSTIRGIPassState static void EnsureWaterPipeline(fg::RenderDevice* device, ReSTIRGIPassState& state) { - constexpr u32 kWaterPipeVersion = 10; + constexpr u32 kWaterPipeVersion = 11; static u32 s_waterPipeVersion = 0; if (state.waterPipeline && state.waterLayout && s_waterPipeVersion == kWaterPipeVersion) return; @@ -335,7 +386,7 @@ static void EnsureWaterPipeline(fg::RenderDevice* device, ReSTIRGIPassState& sta if (auto* backend = device->GetBackend()) bindlessLayout = backend->GetBindlessLayout(); - state.waterLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Water_RT_v10", *csResult.reflection, nvDevice); + state.waterLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Water_RT_v11", *csResult.reflection, nvDevice); nvrhi::ComputePipelineDesc pipeDesc; pipeDesc.CS = csResult.handle; if (bindlessLayout) @@ -349,9 +400,11 @@ static void EnsureWaterPipeline(fg::RenderDevice* device, ReSTIRGIPassState& sta static void InitializeResources(fg::RenderDevice* device, ReSTIRGIPassState& state) { - if (state.initialized && state.enabled) { - EnsureVolumetricPipeline(device, state); - EnsureWaterPipeline(device, state); + if (state.initialized) { + if (state.enabled) { + EnsureVolumetricPipeline(device, state); + EnsureWaterPipeline(device, state); + } return; } @@ -366,11 +419,14 @@ static void InitializeResources(fg::RenderDevice* device, ReSTIRGIPassState& sta state.sampler = cache.GetOrCreateSampler("RTGI", samplerDesc, nvDevice); } - if (!state.cb) { - state.cb = cache.GetOrCreateVolatileCB("RTGI", "RTGI_CB", - (u32)std::max({ sizeof(ReSTIRGICB), sizeof(TemporalCB), sizeof(SpatialCB), sizeof(DITemporalCB), - sizeof(DISpatialCB), sizeof(DIShadeCB), sizeof(WaterCB), sizeof(CompositeCB), sizeof(BlurCB), - sizeof(TemporalFilterCB), sizeof(RTVolCB) }), device); + { + constexpr u32 kRTGICBVersions = 512u; + const u32 cbBytes = (u32)std::max({ sizeof(ReSTIRGICB), sizeof(TemporalCB), sizeof(SpatialCB), + sizeof(DITemporalCB), sizeof(DISpatialCB), sizeof(DIShadeCB), sizeof(WaterCB), + sizeof(CompositeCB), sizeof(BlurCB), sizeof(TemporalFilterCB), sizeof(RTVolCB) }); + nvrhi::IBuffer* cb = cache.GetOrCreateVolatileCB("RTGI", "RTGI_CB", cbBytes, device, kRTGICBVersions); + if (cb != state.cb) + state.cb = cb; } nvrhi::IBindingLayout* bindlessLayout = nullptr; @@ -481,9 +537,10 @@ static void InitializeResources(fg::RenderDevice* device, ReSTIRGIPassState& sta EnsureWaterPipeline(device, state); if (!state.compositePipeline) { + BindingSetBuilder::InvalidateReflectionCache(); auto csResult = GEnv.Render->GetShaderLoader()->LoadComputeShader("restir_gi_composite"); if (csResult.handle && csResult.reflection) { - state.compositeLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Composite_v6", *csResult.reflection, nvDevice); + state.compositeLayout = cache.GetOrCreateBindingLayoutFromReflection("RTGI_Composite_v9", *csResult.reflection, nvDevice); nvrhi::ComputePipelineDesc pipeDesc; pipeDesc.CS = csResult.handle; pipeDesc.bindingLayouts = { state.compositeLayout }; @@ -558,6 +615,7 @@ static bool EnsureIrradianceCache(nvrhi::IDevice* nvDevice, ReSTIRGIPassState& s bufDesc.byteSize = (size_t)entries * 16u; bufDesc.structStride = 16; bufDesc.canHaveUAVs = true; + bufDesc.canHaveRawViews = true; bufDesc.initialState = nvrhi::ResourceStates::UnorderedAccess; bufDesc.keepInitialState = true; state.irradianceCache = nvDevice->createBuffer(bufDesc); @@ -568,8 +626,38 @@ static bool EnsureIrradianceCache(nvrhi::IDevice* nvDevice, ReSTIRGIPassState& s return true; } +static void ReleasePersistentTextures(ReSTIRGIPassState& state) +{ + for (int i = 0; i < 2; i++) { + state.reservoirA[i] = nullptr; + state.reservoirB[i] = nullptr; + state.reservoirC[i] = nullptr; + state.specReservoirA[i] = nullptr; + state.specReservoirB[i] = nullptr; + state.diReservoir[i] = nullptr; + } + state.directLighting = nullptr; + state.noisyDiffuse = nullptr; + state.noisySpecular = nullptr; + state.blurTemp = nullptr; + state.blurTempSpec = nullptr; + state.histDiffuse = nullptr; + state.histSpecular = nullptr; + state.hitDistance = nullptr; + state.volSceneColor = nullptr; + state.irradianceCache = nullptr; + state.irradianceCacheSize = 0; + state.texWidth = 0; + state.texHeight = 0; + state.volTexWidth = 0; + state.volTexHeight = 0; +} + static bool EnsurePersistentTextures(nvrhi::IDevice* nvDevice, ReSTIRGIPassState& state, u32 width, u32 height) { + if (state.allocFailed || s_rtgiAllocFailed) + return false; + const bool sizeOk = state.reservoirA[0] && state.diReservoir[0] && state.reservoirC[0] && state.specReservoirA[0] && state.texWidth == width && state.texHeight == height; if (sizeOk) { @@ -580,6 +668,8 @@ static bool EnsurePersistentTextures(nvrhi::IDevice* nvDevice, ReSTIRGIPassState state.irradianceCache; } + ReleasePersistentTextures(state); + auto makeTex = [&](const char* name, nvrhi::Format fmt) -> nvrhi::TextureHandle { nvrhi::TextureDesc desc; desc.debugName = name; @@ -592,6 +682,15 @@ static bool EnsurePersistentTextures(nvrhi::IDevice* nvDevice, ReSTIRGIPassState return nvDevice->createTexture(desc); }; + auto failAlloc = [&](const char* msg) { + Msg("%s", msg); + ReleasePersistentTextures(state); + state.enabled = false; + state.allocFailed = true; + s_rtgiAllocFailed = true; + return false; + }; + for (int i = 0; i < 2; i++) { state.reservoirA[i] = makeTex(i == 0 ? "RTGI_ReservoirA_0" : "RTGI_ReservoirA_1", nvrhi::Format::RGBA32_FLOAT); state.reservoirB[i] = makeTex(i == 0 ? "RTGI_ReservoirB_0" : "RTGI_ReservoirB_1", nvrhi::Format::RGBA32_FLOAT); @@ -601,9 +700,7 @@ static bool EnsurePersistentTextures(nvrhi::IDevice* nvDevice, ReSTIRGIPassState state.diReservoir[i] = makeTex(i == 0 ? "RTGI_DIReservoir_0" : "RTGI_DIReservoir_1", nvrhi::Format::RGBA32_FLOAT); if (!state.reservoirA[i] || !state.reservoirB[i] || !state.reservoirC[i] || !state.specReservoirA[i] || !state.specReservoirB[i] || !state.diReservoir[i]) { - Msg("! [ReSTIR GI] Reservoir texture create failed"); - state.enabled = false; - return false; + return failAlloc("! [ReSTIR GI] Reservoir texture create failed (VRAM) — falling back to raster lighting"); } } @@ -616,16 +713,12 @@ static bool EnsurePersistentTextures(nvrhi::IDevice* nvDevice, ReSTIRGIPassState state.histSpecular = makeTex("RTGI_HistSpecular", nvrhi::Format::RGBA16_FLOAT); state.hitDistance = makeTex("RTGI_HitDistance", nvrhi::Format::R16_FLOAT); - if (!EnsureIrradianceCache(nvDevice, state)) { - state.enabled = false; - return false; - } + if (!EnsureIrradianceCache(nvDevice, state)) + return failAlloc("! [ReSTIR GI] Irradiance cache create failed — falling back to raster lighting"); if (!state.directLighting || !state.noisyDiffuse || !state.noisySpecular || !state.blurTemp || !state.blurTempSpec || !state.histDiffuse || !state.histSpecular || !state.hitDistance) { - Msg("! [ReSTIR GI] Persistent texture create failed — GI disabled"); - state.enabled = false; - return false; + return failAlloc("! [ReSTIR GI] Persistent texture create failed — GI disabled, falling back to raster lighting"); } state.texWidth = width; @@ -633,6 +726,16 @@ static bool EnsurePersistentTextures(nvrhi::IDevice* nvDevice, ReSTIRGIPassState return true; } +bool EnsureReSTIRGITextures(fg::RenderDevice* device, ReSTIRGIPassState& state, u32 width, u32 height) +{ + if (!device || !device->GetNVRHIDevice() || state.allocFailed || s_rtgiAllocFailed) + return false; + InitializeResources(device, state); + if (!state.enabled) + return false; + return EnsurePersistentTextures(device->GetNVRHIDevice(), state, width, height); +} + struct InitialPassData { fg::RenderDevice* device; RTAccelStructManager* accelMgr; @@ -675,6 +778,7 @@ struct CompositePassData { VirtualResourceHandle classifyWorldPos; VirtualResourceHandle sceneColorIn; VirtualResourceHandle sceneColor; + VirtualResourceHandle particleOpacity; CompositeCB cbData; u32 width, height; u32 reservoirIdx; @@ -701,7 +805,8 @@ ReSTIRGIOutput setupReSTIRGIPass( bool hasPrevFrameData, bool skipInTreeDenoise, VirtualResourceHandle classifyWorldPos, - bool skipLightingTemporal) + bool skipLightingTemporal, + VirtualResourceHandle particleOpacity) { InitializeResources(device, state); @@ -738,16 +843,23 @@ ReSTIRGIOutput setupReSTIRGIPass( float skyWeight = env.CurrentEnv.weight; if (texManager && env.Current[0] && env.Current[1]) { - if (env.Current[0]->sky_texture_name.size()) { - auto h0 = texManager->LoadTexture(env.Current[0]->sky_texture_name.c_str()); - nvrhi::ITexture* t = texManager->GetNVRHITexture(h0); - if (t) sky0Tex = t; + static shared_str s_skyName0; + static shared_str s_skyName1; + static nvrhi::ITexture* s_skyTex0 = nullptr; + static nvrhi::ITexture* s_skyTex1 = nullptr; + + const shared_str& name0 = env.Current[0]->sky_texture_name; + const shared_str& name1 = env.Current[1]->sky_texture_name; + if (name0.size() && name0 != s_skyName0) { + s_skyName0 = name0; + s_skyTex0 = texManager->GetNVRHITexture(texManager->LoadTexture(name0.c_str())); } - if (env.Current[1]->sky_texture_name.size()) { - auto h1 = texManager->LoadTexture(env.Current[1]->sky_texture_name.c_str()); - nvrhi::ITexture* t = texManager->GetNVRHITexture(h1); - if (t) sky1Tex = t; + if (name1.size() && name1 != s_skyName1) { + s_skyName1 = name1; + s_skyTex1 = texManager->GetNVRHITexture(texManager->LoadTexture(name1.c_str())); } + if (s_skyTex0) sky0Tex = s_skyTex0; + if (s_skyTex1) sky1Tex = s_skyTex1; } Fvector sunDir = env.CurrentEnv.sun_dir; @@ -780,6 +892,10 @@ ReSTIRGIOutput setupReSTIRGIPass( initialCB.grassBatchStart = batchCounts.grass > 0 ? batchCounts.identityStatic + batchCounts.terrain + batchCounts.transparent + batchCounts.instancedTotal + batchCounts.skinned : 0xFFFFFFFFu; + initialCB.particleBatchStart = batchCounts.particles > 0 + ? batchCounts.identityStatic + batchCounts.terrain + batchCounts.transparent + batchCounts.instancedTotal + + batchCounts.skinned + batchCounts.grass + : 0xFFFFFFFFu; initialCB.detailAtlasIndex = accelMgr->GetDetailAtlasIndex(); initialCB.localLightSamples = (u32)std::max(0, ::ps_r_rt_gi_local_samples); initialCB.bounces = (u32)std::clamp(::ps_r_rt_gi_bounces, 1, 2); @@ -809,7 +925,17 @@ ReSTIRGIOutput setupReSTIRGIPass( initialCB.cacheSize = state.irradianceCacheSize; initialCB.cacheCellSize = std::max(0.05f, ::ps_r_rt_gi_cache_cell); initialCB.cacheMaxAge = 64u; - initialCB.pad1 = 0; + initialCB.lodDist = std::max(10.f, ::ps_r_rt_gi_lod_dist); + initialCB.sunAngular = std::max(0.001f, ::ps_r_rt_sun_angular); + initialCB.sunSoftSamples = (u32)std::clamp(::ps_r_rt_sun_soft_samples, 1, 8); + { + static Fvector s_prevCamDir = {0, 0, 1}; + Fvector camDir = Device.vCameraDirection; + float motion = 1.f - camDir.dotproduct(s_prevCamDir); + motion = std::clamp(motion * 4.f, 0.f, 1.f); + s_prevCamDir = camDir; + initialCB.cameraMotion = motion; + } ResourceDesc persistDesc; persistDesc.type = ResourceDesc::Type::Texture2D; @@ -916,12 +1042,7 @@ ReSTIRGIOutput setupReSTIRGIPass( const auto& bc = data.accelMgr->GetBatchCounts(); cb.identityStaticCount = bc.identityStatic; cb.terrainBatchCount = bc.terrain; - cb.skinnedBatchStart = bc.skinned > 0 - ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal - : 0xFFFFFFFFu; - cb.grassBatchStart = bc.grass > 0 - ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + bc.skinned - : 0xFFFFFFFFu; + FillRTBatchCBFields(cb.skinnedBatchStart, cb.grassBatchStart, cb.particleBatchStart, bc); cb.detailAtlasIndex = data.accelMgr->GetDetailAtlasIndex(); cb.cacheSize = data.state->irradianceCacheSize; { @@ -941,6 +1062,10 @@ ReSTIRGIOutput setupReSTIRGIPass( if (!skinnedIB) skinnedIB = s_rtgiPlaceholderBuffer.Get(); if (!grassVB) grassVB = s_rtgiPlaceholderBuffer.Get(); if (!grassIB) grassIB = s_rtgiPlaceholderBuffer.Get(); + nvrhi::IBuffer* particleVB = data.accelMgr->GetParticleOutputVB(); + nvrhi::IBuffer* particleIB = data.accelMgr->GetParticleIB(); + if (!particleVB) particleVB = s_rtgiPlaceholderBuffer.Get(); + if (!particleIB) particleIB = s_rtgiPlaceholderBuffer.Get(); auto* shaderLoader = GEnv.Render->GetShaderLoader(); auto* csReflection = shaderLoader->GetCachedReflection("restir_gi_initial", ".cs"); @@ -959,6 +1084,8 @@ ReSTIRGIOutput setupReSTIRGIPass( bsb.BufferSRV("g_SkinnedIB", skinnedIB); bsb.BufferSRV("g_GrassVB", grassVB); bsb.BufferSRV("g_GrassIB", grassIB); + bsb.BufferSRV("g_ParticleVB", particleVB); + bsb.BufferSRV("g_ParticleIB", particleIB); bsb.Texture("t_Depth", depthTex); bsb.Texture("t_Normal", normalTex); bsb.Texture("t_BaseColor", baseColorTex); @@ -1016,7 +1143,8 @@ ReSTIRGIOutput setupReSTIRGIPass( temporalCB.skinnedBatchStart = initialCB.skinnedBatchStart; temporalCB.grassBatchStart = initialCB.grassBatchStart; temporalCB.detailAtlasIndex = initialCB.detailAtlasIndex; - temporalCB.pad[0] = temporalCB.pad[1] = 0; + temporalCB.particleBatchStart = initialCB.particleBatchStart; + temporalCB.cameraMotion = initialCB.cameraMotion; fg.addCallbackPass( "ReSTIR GI Temporal", @@ -1064,12 +1192,7 @@ ReSTIRGIOutput setupReSTIRGIPass( const auto& bc = data.accelMgr->GetBatchCounts(); cb.identityStaticCount = bc.identityStatic; cb.terrainBatchCount = bc.terrain; - cb.skinnedBatchStart = bc.skinned > 0 - ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal - : 0xFFFFFFFFu; - cb.grassBatchStart = bc.grass > 0 - ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + bc.skinned - : 0xFFFFFFFFu; + FillRTBatchCBFields(cb.skinnedBatchStart, cb.grassBatchStart, cb.particleBatchStart, bc); cb.detailAtlasIndex = data.accelMgr->GetDetailAtlasIndex(); } cmdList->writeBuffer(data.state->cb, &cb, sizeof(TemporalCB)); @@ -1090,6 +1213,10 @@ ReSTIRGIOutput setupReSTIRGIPass( if (!megaIB) megaIB = s_rtgiPlaceholderBuffer.Get(); if (!grassVB) grassVB = s_rtgiPlaceholderBuffer.Get(); if (!grassIB) grassIB = s_rtgiPlaceholderBuffer.Get(); + nvrhi::IBuffer* particleVB = data.accelMgr->GetParticleOutputVB(); + nvrhi::IBuffer* particleIB = data.accelMgr->GetParticleIB(); + if (!particleVB) particleVB = s_rtgiPlaceholderBuffer.Get(); + if (!particleIB) particleIB = s_rtgiPlaceholderBuffer.Get(); framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRGI.Temporal"); bsb.ConstantBuffer("ReSTIRTemporalParams", data.state->cb); @@ -1100,6 +1227,8 @@ ReSTIRGIOutput setupReSTIRGIPass( bsb.BufferSRV("g_MegaIB", megaIB); bsb.BufferSRV("g_GrassVB", grassVB); bsb.BufferSRV("g_GrassIB", grassIB); + bsb.BufferSRV("g_ParticleVB", particleVB); + bsb.BufferSRV("g_ParticleIB", particleIB); bsb.Texture("t_PrevReservoirA", data.state->reservoirA[data.readIdx]); bsb.Texture("t_PrevReservoirB", data.state->reservoirB[data.readIdx]); bsb.Texture("t_PrevReservoirC", data.state->reservoirC[data.readIdx]); @@ -1260,7 +1389,9 @@ ReSTIRGIOutput setupReSTIRGIPass( spatialCB.skinnedBatchStart = initialCB.skinnedBatchStart; spatialCB.grassBatchStart = initialCB.grassBatchStart; spatialCB.detailAtlasIndex = initialCB.detailAtlasIndex; - spatialCB.pad[0] = spatialCB.pad[1] = spatialCB.pad[2] = 0; + spatialCB.particleBatchStart = initialCB.particleBatchStart; + spatialCB.lodDist = initialCB.lodDist; + spatialCB.cameraMotion = initialCB.cameraMotion; VirtualResourceHandle fgResAOut = fg.ImportTexture("rtgi_ResA_S", state.reservoirA[readIdx].Get(), resDesc); VirtualResourceHandle fgResBOut = fg.ImportTexture("rtgi_ResB_S", state.reservoirB[readIdx].Get(), resDesc); @@ -1338,12 +1469,7 @@ ReSTIRGIOutput setupReSTIRGIPass( const auto& bc = data.accelMgr->GetBatchCounts(); cb.identityStaticCount = bc.identityStatic; cb.terrainBatchCount = bc.terrain; - cb.skinnedBatchStart = bc.skinned > 0 - ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal - : 0xFFFFFFFFu; - cb.grassBatchStart = bc.grass > 0 - ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + bc.skinned - : 0xFFFFFFFFu; + FillRTBatchCBFields(cb.skinnedBatchStart, cb.grassBatchStart, cb.particleBatchStart, bc); cb.detailAtlasIndex = data.accelMgr->GetDetailAtlasIndex(); } cmdList->writeBuffer(data.state->cb, &cb, sizeof(SpatialCB)); @@ -1362,6 +1488,10 @@ ReSTIRGIOutput setupReSTIRGIPass( if (!megaIB) megaIB = s_rtgiPlaceholderBuffer.Get(); if (!grassVB) grassVB = s_rtgiPlaceholderBuffer.Get(); if (!grassIB) grassIB = s_rtgiPlaceholderBuffer.Get(); + nvrhi::IBuffer* particleVB = data.accelMgr->GetParticleOutputVB(); + nvrhi::IBuffer* particleIB = data.accelMgr->GetParticleIB(); + if (!particleVB) particleVB = s_rtgiPlaceholderBuffer.Get(); + if (!particleIB) particleIB = s_rtgiPlaceholderBuffer.Get(); framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRGI.Spatial"); bsb.ConstantBuffer("ReSTIRSpatialParams", data.state->cb); @@ -1372,6 +1502,8 @@ ReSTIRGIOutput setupReSTIRGIPass( bsb.BufferSRV("g_MegaIB", megaIB); bsb.BufferSRV("g_GrassVB", grassVB); bsb.BufferSRV("g_GrassIB", grassIB); + bsb.BufferSRV("g_ParticleVB", particleVB); + bsb.BufferSRV("g_ParticleIB", particleIB); bsb.Texture("t_Depth", depthTex); bsb.Texture("t_Normal", normalTex); bsb.Texture("t_BaseColor", baseColorTex); @@ -1429,7 +1561,7 @@ ReSTIRGIOutput setupReSTIRGIPass( diSpatCB.identityStaticCount = initialCB.identityStaticCount; diSpatCB.terrainBatchCount = initialCB.terrainBatchCount; diSpatCB.skinnedBatchStart = initialCB.skinnedBatchStart; - diSpatCB.pad0 = 0; + diSpatCB.particleBatchStart = initialCB.particleBatchStart; VirtualResourceHandle fgDIOut = fg.ImportTexture("rtgi_DI_S", state.diReservoir[readIdx].Get(), resDesc); @@ -1497,18 +1629,17 @@ ReSTIRGIOutput setupReSTIRGIPass( if (!megaIB) megaIB = s_rtgiPlaceholderBuffer.Get(); if (!grassVB) grassVB = s_rtgiPlaceholderBuffer.Get(); if (!grassIB) grassIB = s_rtgiPlaceholderBuffer.Get(); + nvrhi::IBuffer* particleVB = data.accelMgr->GetParticleOutputVB(); + nvrhi::IBuffer* particleIB = data.accelMgr->GetParticleIB(); + if (!particleVB) particleVB = s_rtgiPlaceholderBuffer.Get(); + if (!particleIB) particleIB = s_rtgiPlaceholderBuffer.Get(); { const auto& bc = data.accelMgr->GetBatchCounts(); DISpatialCB cb = data.cbData; cb.identityStaticCount = bc.identityStatic; cb.terrainBatchCount = bc.terrain; - cb.skinnedBatchStart = bc.skinned > 0 - ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal - : 0xFFFFFFFFu; - cb.grassBatchStart = bc.grass > 0 - ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + bc.skinned - : 0xFFFFFFFFu; + FillRTBatchCBFields(cb.skinnedBatchStart, cb.grassBatchStart, cb.particleBatchStart, bc); cb.detailAtlasIndex = data.accelMgr->GetDetailAtlasIndex(); cmdList->writeBuffer(data.state->cb, &cb, sizeof(DISpatialCB)); } @@ -1522,6 +1653,8 @@ ReSTIRGIOutput setupReSTIRGIPass( bsb.BufferSRV("g_MegaIB", megaIB); bsb.BufferSRV("g_GrassVB", grassVB); bsb.BufferSRV("g_GrassIB", grassIB); + bsb.BufferSRV("g_ParticleVB", particleVB); + bsb.BufferSRV("g_ParticleIB", particleIB); bsb.BufferSRV("g_LightData", lightData); bsb.Texture("t_SrcDI", srcDI); bsb.Texture("t_Depth", depthTex); @@ -1560,7 +1693,7 @@ ReSTIRGIOutput setupReSTIRGIPass( diShadeCB.identityStaticCount = initialCB.identityStaticCount; diShadeCB.terrainBatchCount = initialCB.terrainBatchCount; diShadeCB.skinnedBatchStart = initialCB.skinnedBatchStart; - diShadeCB.pad = 0; + diShadeCB.particleBatchStart = initialCB.particleBatchStart; struct DIShadePassData { fg::RenderDevice* device; @@ -1624,18 +1757,17 @@ ReSTIRGIOutput setupReSTIRGIPass( if (!megaIB) megaIB = s_rtgiPlaceholderBuffer.Get(); if (!grassVB) grassVB = s_rtgiPlaceholderBuffer.Get(); if (!grassIB) grassIB = s_rtgiPlaceholderBuffer.Get(); + nvrhi::IBuffer* particleVB = data.accelMgr->GetParticleOutputVB(); + nvrhi::IBuffer* particleIB = data.accelMgr->GetParticleIB(); + if (!particleVB) particleVB = s_rtgiPlaceholderBuffer.Get(); + if (!particleIB) particleIB = s_rtgiPlaceholderBuffer.Get(); { const auto& bc = data.accelMgr->GetBatchCounts(); DIShadeCB cb = data.cbData; cb.identityStaticCount = bc.identityStatic; cb.terrainBatchCount = bc.terrain; - cb.skinnedBatchStart = bc.skinned > 0 - ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal - : 0xFFFFFFFFu; - cb.grassBatchStart = bc.grass > 0 - ? bc.identityStatic + bc.terrain + bc.transparent + bc.instancedTotal + bc.skinned - : 0xFFFFFFFFu; + FillRTBatchCBFields(cb.skinnedBatchStart, cb.grassBatchStart, cb.particleBatchStart, bc); cb.detailAtlasIndex = data.accelMgr->GetDetailAtlasIndex(); cmdList->writeBuffer(data.state->cb, &cb, sizeof(DIShadeCB)); } @@ -1649,6 +1781,8 @@ ReSTIRGIOutput setupReSTIRGIPass( bsb.BufferSRV("g_MegaIB", megaIB); bsb.BufferSRV("g_GrassVB", grassVB); bsb.BufferSRV("g_GrassIB", grassIB); + bsb.BufferSRV("g_ParticleVB", particleVB); + bsb.BufferSRV("g_ParticleIB", particleIB); bsb.BufferSRV("g_LightData", lightData); bsb.Texture("t_DIReservoir", diRes); bsb.Texture("t_Depth", depthTex); @@ -1694,19 +1828,23 @@ ReSTIRGIOutput setupReSTIRGIPass( compositeCB.screenWidth = (float)width; compositeCB.screenHeight = (float)height; compositeCB.giIntensity = giIntensity; - compositeCB.pad = 0; + compositeCB.cacheSize = state.irradianceCacheSize; { StaticGlobals fogFill{}; FillGlobalConstants(fogFill); compositeCB.fogParams = fogFill.fog_params; compositeCB.fogColor = fogFill.fog_color; } + compositeCB.cacheCellSize = std::max(0.05f, ::ps_r_rt_gi_cache_cell); + compositeCB.cacheMaxAge = 64u; + compositeCB.frameIndex = Device.dwFrame; + compositeCB.pad = 0.f; VirtualResourceHandle classifyWP = classifyWorldPos.is_valid() ? classifyWorldPos : worldPos; auto& compositeData = fg.addCallbackPass( "ReSTIR GI Composite", - [&, compositeCB, compositeReservoirIdx, outHandle, fgDirectLighting, fgResA, fgResB, fgSpecA, fgSpecB, fgNoisyDiff, fgNoisySpec, fgHitDist, classifyWP](FrameGraph& builder, PassHandle passHandle, CompositePassData& data) { + [&, compositeCB, compositeReservoirIdx, outHandle, fgDirectLighting, fgResA, fgResB, fgSpecA, fgSpecB, fgNoisyDiff, fgNoisySpec, fgHitDist, classifyWP, particleOpacity](FrameGraph& builder, PassHandle passHandle, CompositePassData& data) { RenderPassBuilder pb(builder, passHandle); data.depth = pb.read(depth, ResourceState::ShaderResource); data.normal = pb.read(normal, ResourceState::ShaderResource); @@ -1714,6 +1852,8 @@ ReSTIRGIOutput setupReSTIRGIPass( data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); data.classifyWorldPos = pb.read(classifyWP, ResourceState::ShaderResource); data.sceneColorIn = pb.read(sceneColorIn, ResourceState::ShaderResource); + if (particleOpacity.is_valid()) + data.particleOpacity = pb.read(particleOpacity, ResourceState::ShaderResource); pb.read(fgDirectLighting, ResourceState::ShaderResource); pb.read(fgResA, ResourceState::ShaderResource); pb.read(fgResB, ResourceState::ShaderResource); @@ -1755,6 +1895,8 @@ ReSTIRGIOutput setupReSTIRGIPass( nvrhi::IDevice* nvDevice = data.device->GetNVRHIDevice(); nvrhi::ICommandList* cmdList = ctx->GetCommandList(); + if (!EnsureIrradianceCache(nvDevice, *data.state)) + return; cmdList->writeBuffer(data.state->cb, &data.cbData, sizeof(CompositeCB)); @@ -1762,6 +1904,11 @@ ReSTIRGIOutput setupReSTIRGIPass( auto* csReflection = shaderLoader->GetCachedReflection("restir_gi_composite", ".cs"); if (!csReflection) return; + nvrhi::ITexture* particleOpacityTex = data.particleOpacity.is_valid() + ? fg.GetPhysicalTexture(data.particleOpacity) : nullptr; + if (!particleOpacityTex) + particleOpacityTex = GetPassResourceCache().GetDummyContactHistory(nvDevice); + framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRGI.Composite"); bsb.ConstantBuffer("CompositeParams", data.state->cb); bsb.Texture("t_DirectLighting", directLit); @@ -1776,9 +1923,14 @@ ReSTIRGIOutput setupReSTIRGIPass( bsb.Texture("t_ClassifyWorldPos", classifyTex); bsb.Texture("t_SpecReservoirA", specA); bsb.Texture("t_SpecReservoirB", specB); + bsb.Texture("t_ParticleOpacity", particleOpacityTex); + nvrhi::IBuffer* irrCache = data.state->irradianceCache.Get(); + if (!irrCache) + return; bsb.TextureUAV("u_SceneColor", outTex); bsb.TextureUAV("u_NoisyDiffuse", noisyDiff); bsb.TextureUAV("u_HitDist", hitDist); + bsb.BufferUAV("u_IrradianceCache", irrCache); auto& cache = GetPassResourceCache(); auto bindingSet = cache.GetOrCreateBindingSet(bsb.Build(), data.state->compositeLayout, nvDevice); if (!bindingSet) return; @@ -2030,7 +2182,7 @@ ReSTIRGIOutput setupReSTIRGIPass( if (state.waterPipeline && state.waterLayout && accelMgr) { WaterCB waterCB{}; waterCB.invViewProj = invViewProj; - waterCB.cameraPos = { cameraPos.x, cameraPos.y, cameraPos.z, 0 }; + waterCB.cameraPos = { cameraPos.x, cameraPos.y, cameraPos.z, initialCB.lodDist }; waterCB.sunDir_intensity = initialCB.sunDir_intensity; waterCB.sunColor_skyWeight = initialCB.sunColor_skyWeight; waterCB.screenWidth = (float)width; @@ -2369,7 +2521,7 @@ VirtualResourceHandle setupRTVolumetricPass( framegraph::BindingSetBuilder bsb(*csReflection, nvDevice, "ReSTIRGI.Vol"); bsb.ConstantBuffer("RTVolParams", data.state->cb); bsb.AccelStruct("g_SceneTLAS", tlas); - bsb.Texture("t_Depth", depthTex); + bsb.Texture("t_Depth", depthTex, nvrhi::Format::R32_FLOAT); bsb.Texture("t_SceneColorIn", sceneIn); bsb.Texture("t_WorldPos", worldPosTex); bsb.BufferSRV("g_BatchInfo", batchInfo); @@ -2451,6 +2603,8 @@ void ShutdownReSTIRGI(ReSTIRGIPassState& state) s_rtgiPlaceholderCube = nullptr; state.initialized = false; state.enabled = false; + state.allocFailed = false; + s_rtgiAllocFailed = false; } } diff --git a/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.h index e5eac3a9842..707b0360f9b 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/ReSTIRGIPassSetup.h @@ -65,6 +65,7 @@ struct ReSTIRGIPassState { bool initialized = false; bool enabled = false; bool initialLoadFailed = false; + bool allocFailed = false; }; struct ReSTIRGIOutput { @@ -75,6 +76,8 @@ struct ReSTIRGIOutput { }; bool IsRTGIActive(const RTAccelStructManager* accelMgr); +bool IsRTGIAllocFailed(); +bool EnsureReSTIRGITextures(fg::RenderDevice* device, ReSTIRGIPassState& state, u32 width, u32 height); ReSTIRGIOutput setupReSTIRGIPass( framegraph::FrameGraph& fg, @@ -97,7 +100,8 @@ ReSTIRGIOutput setupReSTIRGIPass( bool hasPrevFrameData, bool skipInTreeDenoise = false, framegraph::VirtualResourceHandle classifyWorldPos = {}, - bool skipLightingTemporal = false + bool skipLightingTemporal = false, + framegraph::VirtualResourceHandle particleOpacity = {} ); void ShutdownReSTIRGI(ReSTIRGIPassState& state); diff --git a/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.h b/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.h index ea6436a7dfa..363ebfa5f87 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.h +++ b/src/Layers/xrRender/FrameGraphPasses/ShaderConstants.h @@ -42,6 +42,24 @@ namespace xray::render { namespace xray::render::fg::passes { +inline u32& RenderResW() { static u32 w = 0; return w; } +inline u32& RenderResH() { static u32 h = 0; return h; } +inline void SetRenderResolution(u32 w, u32 h) +{ + RenderResW() = std::max(1u, w); + RenderResH() = std::max(1u, h); +} +inline u32 GetRenderWidth() +{ + const u32 w = RenderResW(); + return w ? w : std::max(1u, (u32)Device.dwWidth); +} +inline u32 GetRenderHeight() +{ + const u32 h = RenderResH(); + return h ? h : std::max(1u, (u32)Device.dwHeight); +} + // Filled by CascadedShadows pass; consumed by FillGlobalConstants struct ShadowCascadeGPUData { @@ -169,8 +187,8 @@ using GlobalConstants = StaticGlobals; inline void FillClusterParams(StaticGlobals& cb) { auto& clm = ::xray::render::fg::ClusteredLightManager::Instance(); - const u32 w = std::max(1u, (u32)Device.dwWidth); - const u32 h = std::max(1u, (u32)Device.dwHeight); + const u32 w = GetRenderWidth(); + const u32 h = GetRenderHeight(); const float zNear = VIEWPORT_NEAR; const float zFar = g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.far_plane @@ -251,9 +269,12 @@ inline void FillGlobalConstants(GlobalConstants& cb) { const float VertTan = -1.0f * tanf(deg2rad(Device.fFOV / 2.0f)); const float HorzTan = -VertTan / Device.fASPECT; + const float rw = float(GetRenderWidth()); + const float rh = float(GetRenderHeight()); + // Vertex decompression (used for quantized positions) - cb.pos_decompression_params.set(HorzTan, VertTan, (2.0f * HorzTan) / (float)Device.dwWidth, (2.0f * VertTan) / (float)Device.dwHeight); - cb.pos_decompression_params2.set((float)Device.dwWidth, (float)Device.dwHeight, 1.0f / (float)Device.dwWidth, 1.0f / (float)Device.dwHeight); + cb.pos_decompression_params.set(HorzTan, VertTan, (2.0f * HorzTan) / rw, (2.0f * VertTan) / rh); + cb.pos_decompression_params2.set(rw, rh, 1.0f / rw, 1.0f / rh); // Parallax mapping: .x = height scale (r2_parallax_h; 0 disables object parallax), // .y = bias, .z = foliage SSS intensity, .w = sky IBL intensity. @@ -274,13 +295,7 @@ inline void FillGlobalConstants(GlobalConstants& cb) { (ps_r_sky_ibl != 0) ? ps_r_sky_ibl_intensity : 0.0f); } - // Screen resolution (for UI shaders and other effects) - cb.screen_res.set( - (float)Device.dwWidth, // x = width - (float)Device.dwHeight, // y = height - 1.0f / (float)Device.dwWidth, // z = 1/width - 1.0f / (float)Device.dwHeight // w = 1/height - ); + cb.screen_res.set(rw, rh, 1.0f / rw, 1.0f / rh); cb.hud_fov = psHUD_FOV; // Screen-space contact shadow length (0 = disabled); packed as StaticGlobals.padding3 @@ -377,7 +392,7 @@ inline void FillGlobalConstants(GlobalConstants& cb) { inline void FillDynamicTransforms(DynamicTransforms& cb, Fmatrix m_W = Fidentity) { cb.m_W = m_W; cb.m_WV.mul_43(Device.mView, m_W); - cb.m_WVP.mul(Device.mProject, cb.m_WV); + cb.m_WVP.mul(Device.mFullTransform, m_W); cb.L_material.set(0.01903f, 0.74998f, 0.0f, 0.25f); cb.hemi_cube_pos_faces.set(0.08034f, 0.42066f, 0.13277f, 0.0f); @@ -438,7 +453,8 @@ inline void FillSunConstants(StaticGlobals& cb, const SunLightData& sun) { cb.L_hemi_color.set( desc.hemi_color.x * ambScale, desc.hemi_color.y * ambScale, - desc.hemi_color.z * ambScale + desc.hemi_color.z * ambScale, + ps_r2_sun_lumscale_hemi ); } @@ -458,6 +474,17 @@ inline StaticGlobals BuildStaticGlobals(float hdrIntensity = 2.0f, bool allowRtg SunLightData sunData; GetSunLightData(sunData, hdrIntensity); FillSunConstants(sg, sunData); + if (!allowRtgiUnlit && ((ps_r_rt_gi != 0) || (ps_r_path_tracer != 0))) + { + const float invAmb = (ps_r_rt_gi_ambient_scale > 1e-4f) + ? (1.0f / std::clamp(ps_r_rt_gi_ambient_scale, 0.0f, 1.0f)) : 1.0f; + sg.L_ambient.x *= invAmb; + sg.L_ambient.y *= invAmb; + sg.L_ambient.z *= invAmb; + sg.L_hemi_color.x *= invAmb; + sg.L_hemi_color.y *= invAmb; + sg.L_hemi_color.z *= invAmb; + } FillClusterParams(sg); return sg; } diff --git a/src/Layers/xrRender/xrRender_console.cpp b/src/Layers/xrRender/xrRender_console.cpp index 417f266bef2..337542e5a5e 100644 --- a/src/Layers/xrRender/xrRender_console.cpp +++ b/src/Layers/xrRender/xrRender_console.cpp @@ -113,7 +113,7 @@ int ps_r_local_shadow_tiles = 96; int ps_r_local_shadow_update_div = 1; int ps_r_local_shadow_redraw_budget = 16; int ps_r_local_shadow_skinned_max = 24; -float SunshaftsIntensity = 0.f; +ECORE_API float SunshaftsIntensity = 0.f; float ps_r_local_shadow_near = 10.f; float ps_r_local_shadow_mid = 30.f; int ps_r_local_shadow_far_period = 5; @@ -553,6 +553,83 @@ class CCC_Preset : public CCC_Token } }; +#if RENDER == R_R4 +const xr_token qrt_quality_token[] = +{ + { "off", 0 }, + { "low", 1 }, + { "medium", 2 }, + { "high", 3 }, + { "ultra", 4 }, + { nullptr, 0 } +}; + +class CCC_RTQuality : public CCC_Token +{ +public: + CCC_RTQuality(LPCSTR N, u32* V, const xr_token* T) : CCC_Token(N, V, T) {} + + virtual void Execute(LPCSTR args) + { + CCC_Token::Execute(args); + + struct RTQualityPreset + { + int gi; + float intensity; + int bounces; + int spatial_samples; + float spatial_radius; + int m_max; + int local_samples; + int di_candidates; + int di_spatial_samples; + float di_spatial_radius; + int di_m_max; + int atrous_steps; + float ambient_scale; + int cache_size; + float cache_cell; + int vol_steps; + float detail_dist; + float lod_dist; + int sun_soft_samples; + }; + + static const RTQualityPreset kPresets[] = + { + { 0, 1.0f, 1, 2, 24.0f, 4, 2, 6, 2, 16.0f, 10, 3, 0.40f, 131072, 1.00f, 8, 12.f, 30.f, 2 }, + { 1, 1.0f, 1, 2, 24.0f, 4, 2, 6, 2, 16.0f, 10, 3, 0.40f, 131072, 1.00f, 8, 12.f, 30.f, 2 }, + { 1, 1.0f, 1, 4, 40.0f, 8, 4, 8, 4, 32.0f, 20, 4, 0.35f, 262144, 0.75f, 16, 20.f, 40.f, 4 }, + { 1, 1.0f, 2, 6, 48.0f, 12, 6, 12, 6, 40.0f, 24, 5, 0.30f, 393216, 0.60f, 24, 30.f, 50.f, 6 }, + { 1, 1.0f, 2, 8, 56.0f, 16, 8, 16, 8, 48.0f, 30, 6, 0.25f, 524288, 0.50f, 32, 40.f, 60.f, 8 }, + }; + + const u32 idx = (*value <= 4u) ? *value : 2u; + const RTQualityPreset& p = kPresets[idx]; + ps_r_rt_gi = p.gi; + ps_r_rt_gi_intensity = p.intensity; + ps_r_rt_gi_bounces = p.bounces; + ps_r_rt_gi_spatial_samples = p.spatial_samples; + ps_r_rt_gi_spatial_radius = p.spatial_radius; + ps_r_rt_gi_m_max = p.m_max; + ps_r_rt_gi_local_samples = p.local_samples; + ps_r_rt_di_candidates = p.di_candidates; + ps_r_rt_di_spatial_samples = p.di_spatial_samples; + ps_r_rt_di_spatial_radius = p.di_spatial_radius; + ps_r_rt_di_m_max = p.di_m_max; + ps_r_rt_gi_atrous_steps = p.atrous_steps; + ps_r_rt_gi_ambient_scale = p.ambient_scale; + ps_r_rt_gi_cache_size = p.cache_size; + ps_r_rt_gi_cache_cell = p.cache_cell; + ps_r_rt_vol_steps = p.vol_steps; + ps_r_rt_detail_dist = p.detail_dist; + ps_r_rt_gi_lod_dist = p.lod_dist; + ps_r_rt_sun_soft_samples = p.sun_soft_samples; + } +}; +#endif + class CCC_memory_stats : public IConsole_Command { public: @@ -1031,6 +1108,7 @@ void xrRender_initconsole() //CMD3(CCC_Mask, "r2_sun_shafts", &ps_r2_ls_flags, R2FLAG_SUN_SHAFTS); CMD3(CCC_Token, "r2_sun_shafts", &ps_r_sun_shafts, qsun_shafts_token); CMD4(CCC_Float, "r2_sun_shafts_scale", &ps_r_sun_shafts_scale, 0.0f, 4.0f); + CMD4(CCC_SunshaftsIntensity, "r__sunshafts_intensity", &SunshaftsIntensity, 0.f, 1.f); CMD3(CCC_SSAO_Mode, "r2_ssao_mode", &ps_r_ssao_mode, qssao_mode_token); CMD3(CCC_Token, "r2_ssao", &ps_r_ssao, qssao_token); CMD3(CCC_Mask, "r2_ssao_blur", &ps_r2_ls_flags_ext, R2FLAGEXT_SSAO_BLUR); // Need restart @@ -1094,6 +1172,7 @@ void xrRender_initconsole() CMD4(CCC_Integer, "r4_debug_gpu_culling", &ps_r4_debug_gpu_culling, 0, 1); CMD4(CCC_Integer, "r_path_tracer", &ps_r_path_tracer, 0, 1); CMD4(CCC_Integer, "r_path_tracer_bounces", &ps_r_path_tracer_bounces, 1, 16); + CMD3(CCC_RTQuality, "r_rt_quality", &ps_r_rt_quality, qrt_quality_token); CMD4(CCC_Integer, "r_rt_gi", &ps_r_rt_gi, 0, 1); CMD4(CCC_Float, "r_rt_gi_intensity", &ps_r_rt_gi_intensity, 0.0f, 4.0f); CMD4(CCC_Integer, "r_rt_gi_spatial_samples", &ps_r_rt_gi_spatial_samples, 0, 16); @@ -1112,6 +1191,10 @@ void xrRender_initconsole() CMD4(CCC_Float, "r_rt_gi_cache_cell", &ps_r_rt_gi_cache_cell, 0.05f, 8.0f); CMD4(CCC_Integer, "r_rt_vol_steps", &ps_r_rt_vol_steps, 0, 64); CMD4(CCC_Integer, "r_rt_vol_light_samples", &ps_r_rt_vol_light_samples, 0, 8); + CMD4(CCC_Float, "r_rt_detail_dist", &ps_r_rt_detail_dist, 2.f, 64.f); + CMD4(CCC_Float, "r_rt_gi_lod_dist", &ps_r_rt_gi_lod_dist, 10.f, 120.f); + CMD4(CCC_Integer, "r_rt_sun_soft_samples", &ps_r_rt_sun_soft_samples, 1, 8); + CMD4(CCC_Float, "r_rt_sun_angular", &ps_r_rt_sun_angular, 0.001f, 0.05f); CMD4(CCC_Integer, "r_taa", &ps_r_taa, 0, 1); CMD4(CCC_Float, "r_taa_sharpness", &ps_r_taa_sharpness, 0.0f, 1.0f); diff --git a/src/Layers/xrRender/xrRender_console.h b/src/Layers/xrRender/xrRender_console.h index 253312bcf29..4923518cc70 100644 --- a/src/Layers/xrRender/xrRender_console.h +++ b/src/Layers/xrRender/xrRender_console.h @@ -288,7 +288,7 @@ extern ECORE_API float ps_r_smoke_max_width; extern ECORE_API float ps_r_smoke_gravity; extern ECORE_API float ps_r_smoke_buoyancy; extern ECORE_API float ps_r_smoke_turbulence; -extern float SunshaftsIntensity; +extern ECORE_API float SunshaftsIntensity; extern void xrRender_initconsole(); } // namespace xray::render::fg diff --git a/src/xrEngine/xr_ioc_cmd.cpp b/src/xrEngine/xr_ioc_cmd.cpp index 27539a75d72..9c49e3fc1da 100644 --- a/src/xrEngine/xr_ioc_cmd.cpp +++ b/src/xrEngine/xr_ioc_cmd.cpp @@ -26,22 +26,27 @@ ENGINE_API int ps_r_path_tracer = 0; ENGINE_API int ps_r_path_tracer_bounces = 8; ENGINE_API int ps_r_rt_gi = 0; ENGINE_API float ps_r_rt_gi_intensity = 1.0f; -ENGINE_API int ps_r_rt_gi_spatial_samples = 8; +ENGINE_API int ps_r_rt_gi_spatial_samples = 4; ENGINE_API float ps_r_rt_gi_spatial_radius = 40.0f; ENGINE_API int ps_r_rt_gi_m_max = 8; -ENGINE_API int ps_r_rt_gi_local_samples = 16; +ENGINE_API int ps_r_rt_gi_local_samples = 4; ENGINE_API int ps_r_rt_di_candidates = 12; ENGINE_API int ps_r_rt_di_spatial_samples = 6; ENGINE_API float ps_r_rt_di_spatial_radius = 32.0f; ENGINE_API int ps_r_rt_di_m_max = 20; ENGINE_API int ps_r_rt_gi_atrous_steps = 6; ENGINE_API float ps_r_rt_gi_temporal_alpha = 0.96f; -ENGINE_API float ps_r_rt_gi_ambient_scale = 0.65f; -ENGINE_API int ps_r_rt_gi_bounces = 2; +ENGINE_API float ps_r_rt_gi_ambient_scale = 0.35f; +ENGINE_API int ps_r_rt_gi_bounces = 1; ENGINE_API int ps_r_rt_gi_cache_size = 262144; ENGINE_API float ps_r_rt_gi_cache_cell = 0.75f; ENGINE_API int ps_r_rt_vol_steps = 16; ENGINE_API int ps_r_rt_vol_light_samples = 2; +ENGINE_API float ps_r_rt_detail_dist = 20.f; +ENGINE_API float ps_r_rt_gi_lod_dist = 40.f; +ENGINE_API int ps_r_rt_sun_soft_samples = 4; +ENGINE_API float ps_r_rt_sun_angular = 0.0047f; +ENGINE_API u32 ps_r_rt_quality = 2; ENGINE_API int ps_r_taa = 1; ENGINE_API float ps_r_taa_sharpness = 0.65f; ENGINE_API int ps_r_taa_jitter = 1; diff --git a/src/xrEngine/xr_ioc_cmd.h b/src/xrEngine/xr_ioc_cmd.h index 3aba01d1552..6f34e329d4c 100644 --- a/src/xrEngine/xr_ioc_cmd.h +++ b/src/xrEngine/xr_ioc_cmd.h @@ -51,6 +51,11 @@ extern ENGINE_API int ps_r_rt_gi_cache_size; extern ENGINE_API float ps_r_rt_gi_cache_cell; extern ENGINE_API int ps_r_rt_vol_steps; extern ENGINE_API int ps_r_rt_vol_light_samples; +extern ENGINE_API float ps_r_rt_detail_dist; +extern ENGINE_API float ps_r_rt_gi_lod_dist; +extern ENGINE_API int ps_r_rt_sun_soft_samples; +extern ENGINE_API float ps_r_rt_sun_angular; +extern ENGINE_API u32 ps_r_rt_quality; extern ENGINE_API int ps_r_taa; extern ENGINE_API float ps_r_taa_sharpness; extern ENGINE_API int ps_r_taa_jitter; From ca2fa53f87faeb619619cdcd2256d8931776cfe7 Mon Sep 17 00:00:00 2001 From: FoxConED Date: Thu, 30 Jul 2026 00:01:39 +0300 Subject: [PATCH 260/261] xrRender: integrate FrameGraph lighting passes and shared shaders --- res/gamedata/shaders/r5/ao/ao_apply.ps | Bin 876 -> 878 bytes res/gamedata/shaders/r5/ao/ao_blur.ps | Bin 1322 -> 1324 bytes res/gamedata/shaders/r5/ao/ao_common.h | 2 +- res/gamedata/shaders/r5/ao/gtao_render.ps | Bin 4154 -> 4142 bytes res/gamedata/shaders/r5/ao/hbao.ps | Bin 2302 -> 2290 bytes res/gamedata/shaders/r5/ao/ssao.ps | Bin 2783 -> 2876 bytes res/gamedata/shaders/r5/bindless_common.h | 2 +- res/gamedata/shaders/r5/bindless_forward.ps | Bin 5031 -> 5667 bytes res/gamedata/shaders/r5/bindless_forward.vs | 19 +- res/gamedata/shaders/r5/bindless_terrain.ps | Bin 7124 -> 11146 bytes res/gamedata/shaders/r5/bloom/composite.ps | Bin 815 -> 815 bytes res/gamedata/shaders/r5/common_functions.h | 164 ++++++++- res/gamedata/shaders/r5/common_iostructs.h | 1 + res/gamedata/shaders/r5/contact_shadows.ps | Bin 4963 -> 5376 bytes res/gamedata/shaders/r5/cull_utils.h | 107 +++--- res/gamedata/shaders/r5/decal_box.ps | Bin 1625 -> 2617 bytes res/gamedata/shaders/r5/distortion_apply.ps | Bin 1654 -> 2754 bytes res/gamedata/shaders/r5/dlss_rr_guides.cs | 6 +- res/gamedata/shaders/r5/dof.ps | Bin 3091 -> 3087 bytes res/gamedata/shaders/r5/flare_visibility.cs | 7 +- res/gamedata/shaders/r5/ibl_probe_capture.cs | 2 +- res/gamedata/shaders/r5/object_cull_debug.cs | 60 +++- res/gamedata/shaders/r5/postprocess.ps | Bin 3118 -> 2240 bytes .../shaders/r5/shared/clustered_lighting.h | 23 +- .../shaders/r5/shared/shadow_sampling.h | 10 + res/gamedata/shaders/r5/shared/sky_ibl.h | 7 +- res/gamedata/shaders/r5/shared/ssr.h | 195 ++++++++++- res/gamedata/shaders/r5/skinned_mdi_common.h | 5 + res/gamedata/shaders/r5/ssgi_apply.ps | Bin 1607 -> 1644 bytes res/gamedata/shaders/r5/ssgi_blur.ps | Bin 1216 -> 1218 bytes res/gamedata/shaders/r5/ssgi_temporal.ps | Bin 1600 -> 1602 bytes res/gamedata/shaders/r5/ssgi_trace.ps | Bin 4559 -> 4598 bytes res/gamedata/shaders/r5/ssr_apply.ps | Bin 2899 -> 2773 bytes res/gamedata/shaders/r5/ssr_blur.ps | Bin 5362 -> 5360 bytes res/gamedata/shaders/r5/ssr_resolve.ps | Bin 8434 -> 8479 bytes res/gamedata/shaders/r5/ssr_temporal.ps | Bin 5008 -> 5006 bytes res/gamedata/shaders/r5/sssss_blur.ps | Bin 2462 -> 2460 bytes res/gamedata/shaders/r5/sun_forward.ps | Bin 757 -> 566 bytes res/gamedata/shaders/r5/sun_forward.vs | 12 +- res/gamedata/shaders/r5/taa.ps | Bin 7917 -> 7916 bytes .../shaders/r5/volumetric/ray_march.ps | Bin 2403 -> 2401 bytes .../shaders/r5/volumetric/sunshafts.ps | Bin 2654 -> 2189 bytes .../r5/volumetric/sunshafts_combine.ps | Bin 904 -> 849 bytes src/Layers/xrRender/Backend/VulkanBackend.cpp | 86 +++-- src/Layers/xrRender/Backend/VulkanBackend.h | 1 + src/Layers/xrRender/Bindless/BindlessTypes.h | 2 +- src/Layers/xrRender/ClusteredLightManager.cpp | 26 +- src/Layers/xrRender/ClusteredLightManager.h | 1 + .../xrRender/Denoising/IDenoiseBackend.h | 2 +- .../xrRender/Denoising/NRDDenoiseBackend.cpp | 2 +- src/Layers/xrRender/FGDetailManager.cpp | 2 +- src/Layers/xrRender/FGRenderBase.cpp | 63 +--- src/Layers/xrRender/FrameGraph/FrameGraph.cpp | 11 +- .../xrRender/FrameGraph/PassResourceCache.cpp | 45 ++- .../xrRender/FrameGraph/PassResourceCache.h | 1 + .../AmbientOcclusionPassSetup.cpp | 3 +- .../FrameGraphPasses/BloomPassSetup.cpp | 2 +- .../ContactShadowsPassSetup.cpp | 14 +- .../ContactShadowsPassSetup.h | 1 + .../FrameGraphPasses/DecalPassSetup.cpp | 10 +- .../FrameGraphPasses/DepthPrepassSetup.cpp | 4 +- .../FrameGraphPasses/DetailPassSetup.cpp | 17 +- .../DistortionApplyPassSetup.cpp | 4 +- .../ForwardColorPassSetup.cpp | 43 ++- .../FrameGraphPasses/LocalShadowPassSetup.cpp | 73 +++- .../FrameGraphPasses/LocalShadowPassSetup.h | 2 + .../FrameGraphPasses/LodPassSetup.cpp | 27 +- .../xrRender/FrameGraphPasses/PassCommon.cpp | 4 +- .../FrameGraphPasses/PathTracerPassSetup.cpp | 13 +- .../FrameGraphPasses/PostProcessPassSetup.cpp | 18 +- .../FrameGraphPasses/PostProcessPassSetup.h | 1 + .../FrameGraphPasses/RainShadowPassSetup.cpp | 4 +- .../FrameGraphPasses/SSRPassSetup.cpp | 52 ++- .../FrameGraphPasses/ShadowCascadeFit.cpp | 2 +- .../FrameGraphPasses/ShadowPassSetup.cpp | 2 + .../FrameGraphPasses/SunShaftsPassSetup.cpp | 70 ++-- .../FrameGraphPasses/SunShaftsPassSetup.h | 1 + .../FrameGraphPasses/TAAPassSetup.cpp | 9 +- .../FrameGraphPasses/TransparentPassSetup.cpp | 33 +- .../xrRender/FrameGraphPasses/UIPassSetup.cpp | 6 + .../FrameGraphPasses/VolumetricPassSetup.cpp | 8 +- src/Layers/xrRender/GPUCullingManager.cpp | 181 ++++++---- src/Layers/xrRender/GPUCullingManager.h | 28 +- .../xrRender/Geometry/MaterialCache.cpp | 71 ++-- .../Light_Render_Direct_ComputeXFS.cpp | 2 +- .../xrRender/Materials/MaterialSystem.cpp | 6 +- .../xrRender/PBRConverter/ONNXModelRunner.cpp | 43 +-- .../PBRConverter/PBRTextureConverter.cpp | 74 ++--- src/Layers/xrRender/Profiler/StatsOverlay.cpp | 24 ++ .../xrRender/RenderContext/RenderDevice.cpp | 48 ++- .../xrRender/RenderContext/RenderDevice.h | 2 +- src/Layers/xrRender/ResourceManager.cpp | 9 +- .../ResourceManager/TextureManager.cpp | 175 +++++----- .../xrRender/ResourceManager/TextureManager.h | 8 +- src/Layers/xrRender/fgEnvironmentRender.cpp | 62 +++- src/Layers/xrRender/fgLensFlareRender.cpp | 8 +- src/Layers/xrRender/r4_rendertarget.cpp | 17 +- src/Layers/xrRender/r_FrameGraphRenderer.cpp | 311 ++++++++++++------ .../xrRender/r_FrameGraphRenderer_Loader.cpp | 18 +- src/xrCore/_matrix.h | 51 +++ src/xrEngine/IRenderBackend.h | 1 + 101 files changed, 1642 insertions(+), 784 deletions(-) diff --git a/res/gamedata/shaders/r5/ao/ao_apply.ps b/res/gamedata/shaders/r5/ao/ao_apply.ps index f77b87dbba4b6301b36d4079caa89be6579459ef..cdbf322f4e17fb0a2e16e69e3ff5d889fa36a95b 100644 GIT binary patch delta 29 icmaFE_Kt1C9wtH0;^6E`m(+ri42={JsX6&H(?S5T(+jl# delta 27 hcmaFI_J(c49wz>j)Pj->1v^^>13gP1n0%ILApnib33&hj diff --git a/res/gamedata/shaders/r5/ao/ao_blur.ps b/res/gamedata/shaders/r5/ao/ao_blur.ps index 778edde0c2a75ede05ed9842833a46c5b65211f7..2c4ae686b067f899b767e69115de9ff748fd5ca5 100644 GIT binary patch delta 32 ncmZ3*wT5d0JCm?yad38}OKL$$hDLH~UP)?El;&m$CLv}3u`UWN delta 30 kcmZ3(wTf#4JCk5?YF= 0.9999) + if (depth <= 1e-7) return true; if (worldPos.w < 0.5) return true; diff --git a/res/gamedata/shaders/r5/ao/gtao_render.ps b/res/gamedata/shaders/r5/ao/gtao_render.ps index ab2d5e3a3c13499dbd2990a8943d9fb1f846d909..54e9e6c6ded53a67b78ff2a94dba8b829ef0eddd 100644 GIT binary patch delta 11 Scmdm`uufrv75`*6{x1L<4g`Jx delta 23 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 diff --git a/res/gamedata/shaders/r5/common_functions.h b/res/gamedata/shaders/r5/common_functions.h index b6c5926c8c8..d46e487007e 100644 --- a/res/gamedata/shaders/r5/common_functions.h +++ b/res/gamedata/shaders/r5/common_functions.h @@ -114,6 +114,12 @@ float3 v_hemi(float3 n) return L_hemi_color.rgb*(.5f + .5f*n.y); } +float calc_model_hemi(float3 norm_w) +{ + float ny = normalize(norm_w).y; + return saturate(0.52f + 0.48f * ny); +} + float3 v_sun(float3 n) { return L_sun_color*dot(n,-L_sun_dir_w); @@ -218,6 +224,13 @@ float gbuf_unpack_mtl( float mtl_hemi ) } #include "shared/pbr_brdf.h" +#if defined(SKY_IBL) +#include "shared/sky_ibl.h" +#endif +#include "shared/foliage_sss.h" +#include "shared/glass_sss.h" +#include "shared/basecolor_pack.h" +#include "shared/shadow_sampling.h" #include "shared/clustered_lighting.h" float3 worldNormalToView(float3 N) @@ -234,12 +247,18 @@ float3 reconstruct_world_pos(float2 svPosXY, float depth) return world.xyz / world.w; } +bool IsSkyDepth(float d) +{ + return d <= 1e-7; +} + f_forward output_forward_color(float3 albedo, float3 normal, float3 worldPos, float metallic, float roughness) { f_forward res; res.color = float4(albedo, 1.0); res.normal = float4(normalize(normal), roughness); res.baseColor = float4(albedo, metallic); + res.worldPos = float4(worldPos, 1.0); return res; } @@ -250,43 +269,156 @@ f_forward output_forward_pbr( float metallic, float roughness, float ao, - float4 svPosition = float4(0, 0, 0, 0)) + float4 svPosition = float4(0, 0, 0, 0), + float hemi = 1.0, + float sunOcclusion = 1.0, + float sssStrength = 0.0, + float3 sssTint = float3(1, 1, 1), + float sssThickness = 1.0, + float sssMask = 0.0, + float sssProfile = -1.0) { f_forward res; float3 N = normalize(worldNormal); float3 V = normalize(eye_position - worldPos); float3 L = normalize(-L_sun_dir_w); + float albedoLum = dot(albedo, float3(0.2126, 0.7152, 0.0722)); + metallic *= saturate(albedoLum * 3.2 + 0.35); + albedo = SanitizeMetalAlbedo(albedo, metallic); + float hemiTerm = saturate(hemi); + const bool rtgiUnlit = parallax.w < -0.5; + + float3 ambientColor = L_ambient.rgb + L_hemi_color.rgb * L_hemi_color.w * hemiTerm; + float3 ambient = 0; + if (!rtgiUnlit) + { +#if defined(SKY_IBL) + float iblIntensity = parallax.w; + if (iblIntensity > 1e-4) + { + ambient = PBRAmbientIBL( + albedo, N, V, worldPos, + metallic, roughness, ao, + hemiTerm, iblIntensity); + } + else +#endif + { + ambient = PBRAmbient( + albedo, N, V, + metallic, roughness, ao, + ambientColor + ); + } + } + else + { + ambient = PBRAmbient( + albedo, N, V, + metallic, roughness, ao, + ambientColor + ); + } + + float3 finalColor = ambient; + float sunShadow = 0.0; - float3 sunLight = PBRDirectLighting( - albedo, N, V, L, - L_sun_color, - metallic, roughness, (uint)pbr_diffuse_mode - ); + if (!rtgiUnlit) + { + float sunTerm = saturate(sunOcclusion); + float ndl = dot(N, L); + bool needSunShadow = (ndl > 0.0) || (sssStrength > 1e-4); + + float csm = 1.0; + float contact = 1.0; + float2 maskUV = float2(0, 0); + if (svPosition.w != 0) + maskUV = float2(svPosition.x * screen_res.z, svPosition.y * screen_res.w); +#if defined(SHADOW_MASK_FORWARD) + bool useMask = ShadowMaskEnabled() && (svPosition.w != 0); + if (useMask) + { + float3 mask = SampleShadowMask(maskUV); + if (needSunShadow) + { + csm = lerp(0.20, 1.0, saturate(mask.r)); + contact = saturate(mask.g); + } + } + else +#endif + if (needSunShadow) + { +#if defined(CSM_CHEAP_FORWARD) + csm = SampleCSM_Fast(worldPos, N); +#else + csm = SampleCSM(worldPos, N); +#endif + csm = lerp(0.20, 1.0, saturate(csm)); + contact = ContactShadow(worldPos, L); + } + sunShadow = csm * contact * sunTerm; - float3 ambientColor = L_ambient.rgb + L_hemi_color.rgb * L_hemi_color.w; - float3 ambient = PBRAmbient( - albedo, N, V, - metallic, roughness, ao, - ambientColor - ); + finalColor += PBRDirectLighting( + albedo, N, V, L, + L_sun_color, + metallic, roughness, (uint)pbr_diffuse_mode + ) * sunShadow; - float3 finalColor = sunLight + ambient; + if (sssStrength > 1e-4) + { + if (sssProfile > 3.5) + { + finalColor += EvaluateGlassTransmission( + albedo, N, V, L, L_sun_color, sunShadow, + sssThickness, sssStrength); + } + else + { + finalColor += EvaluateFoliageSSS( + albedo, N, V, L, L_sun_color, sunShadow, + sssTint, sssThickness, sssStrength); +#if defined(SKY_IBL) + if (parallax.w > 1e-4) + { + finalColor += EvaluateFoliageSkySSS( + albedo, N, sssTint, sssThickness, sssStrength * parallax.w); + } +#endif + } + } + } #ifdef CLUSTERED_LIGHTING_FORWARD if (svPosition.w != 0) { - float linearDepth = mul(m_V, float4(worldPos, 1.0)).z; + float linearDepth = abs(mul(m_V, float4(worldPos, 1.0)).z); float3 clusterLights = EvaluateClusteredLights( worldPos, N, V, albedo, metallic, roughness, - svPosition.xy, linearDepth, (uint)pbr_diffuse_mode); + svPosition.xy, linearDepth, (uint)pbr_diffuse_mode, 1.0); finalColor += clusterLights; + + int clusterDebug = (int)(dev_param_2.w + 0.5); + if (clusterDebug > 0) + finalColor = ClusterDebugColor(svPosition.xy, linearDepth, clusterDebug); } #endif + { + float dist = length(worldPos - eye_position); + float fog = saturate(dist * fog_params.w + fog_params.x); + finalColor = lerp(finalColor, fog_color.rgb, fog); + } + + int shadowDebug = (int)(dev_param_3.z + 0.5); + if (shadowDebug > 0) + finalColor = ShadowDebugColor(worldPos, N, L, shadowDebug); + res.color = float4(finalColor, 1.0); res.normal = float4(N, roughness); - res.baseColor = float4(albedo, metallic); + res.baseColor = float4(albedo, PackBaseColorA(metallic, sssMask)); + res.worldPos = float4(worldPos, 1.0); return res; } diff --git a/res/gamedata/shaders/r5/common_iostructs.h b/res/gamedata/shaders/r5/common_iostructs.h index c0fa6939f22..ea325e5dc20 100644 --- a/res/gamedata/shaders/r5/common_iostructs.h +++ b/res/gamedata/shaders/r5/common_iostructs.h @@ -309,6 +309,7 @@ struct f_forward float4 color : SV_Target0; float4 normal : SV_Target1; float4 baseColor : SV_Target2; + float4 worldPos : SV_Target3; }; struct gbuffer_data diff --git a/res/gamedata/shaders/r5/contact_shadows.ps b/res/gamedata/shaders/r5/contact_shadows.ps index 462b098c34e0e184ae10fe472df9758a45862c86..ecb829b3ed62f37ea2cb5a6a4f9fc3ed50b7f409 100644 GIT binary patch delta 1096 zcma)4O=}ZT6lKzC%tvcmS`cdLZEIvEolMdsQ%chcLeW+%2&JhMp^TZAPG~X-^N~r4 zV9~W;d2U=tL3i#XTM=BSzd<(&#kIdc=zVX-v}h4$Gc)g=bKbq@o}+8~se^md6Vp|* zTyu2-Q?{X5M9{D6ViA* zCqW8dc~vS#qkvDf0-~jLSbY&@NjgstL*w*e{}TP&|9n_gVFt`b)g}Rr?w)%c4!*mC z=Wh!`tM>YCXPGn|19iM*iB`fBSD7+dzeh&<3t(*0XR%8Ij4y+sb0GZ|8Jo(3!9ul!h!v&X*~4L2eAu6dD3VrD zVJu~oSh^6VJ+Q^PTNCTsCE4$zATkB6C`rm5gVV#Ii%Fl6_H)ThI*BEQEr-6PpwN9c z6c_UTJkWGoctg)*14F)13jAUV|dSZEA^bO#$ zr#%P`0vrD91DP4p`j;(?f7%&|z8{`Q@(|#TSe3KSvWChdPZEBweh67odd6gxd6gcG G>>mRX(=nEP&rr)5)W!IF+^jgyR+K-;b`{uphzt_qAy7FLQ0c;X-Wb?<& zLk@{qK4NgxIRv*A1k9!whfuHgFM-Zlp#gKiaWf(m1@bfScRUU(Lfn^U*AFCrscrEibxHhAZH+9>Lm^+V#Lc0r+M+lZdiXTDEunJquVt)) z*-rn$)rwP-cP3jc0d@W;Q5?^gXk`%^MIWl48#SlWQG@B81EyS* zxMs+_%7yNmiE&MQn4G$!?Cn)x0h(w}k0@@}=Anp9%P@?SWpapZtAtd;928|Ba#C+0 n|4HfR radius) + if (dist > r) return false; } return true; @@ -89,93 +94,121 @@ bool DistanceTestAABB(float3 aabb_min, float3 aabb_max, float3 camera_pos, float // ═══════════════════════════════════════════════════════ // HI-Z OCCLUSION CULLING // ═══════════════════════════════════════════════════════ -// Single source of truth for the Hi-Z occlusion test. -// pyramidViewProj must be the view-projection the pyramid's depth was -// rendered with (the previous frame's for temporal Hi-Z). +// Requires caller to define: +// Texture2D g_HiZPyramid : register(t1); -struct HiZTestResult -{ - bool visible; - float frontDepth; - float hiZDepth; -}; -HiZTestResult HiZTestSphereEx( +// Hi-Z occlusion test for sphere (4-tap conservative) +// Returns: true = visible, false = occluded (cull) +// +// TEMPORAL HI-Z: When using previous frame's depth for Hi-Z: +// - Use prevViewProj for Hi-Z UV calculation (matches depth buffer camera) +// - Use currentViewProj for depth comparison (current camera position) +// - This handles camera movement without false culling +bool HiZTestSphereTemporal( float3 center, float radius, float3 cameraPos, - float4x4 pyramidViewProj, + float4x4 currentViewProj, // Current frame - for frustum/depth compare + float4x4 prevViewProj, // Previous frame - for Hi-Z UV lookup Texture2D hiZPyramid, SamplerState pointSampler, uint hiZWidth, uint hiZHeight, uint hiZMipLevels) { - HiZTestResult result; - result.visible = true; - result.frontDepth = 0.0; - result.hiZDepth = 0.0; + // Camera near/inside the bounding volume: large indoor wall batches have huge + // spheres; temporal Hi-Z cannot represent thin walls wrapping the eye. + // 2.5*radius (squared 6.25) — was 1.5 and still popped Skadovsk-class interiors. + float3 toCenter = center - cameraPos; + float r2 = radius * radius; + if (dot(toCenter, toCenter) <= r2 * 6.25) + return true; - float4 clipPos = mul(pyramidViewProj, float4(center, 1.0)); - if (clipPos.w <= 0.001) - return result; + // Project sphere center to PREVIOUS frame's clip space (matches Hi-Z data) + float4 prevClipPos = mul(prevViewProj, float4(center, 1.0)); - float3 ndc = clipPos.xyz / clipPos.w; + // Behind previous camera - conservatively visible + if (prevClipPos.w <= 0.001) + return true; - float projScale = max(abs(pyramidViewProj[0][0]), abs(pyramidViewProj[1][1])); - float2 ndcSize = float2(radius, radius) * projScale / clipPos.w; + // Perspective divide -> NDC (previous frame) + float3 prevNdc = prevClipPos.xyz / prevClipPos.w; - float2 minNDC = ndc.xy - ndcSize; - float2 maxNDC = ndc.xy + ndcSize; + // Calculate screen-space bounding box (in previous frame's space) + float projScale = max(abs(prevViewProj[0][0]), abs(prevViewProj[1][1])); + float2 ndcSize = float2(radius, radius) * projScale / prevClipPos.w * 1.5; - if (any(minNDC < -1.0) || any(maxNDC > 1.0)) - return result; + float2 minNDC = prevNdc.xy - ndcSize; + float2 maxNDC = prevNdc.xy + ndcSize; - float2 minUV = minNDC * 0.5 + 0.5; - float2 maxUV = maxNDC * 0.5 + 0.5; + // Conservatively visible if off-screen in previous frame + if (any(minNDC > 1.0) || any(maxNDC < -1.0)) + return true; + // Convert NDC to UV space [0, 1] + float2 minUV = saturate(minNDC * 0.5 + 0.5); + float2 maxUV = saturate(maxNDC * 0.5 + 0.5); + + // Flip Y (NDC Y+ is up, UV Y+ is down) minUV.y = 1.0 - minUV.y; maxUV.y = 1.0 - maxUV.y; + // Re-sort after Y flip float4 boxUV = float4(min(minUV, maxUV), max(minUV, maxUV)); + // Calculate box size in pixels float boxWidth = (boxUV.z - boxUV.x) * float(hiZWidth); float boxHeight = (boxUV.w - boxUV.y) * float(hiZHeight); + // Large screen footprint (doors/walls filling the view) → never occlusion-cull. + // Sphere Hi-Z is unreliable for near-field interior geometry. + float screenArea = boxWidth * boxHeight; + float fullArea = float(hiZWidth) * float(hiZHeight); + if (screenArea > fullArea * 0.18) + return true; + + // Coarser mip = larger MAX footprint → fewer false occlusions. float mipLevel = ceil(log2(max(1.0, max(boxWidth, boxHeight)))); mipLevel = clamp(mipLevel, 0.0, float(hiZMipLevels - 1)); + // Sample Hi-Z at 4 corners (previous frame's depth) float d1 = hiZPyramid.SampleLevel(pointSampler, float2(boxUV.x, boxUV.y), mipLevel); float d2 = hiZPyramid.SampleLevel(pointSampler, float2(boxUV.z, boxUV.y), mipLevel); float d3 = hiZPyramid.SampleLevel(pointSampler, float2(boxUV.x, boxUV.w), mipLevel); float d4 = hiZPyramid.SampleLevel(pointSampler, float2(boxUV.z, boxUV.w), mipLevel); - result.hiZDepth = min(min(d1, d2), min(d3, d4)); + float hiZDepth = min(min(d1, d2), min(d3, d4)); float3 viewDir = normalize(center - cameraPos); float3 frontPoint = center - viewDir * radius; - float4 frontClip = mul(pyramidViewProj, float4(frontPoint, 1.0)); + float4 frontClip = mul(prevViewProj, float4(frontPoint, 1.0)); + if (frontClip.w <= 0.001) - return result; + return true; + + float frontDepth = frontClip.z / frontClip.w; + + const float depthSlop = 0.0025; - result.frontDepth = frontClip.z / frontClip.w; - result.visible = result.frontDepth >= result.hiZDepth; - return result; + return frontDepth + depthSlop >= hiZDepth; } +// Legacy single-viewProj version (for non-temporal Hi-Z) bool HiZTestSphere( float3 center, float radius, float3 cameraPos, - float4x4 pyramidViewProj, + float4x4 viewProj, Texture2D hiZPyramid, SamplerState pointSampler, uint hiZWidth, uint hiZHeight, uint hiZMipLevels) { - return HiZTestSphereEx(center, radius, cameraPos, pyramidViewProj, - hiZPyramid, pointSampler, hiZWidth, hiZHeight, hiZMipLevels).visible; + // Use same viewProj for both sampling and depth comparison + return HiZTestSphereTemporal(center, radius, cameraPos, viewProj, viewProj, + hiZPyramid, pointSampler, hiZWidth, hiZHeight, hiZMipLevels); } #endif // CULL_UTILS_H diff --git a/res/gamedata/shaders/r5/decal_box.ps b/res/gamedata/shaders/r5/decal_box.ps index cd3a04c5433f0960332344f0fb4d5e0ae56cf505..d97992c64a63f74d3a515996fcb4e419f2dc445c 100644 GIT binary patch literal 2617 zcmbVOZExE)5dN-TaczJ=PCd2{#YHYXp?>oNP za_g?Zz6cU|_uSp{+>ts<=z?VwX5;9WsNtQlEKbXW!t0o?*E|bWuPbYFmL(}Iis;m4 zu_A&dKR>UDcv5cOnT75TdN-r!Zglf5nuRyvum=E*^5piPgV7+I+(Yx^_V{rW4yU8P zp;dZbAw?NWcsG6Q)0m`vB8m6eLugo}oXF0m1J7KfNyzp7{sl8=OkuwTV2YySR=qn&|<}pAQmZE7MI@3rRT--a0((SNfy&#A3>c%)39SY z0M%rfzt0F*mY1?-QV5AyQrWOsR>=cRHgxwKe$;-!A>KbGt2yT>tgT#V?`y|E>ksEp z>3Y|+g5dT!ity(lwYqPAuY(+hNeBvMS|hgif>YN zYzLdIl4d^>SelaBl!kA?h?4|;WZOSL)uB;CFXg0C92T>uZD09eefa!tMwW9ubEu-4 zM;*!a%#VU;e^^o;ZgYyBRiNOxLOCmD68j)l=+^oQn!#K6@&!q>E7#D*mGkq8NheT^ zs_N3pAu<@0~&9VkvC8>(^$|ZI}Gsor$f87$!-)8hb5sbXo z&ad@4^W+9#4@TTiGU?JRMLnK!fo?Oh&QscFi$z(WN<`K% zc2!MSQ%Ta4#dQ$8T&^;_=j%Xt9daUYgKW$gJkWxtpQuY>di&teP|W~U{UMHxs*~60 z1!6gei!KJMYTO2zEHUFkRjnBR2^`@j6I~{(SvEjBw81yuxda1fq5R79&dn%CAQ7Ro$3#duy^-iZ~i8o-~+H39FB%$TM-^}jX zYd2TQ@c~O_-kX^>Z^pb#`7$dwOh3$D&Evt#tXPOTEI`f~)EL{Nn1Pt<_osD1&O;e%B? z=rB%fM)(zo^-VX~zAFYB7z}Qt6mYJ8uhR?8R8x3;@9H+KbI}z3S6wJ6#NSpMR659t z3N~2(UUG!b;}gK=?(QZu5msiZvykZ#T6i6B;9M2F`~$Lk~0-g{&y+D#Xd5CudCE@+$7S zXAHX8H)^g~KG&S-OW=Mp?8-1jjfI_cT+Y<)qm{(S-8&tAl;xo@~@5s?T zYLe1OKpJ-Ajvpw;3!VDaR;&R`Fw4k#nDA#JX`6J@yvo?33{P8 zFXoEt=)qaMB{o;!`}79*(=5b6<-i1IdT2HIYSi}=NV%?>5wIH_-Cg4#VidwM}?pZ|64wPqd5K5BwRBXL+{G-aVbIuiBGIgCp9f>_l^Fk;ai7 z^5>b+-M1A}r4jJy6WP*tM}1&U?E9KNAM{af4sN7wZ_L2@!Ia7e+r(Qb0#p@&qyQzS<8@$WzJr4c_ DV?awm literal 1654 zcmbW0L2ueX5QXpl6{F+=DRmuatF|GzgdULEfC^2jUaXZpz)Hqzt(S%*;=gys>s{MP z6Gin6`^}p-Gatt(u4Im|_#S_Zqu^NPNw!N7juN%qsyy5r1;G;cW~XsDfpxsdMNw{) zfiIwOElY#CXQKXDM>L`LN*k&2j_kB88?Q1Y%mrCaurQmBa7e64iJmO`)5MBSFD9cm%AMTT(B!Tp$_ecLOyMV!$(g&tVL(50Y>eEcD()v828 zCLPmof7eohQAT7pDA${(%3B(P{p1RIZQU56Fp8qne)W|V8tHa<>vFHk+JuEt?R0|I z5^sM$349Ol+A&?I+zg?Rd(1AY1Dr!o(yM;8;?SM$(X`1q1rBE5q?u5`Qe(n^P?!7mQtLKu9NL!s%8TqoxZq9p;lVGmqGklO z=w?pMsE5ier}3@QSsK%Dl$Jl!6Eo2J68c@$ja`+$CFMEYRj4M@^9)%Iefaslg%TTaME~`fkiOxl>G%Q--j193 zACF1!rj5iQ%2fEe7r9^)_jD$4aCCTMIzD~q;=$Fv7wz7n>@I5mDXckE2-ZlviYf0( HG#rAzjnWX^ diff --git a/res/gamedata/shaders/r5/dlss_rr_guides.cs b/res/gamedata/shaders/r5/dlss_rr_guides.cs index c848d37bacd..738d416e2d0 100644 --- a/res/gamedata/shaders/r5/dlss_rr_guides.cs +++ b/res/gamedata/shaders/r5/dlss_rr_guides.cs @@ -41,7 +41,7 @@ void main(uint3 dtid : SV_DispatchThreadID) return; float depth = g_Depth.Load(int3(pixel, 0)); - if (depth >= 1.0) + if (depth <= 0.0) { u_DiffuseAlbedo[pixel] = float4(1.0, 1.0, 1.0, 1.0); u_SpecularAlbedo[pixel] = 0; @@ -64,6 +64,10 @@ void main(uint3 dtid : SV_DispatchThreadID) float3 diffAlb = albedo * (1.0 - metallic); float3 specAlb = EnvBRDFApprox2(F0, alpha, NoV); float specHit = max(g_NoisySpecular.Load(int3(pixel, 0)).a, 0.0); + float r2 = roughness * roughness; + float minFootprint = (0.15 + 4.5 * r2) / max(NoV, 0.12); + specHit = max(specHit, minFootprint); + specHit *= (1.0 + 5.0 * r2); u_DiffuseAlbedo[pixel] = float4(diffAlb, 1.0); u_SpecularAlbedo[pixel] = float4(specAlb, 1.0); diff --git a/res/gamedata/shaders/r5/dof.ps b/res/gamedata/shaders/r5/dof.ps index 2e21966ac19204f8ae075dbb2622b30ed48661f1..76e76db9fc7bc130aa33fc51a8d71bd0e054b72d 100644 GIT binary patch delta 32 hcmbO%(J!&Vh>hLGR>3e;*L<@z+fNo4XRg?ER>45e($dm&vnAV476f;)CHFM|uC582 diff --git a/res/gamedata/shaders/r5/flare_visibility.cs b/res/gamedata/shaders/r5/flare_visibility.cs index 15ae89f4b84..76a0f570717 100644 --- a/res/gamedata/shaders/r5/flare_visibility.cs +++ b/res/gamedata/shaders/r5/flare_visibility.cs @@ -26,9 +26,12 @@ void main(uint3 tid : SV_GroupThreadID) float a = float(tid.x) * 2.39996323; float2 p = sun_pos_px + r * float2(cos(a), sin(a)); - uint vis = 1; + uint vis = 0; if (p.x >= 0.0 && p.y >= 0.0 && p.x < dim_x && p.y < dim_y) - vis = (g_Depth.Load(int3(int2(p), 0)) <= 0.0) ? 1 : 0; + { + float d = g_Depth.Load(int3(int2(p), 0)); + vis = (d <= 1e-7) ? 1 : 0; + } InterlockedAdd(gs_visible, vis); GroupMemoryBarrierWithGroupSync(); diff --git a/res/gamedata/shaders/r5/ibl_probe_capture.cs b/res/gamedata/shaders/r5/ibl_probe_capture.cs index 0d53de0a43d..759e8ebaf45 100644 --- a/res/gamedata/shaders/r5/ibl_probe_capture.cs +++ b/res/gamedata/shaders/r5/ibl_probe_capture.cs @@ -71,7 +71,7 @@ void main(uint3 dtID : SV_DispatchThreadID) if (!ProjectWorld(p, suv, vz)) continue; float rawDepth = g_SceneDepth.SampleLevel(smp_nofilter, suv, 0).x; - if (rawDepth >= 0.9995) + if (rawDepth <= 1e-7) continue; float4 worldH = mul(cb_InvVP, float4(suv.x * 2.0 - 1.0, 1.0 - suv.y * 2.0, rawDepth, 1.0)); float3 hitPos = worldH.xyz / max(worldH.w, 1e-5); diff --git a/res/gamedata/shaders/r5/object_cull_debug.cs b/res/gamedata/shaders/r5/object_cull_debug.cs index e480d313b79..e7cec1b98f3 100644 --- a/res/gamedata/shaders/r5/object_cull_debug.cs +++ b/res/gamedata/shaders/r5/object_cull_debug.cs @@ -28,7 +28,6 @@ struct GPUObjectData cbuffer CullDebugParams : register(b5) { float4x4 g_ViewProj; // View-projection matrix - float4x4 g_PrevViewProj; // Previous frame (for Hi-Z sampling) float3 g_CameraPos; // Camera world position float g_MaxDistance; // Maximum render distance (squared) float4 g_FrustumPlanes[6]; // View frustum planes (world space) @@ -63,9 +62,62 @@ struct GPUObjectData // Returns: x = object depth, y = hi-z depth, z = 1 if visible, 0 if occluded float3 OcclusionTestSphereDebug(float3 center, float radius) { - HiZTestResult r = HiZTestSphereEx(center, radius, g_CameraPos, g_PrevViewProj, - g_HiZPyramid, smp_nofilter, g_HiZWidth, g_HiZHeight, g_HiZMipLevels); - return float3(r.frontDepth, r.hiZDepth, r.visible ? 1.0 : 0.0); + float3 result = float3(0.0, 0.0, 1.0); // Default: visible + + // Project sphere center to clip space (use our cbuffer, not common.h's m_VP) + float4 clipPos = mul(g_ViewProj, float4(center, 1.0)); + + // Behind camera - conservatively visible + if (clipPos.w <= 0.001) + return float3(0.0, 1.0, 1.0); + + float3 ndc = clipPos.xyz / clipPos.w; + + // Calculate screen-space bounding box + float projScale = max(abs(g_ViewProj[0][0]), abs(g_ViewProj[1][1])); + float2 ndcSize = float2(radius, radius) * projScale / clipPos.w; + float2 minNDC = ndc.xy - ndcSize; + float2 maxNDC = ndc.xy + ndcSize; + + // Completely outside screen + if (any(minNDC > 1.0) || any(maxNDC < -1.0)) + return float3(1.0, 0.0, 0.0); // Occluded (off-screen) + + // Convert to UV space + float2 minUV = minNDC * 0.5 + 0.5; + float2 maxUV = maxNDC * 0.5 + 0.5; + minUV.y = 1.0 - minUV.y; + maxUV.y = 1.0 - maxUV.y; + float4 boxUV = float4(min(minUV, maxUV), max(minUV, maxUV)); + + // Calculate mip level + float boxWidth = (boxUV.z - boxUV.x) * float(g_HiZWidth); + float boxHeight = (boxUV.w - boxUV.y) * float(g_HiZHeight); + float mipLevel = floor(log2(max(1.0, max(boxWidth, boxHeight) * 0.5))); + mipLevel = clamp(mipLevel, 0.0, float(g_HiZMipLevels - 1)); + + // Sample Hi-Z at 4 corners + float d1 = g_HiZPyramid.SampleLevel(smp_nofilter, float2(boxUV.x, boxUV.y), mipLevel); + float d2 = g_HiZPyramid.SampleLevel(smp_nofilter, float2(boxUV.z, boxUV.y), mipLevel); + float d3 = g_HiZPyramid.SampleLevel(smp_nofilter, float2(boxUV.x, boxUV.w), mipLevel); + float d4 = g_HiZPyramid.SampleLevel(smp_nofilter, float2(boxUV.z, boxUV.w), mipLevel); + float hiZDepth = min(min(d1, d2), min(d3, d4)); + + // Calculate object's front depth + float3 viewDir = normalize(center - g_CameraPos); + float3 frontPoint = center - viewDir * radius; + float4 frontClip = mul(g_ViewProj, float4(frontPoint, 1.0)); + + if (frontClip.w <= 0.001) + return float3(0.0, hiZDepth, 1.0); // Near plane intersection - visible + + float objectDepth = saturate(frontClip.z / frontClip.w); + + // Depth comparison + float depthBias = 0.0001; + bool visible = objectDepth + depthBias >= hiZDepth; + + return float3(objectDepth, hiZDepth, visible ? 1.0 : 0.0); } // ═══════════════════════════════════════════════════════ diff --git a/res/gamedata/shaders/r5/postprocess.ps b/res/gamedata/shaders/r5/postprocess.ps index 649c14a01ea43592076ccaedb9af9e8a4d5d6885..d13f7c2272856f213c849b5f04523f2288fc3b9c 100644 GIT binary patch delta 65 zcmZ1{aX@fG2=nF_%}wr+7M6{2V63)AOsb{A*HMqrLtK_QT@FT zExOWS_vMe%2Aq4I4@`D? zZokbTAl-zmyL;b${Lfkec59oE&cZ~Fa`Yueuxp~%gZ<+M0_)rTUp1(qRA`<_l~hww zjZ~h_B`uQ(I_TMVa_Iz!AFO;WyqKVwq*S{8^rtWO1i(W7{Ue`=>PU!Q8y9 z1J|~ED!^r~u{vanZ~*%VU@zE!2A?_piR-ZsoI9b_3=gUvQ7EPu{;@Of?Bn{Vc~ftX zR*Y+kVif0DUg`qbNiu!{BKt8a(_~sHIiLSQK)h+HnUMIOhNJGhO`|PCWJC0bMSl&j qe%jhP60P7H5qovj)9Q}U`98c~4;^9#2{B?H+7M#{o}ZducU}R3-t$!e diff --git a/res/gamedata/shaders/r5/shared/clustered_lighting.h b/res/gamedata/shaders/r5/shared/clustered_lighting.h index c554f11299f..e6f26e05adf 100644 --- a/res/gamedata/shaders/r5/shared/clustered_lighting.h +++ b/res/gamedata/shaders/r5/shared/clustered_lighting.h @@ -162,17 +162,28 @@ float3 EvaluateClusteredLights( if (texIdx != 0xFFFFFFFFu) { float4 projPos = mul(light.spotVP, float4(worldPos, 1.0)); - if (projPos.w > 0) + float3 spotDir = light.directionAndSpotScale.xyz; + float spotScale = light.directionAndSpotScale.w; + float spotOffset = light.spotParamsAndType.x; + float cone = SpotLightAttenuation(toLight, spotDir, spotScale, spotOffset); + if (projPos.w > 1e-4) { float2 projUV = projPos.xy / projPos.w * 0.5 + 0.5; projUV.y = 1.0 - projUV.y; - Texture2D spotTex = GetBindlessTexture(texIdx); - float4 texSample = spotTex.SampleLevel(smp_rtlinear, projUV, 0); - atten *= texSample.r; + if (all(projUV >= 0.0) && all(projUV <= 1.0)) + { + Texture2D spotTex = GetBindlessTexture(texIdx); + float4 texSample = spotTex.SampleLevel(smp_rtlinear, projUV, 0); + atten *= texSample.r * cone; + } + else + { + atten = 0.0; + } } else { - atten = 0; + atten = 0.0; } } else @@ -187,7 +198,7 @@ float3 EvaluateClusteredLights( if (atten > 0.001f) { float shadow = 1.0; - if (light.localShadowRect.w > 0.5) + if (light.localShadowRect.w > 0.5 && invRangeSq >= 0.0) { float soft = (lightType > 0.5f) ? max(dev_param_1.z, 0.1) diff --git a/res/gamedata/shaders/r5/shared/shadow_sampling.h b/res/gamedata/shaders/r5/shared/shadow_sampling.h index aa75c1dd6d0..a475f6f29a6 100644 --- a/res/gamedata/shaders/r5/shared/shadow_sampling.h +++ b/res/gamedata/shaders/r5/shared/shadow_sampling.h @@ -562,6 +562,11 @@ float SampleCSM_Volumetric(float3 worldPos) return result * rangeFade; } +float SampleCSM_SunShafts(float3 worldPos) +{ + return SampleCSM_Volumetric(worldPos); +} + // Overload without a normal (volumetric march, debug): no normal-offset bias. float SampleCSM(float3 worldPos) { @@ -629,6 +634,11 @@ float SampleCSM_Volumetric(float3 worldPos) return 1.0; } +float SampleCSM_SunShafts(float3 worldPos) +{ + return 1.0; +} + float3 ShadowDebugColor(float3 worldPos, float3 N, float3 L, int mode) { float ndl = saturate(dot(normalize(N), normalize(L))); diff --git a/res/gamedata/shaders/r5/shared/sky_ibl.h b/res/gamedata/shaders/r5/shared/sky_ibl.h index bd0d7518a63..72c9436baae 100644 --- a/res/gamedata/shaders/r5/shared/sky_ibl.h +++ b/res/gamedata/shaders/r5/shared/sky_ibl.h @@ -156,8 +156,9 @@ float3 PBRAmbientIBL( float3 F0 = CalculateF0(albedo, metallic); float NdotV = max(dot(N, V), 0.0); float3 F = F_SchlickRoughness(NdotV, F0, roughness); + float hemiOcc = saturate(hemiOcclusion); float hemiDir = 0.55 + 0.45 * saturate(N.y); - float3 weatherScale = max(L_ambient.rgb + L_hemi_color.rgb * L_hemi_color.w * hemiDir, 0.0); + float3 weatherScale = max(L_ambient.rgb + L_hemi_color.rgb * L_hemi_color.w * hemiDir * hemiOcc, 0.0); float4 iblMeta = g_IBLSkySH[IBL_SH_COUNT * 2]; float mipCount = max(iblMeta.x, 1.0); @@ -188,7 +189,7 @@ float3 PBRAmbientIBL( float match = weatherLum / irrLum; match = clamp(match, 0.75, 1.6); irradiance *= match * max(intensity, 0.35) * max(ssgiScale, 0.65); - irradiance = max(irradiance, weatherScale * lerp(0.85, 0.7, overcast)); + irradiance = max(irradiance * hemiOcc, weatherScale * lerp(0.85, 0.7, overcast)); float roughSpec = saturate(1.05 - roughness * 1.15); specEnv *= weatherScale * max(intensity, 0.35) * roughSpec; @@ -203,7 +204,7 @@ float3 PBRAmbientIBL( float3 specularAmbient = EnvBRDF(F0, NdotV, roughness, lut) * specEnv; specularAmbient *= roughSpec; - return (diffuseAmbient + specularAmbient) * ao * saturate(hemiOcclusion); + return (diffuseAmbient + specularAmbient) * ao; } #endif diff --git a/res/gamedata/shaders/r5/shared/ssr.h b/res/gamedata/shaders/r5/shared/ssr.h index e56fd90d0a6..e4a386e9c64 100644 --- a/res/gamedata/shaders/r5/shared/ssr.h +++ b/res/gamedata/shaders/r5/shared/ssr.h @@ -89,7 +89,7 @@ float4 compute_ssr_ex( float3 V = normalize(position - eye_position); float3 R = reflect(V, N); #ifdef SSR_WATER_DOWNWARD_BIAS - if (R.y < -0.2) + if (R.y < -0.55) return float4(skybox, 0.0); #endif @@ -152,7 +152,6 @@ float4 compute_ssr_ex( float w; if (!WorldToUv(marchPos, uv, w)) { - // Behind camera: keep last on-screen sample with soft conf (no hard cut) if (tPrev > tMin && all(prevUV >= 0.0) && all(prevUV <= 1.0)) { hitUV = prevUV; @@ -164,8 +163,6 @@ float4 compute_ssr_ex( } if (any(uv < 0.0) || any(uv > 1.0)) { - // Left frustum: clamp to edge — all wet pixels share ~same R, so a hard - // miss here snaps the WHOLE screen (sky or geo). Keep edge sample. if (tPrev > tMin) hitUV = saturate(prevUV); else @@ -180,7 +177,7 @@ float4 compute_ssr_ex( float sceneVZ = SampleSceneViewZ(uv, rawDepth); float marchVZ = abs(mul(m_V, float4(marchPos, 1.0)).z); - if (rawDepth >= 0.9995) + if (IsSkyDepth(rawDepth)) { #ifdef SSR_ACCEPT_SKY if (t > tMin * 1.25) @@ -197,7 +194,7 @@ float4 compute_ssr_ex( } float delta = sceneVZ - marchVZ; - if (delta <= thickness && delta >= -thickness * 5.0) + if (delta <= thickness && delta >= -thickness) { #ifdef SSR_OPAQUE_REFLECTOR float minUvSep = lerp(0.028, 0.0035, nearAmt); @@ -245,12 +242,14 @@ float4 compute_ssr_ex( float mHit = abs(mul(m_V, float4(origin + R * (0.5 * (distA + distB)), 1.0)).z); float err = abs(sHit - mHit); hitConf = saturate(1.0 - err / max(thickness * 2.5, 1e-3)); + float3 hitPos = ReconstructWorld(hitUV, dHit); + float toward = dot(normalize(hitPos - origin), R); + hitConf *= saturate(toward * 2.0); hit = hitConf > 0.04; break; } - else if (delta < -thickness * 5.0) + else if (delta < -thickness) { - // Occluded: soft keep last UV instead of hard kill (reduces yaw snap) if (tPrev > tMin * 1.5 && all(prevUV >= 0.0) && all(prevUV <= 1.0)) { hitUV = prevUV; @@ -314,6 +313,177 @@ float4 compute_ssr(float3 position, float3 normal, float3 skybox) SSR_SAMPLES, SSR_DISTANCE, SSR_THICKNESS, SSR_REFINE, 0.0); } +float4 compute_ssr_water(float3 position, float3 normal, float3 skybox) +{ + float3 N = normalize(lerp(normalize(normal), float3(0.0, 1.0, 0.0), 0.7)); + float3 V = normalize(position - eye_position); + float3 R = reflect(V, N); + if (R.y < 0.02) + R = normalize(float3(R.x, 0.02, R.z)); + R = normalize(R); + + const int steps = 36; + const float maxDist = 160.0; + const float thickness = 1.15; + const int refineCount = 6; + const float minUvSep = 0.008; + const float tMin = 0.85; + + float3 origin = position + N * 0.1; + float2 startUV = 0.0; + float startW = 1.0; + if (!WorldToUv(origin, startUV, startW)) + return float4(skybox, 0.0); + + float2 dirUV = 0.0; + { + float2 uv1 = 0.0; + float w1 = 1.0; + if (!WorldToUv(origin + R * 3.0, uv1, w1)) + return float4(skybox, 0.0); + dirUV = uv1 - startUV; + float dirLen = length(dirUV); + if (dirLen < 1e-4) + return float4(skybox, 0.0); + dirUV /= dirLen; + } + + float2 hitUV = 0.0; + bool hit = false; + float hitConf = 0.0; + float hitT = 0.0; + + float tPrev = tMin; + float2 prevUV = startUV; + { + float pw = 1.0; + WorldToUv(origin + R * tMin, prevUV, pw); + } + + [loop] + for (int i = 1; i <= steps; ++i) + { + float u = float(i) / float(steps); + float t = max(tMin, maxDist * u * u); + float3 marchPos = origin + R * t; + + float2 uv = 0.0; + float w = 1.0; + if (!WorldToUv(marchPos, uv, w)) + break; + if (any(uv < 0.0) || any(uv > 1.0)) + break; + + float rawDepth = 0.0; + float sceneVZ = SampleSceneViewZ(uv, rawDepth); + if (IsSkyDepth(rawDepth)) + { + prevUV = uv; + tPrev = t; + continue; + } + + float3 scenePos = ReconstructWorld(uv, rawDepth); + if (scenePos.y < position.y - 0.2) + { + prevUV = uv; + tPrev = t; + continue; + } + + float3 toScene = scenePos - origin; + float along = dot(toScene, R); + if (along < tMin) + { + prevUV = uv; + tPrev = t; + continue; + } + + float perp = length(scenePos - (origin + R * along)); + float marchVZ = abs(mul(m_V, float4(marchPos, 1.0)).z); + float delta = sceneVZ - marchVZ; + float thick = thickness * (1.0 + along * 0.012); + if (delta > thick || delta < -thick * 2.0 || perp > thick) + { + if (delta < -thick * 2.0) + break; + prevUV = uv; + tPrev = t; + continue; + } + + if (length(uv - startUV) < minUvSep) + { + prevUV = uv; + tPrev = t; + continue; + } + + float2 travel = uv - startUV; + float travelLen = length(travel); + if (travelLen < minUvSep || dot(travel / travelLen, dirUV) < 0.2) + { + prevUV = uv; + tPrev = t; + continue; + } + + float distA = tPrev; + float distB = t; + [loop] + for (int r = 0; r < refineCount; ++r) + { + float midT = 0.5 * (distA + distB); + float2 uvM = 0.0; + float wM = 1.0; + WorldToUv(origin + R * midT, uvM, wM); + float dM = 0.0; + float sVZ = SampleSceneViewZ(uvM, dM); + float mVZ = abs(mul(m_V, float4(origin + R * midT, 1.0)).z); + if ((sVZ - mVZ) <= thickness) + distB = midT; + else + distA = midT; + } + + hitT = 0.5 * (distA + distB); + float wH = 1.0; + WorldToUv(origin + R * hitT, hitUV, wH); + if (any(hitUV < 0.0) || any(hitUV > 1.0) || length(hitUV - startUV) < minUvSep) + break; + + float dHit = 0.0; + float sHit = SampleSceneViewZ(hitUV, dHit); + if (IsSkyDepth(dHit)) + break; + float3 hitPos = ReconstructWorld(hitUV, dHit); + if (hitPos.y < position.y - 0.2) + break; + + float alongH = dot(hitPos - origin, R); + float perpH = length(hitPos - (origin + R * alongH)); + float mHit = abs(mul(m_V, float4(origin + R * hitT, 1.0)).z); + float err = abs(sHit - mHit); + hitConf = saturate(1.0 - err / max(thickness * 2.0, 1e-3)); + hitConf *= saturate(1.0 - perpH / max(thickness * 2.5, 1e-3)); + hitConf *= RayAttenBorder(hitUV, SSR_EDGE_ATTENUATION); + hitConf *= saturate((alongH - tMin) * 0.5); + hit = hitConf > 0.08; + break; + } + + if (!hit) + return float4(skybox, 0.0); + + float3 img = g_SceneColor.SampleLevel(smp_nofilter, hitUV, 0).xyz; + float hitLum = dot(max(img, 0.0), float3(0.2126, 0.7152, 0.0722)); + if (hitLum < 1e-5) + return float4(skybox, 0.0); + hitConf *= saturate(hitLum * 2.5 + 0.45); + return float4(img, saturate(hitConf * 1.25)); +} + float4 compute_ssr_near( float3 position, float3 normal, @@ -384,7 +554,7 @@ float4 compute_ssr_near( float sceneVZ = SampleSceneViewZ(uv, rawDepth); float marchVZ = abs(mul(m_V, float4(marchPos, 1.0)).z); - if (rawDepth >= 0.9995) + if (IsSkyDepth(rawDepth)) { if (t > tMin * 1.5) { @@ -399,7 +569,7 @@ float4 compute_ssr_near( } float delta = sceneVZ - marchVZ; - if (delta <= thickness && delta >= -thickness * 4.0) + if (delta <= thickness && delta >= -thickness) { if (length(uv - startUV) < 0.0025) { @@ -436,10 +606,13 @@ float4 compute_ssr_near( float mHit = abs(mul(m_V, float4(origin + R * (0.5 * (distA + distB)), 1.0)).z); float err = abs(sHit - mHit); hitConf = saturate(1.0 - err / max(thickness * 2.0, 1e-3)); + float3 hitPos = ReconstructWorld(hitUV, dHit); + float toward = dot(normalize(hitPos - origin), R); + hitConf *= saturate(toward * 2.0); hit = hitConf > 0.03; break; } - else if (delta < -thickness * 4.0) + else if (delta < -thickness) { if (tPrev > tMin * 1.2 && all(prevUV >= 0.0) && all(prevUV <= 1.0)) { diff --git a/res/gamedata/shaders/r5/skinned_mdi_common.h b/res/gamedata/shaders/r5/skinned_mdi_common.h index 84d8bed2f5e..769f293261a 100644 --- a/res/gamedata/shaders/r5/skinned_mdi_common.h +++ b/res/gamedata/shaders/r5/skinned_mdi_common.h @@ -10,6 +10,11 @@ struct SkinnedDrawRecord uint splatOffset; uint splatCount; uint pad; + float4x4 prevWorld; + uint prevBoneOffset; + uint prevValid; + uint pad1; + uint pad2; }; StructuredBuffer g_SkinnedRecords : register(t14); diff --git a/res/gamedata/shaders/r5/ssgi_apply.ps b/res/gamedata/shaders/r5/ssgi_apply.ps index 003cbf2c335a45f1d547441aed9c160dd8c86313..78efb21cb7eeedf208e2a9584dd62e53291247cf 100644 GIT binary patch delta 51 zcmX@k^M+@F313oTacXjYPJU5*L1J?DMtM0V5zpe_>`Ir^f|3l4qQr7ALvyn~(+_3< D?{X5+ delta 38 ucmaFEbDU>_$;NO_CZVFla+lPCk_-hqTLl9>OG``B$rqXQH!o&7$qWGfF$|^v diff --git a/res/gamedata/shaders/r5/ssgi_blur.ps b/res/gamedata/shaders/r5/ssgi_blur.ps index ea7bf503d18877b2860264495b0fba435816be9d..3bdda8954efe67c05efbaeb4cc91b59a2e38afea 100644 GIT binary patch delta 32 ncmX@Wd5Ckvd`4l<;^6E`m(+ri42|T}ypq(SD9z0q7zLOC$c+nw delta 30 lcmX@ad4O}nd`7|K)Vz|^q9_GBTLl9>OG``B&6^knm;s^R2}1w? diff --git a/res/gamedata/shaders/r5/ssgi_temporal.ps b/res/gamedata/shaders/r5/ssgi_temporal.ps index 0ffcc3eeb6119d1fa37b99582f6992bf26a044a3..9f12a016bca294ba781934dda4ee52a4f2c4cdc0 100644 GIT binary patch delta 30 jcmX@WbBJd{FO#5Wad38}OKL$$hDHjA)Z9FWX$dm`ur>=Z delta 28 jcmX@abAV? SiGrjxC(jhn-(1Ka#{>X|kQ|Z# delta 68 zcmeySd|r8i$;R;ATp~q@h0JvninA-j6N@u6brf<^iwZOh^h}JvGC(pnwIneoJhemA6^DP~?jY z^x>modY@zOH0eibC}dN|6}?j2mpS+3331c diff --git a/res/gamedata/shaders/r5/ssr_resolve.ps b/res/gamedata/shaders/r5/ssr_resolve.ps index ea082df6e3b43d4d8c3306d4fc7780752b35be9e..a37ed28641ee89b6175f68c1915e16b4b45f657b 100644 GIT binary patch delta 89 zcmez5INxc*BsLMx;^6E`m(+ri42`11axg=4a-5L<=F4p5+>;*(h-#(hYc4JYAjnKp&?qh~N>i{=FwiqF+#Dw~g%1FY7aVc` delta 61 zcmbR5^vQ93I=+XmX@ZQ|FBQu1^|$52`vBs diff --git a/res/gamedata/shaders/r5/sssss_blur.ps b/res/gamedata/shaders/r5/sssss_blur.ps index 8c51f97f076618d74af41fb115351b8405d8a55e..219b5bf6e31e0fcbf63ecbf0d712173bd25a4d01 100644 GIT binary patch delta 20 ccmbOyJV$uL7G`!ETLr^ZUGvR*nIEzO07iKRy8r+H delta 26 icmbOuJWqJT7G_R6TLl9>OG``B$u`XTn_n?+Vg&$e!w3QZ diff --git a/res/gamedata/shaders/r5/sun_forward.ps b/res/gamedata/shaders/r5/sun_forward.ps index e6e080ca221c075e43ac6a13a48efabacebae045..fc2e1c74f4a6c927bbadbd27606152089ffceddd 100644 GIT binary patch delta 43 xcmey$x{YPSbH>REOa}4_Kv0xgQd*R!P+Xc9l3JmlrQoTToS&0lWX)B}1pq;-4deg- delta 231 zcmdnS@|AVNb4C+=eTCq}+=84`h2qjYg_6{YlG3771&z|;(!`vcN`;KXVudsyD?cqw zAt^CCHANvaPjm7kM$>vO1t0+Gb}T3Wsw+z^0_s&r&dc(vuWyL5f3CE5IBr1y4P=6tc=hm^!rzy+j3svP5exuz5wP MK=sjsO71Y!e6g`VQN>hS?ruYI)&?wI@%1H^x zFE-ID*POhUHEy#H+fzo);L^O{jKs8(;>p$=$#Nj`Yq`K?rRC%&mKZDKWTt17gcK!~ zrRIc9*5XtU%gZmyP0Y!xO4aa*FD}iCPsuEb2O1d2Da(VRwv@ASvM!fAmyUvgo`I${ HS1lI+*f&bw delta 707 zcmbV~!AiqG5QZsQDiJ}&lV0W^nASE#YY%N{z4YioDAb!_n{Mf1k`0@!F-qyhqk@oE z@CC#N@J&1mzJ#+$THAVa$tA=8&wlgIKJR?)z4YH6U)QF9$L3xjq0hMkEa=`u&;orx zeK~@53rwS4uh)uAO;ggL!-=dyF!nl3T%pk8!X*y7r}~A3RcpY66{31Ue9LH7h zA)x@tLm~#LxM2-cKR3C7u&-#_2lFeCMFdqpTpu;719qM802uk4dP@lKM!A=dc z828uP@yGhLp>9-X)^;hhKbY_P`LCSTdG)7Dthg9Ju`tL&2NCkmS9IE0Tk)gDF jB8VFpA++(4nQ*>ZZYaKHY;7FwV#bLV-won5wTbov1U}St diff --git a/res/gamedata/shaders/r5/volumetric/sunshafts_combine.ps b/res/gamedata/shaders/r5/volumetric/sunshafts_combine.ps index 73752c0222622da608e4b09a87204b72474d37be..156a0ec2c5c68d4b55e35bbd389b84887706a9e2 100644 GIT binary patch delta 106 zcmeBRzsRPfoRXTBnU|^%>>F>FGOnNyNlq+q2` zl$xGd3}k8)8)#Z%s>}hJlUSsnfTGe6sFEu=sWdGOXhd*nUT{WYT1l~Uer^&_wX@Sk kJ0r%)@=RirI~a|Cw8G>&jE+D$5zJo11Y)aC=3#aP0Q>Ve%>V!Z diff --git a/src/Layers/xrRender/Backend/VulkanBackend.cpp b/src/Layers/xrRender/Backend/VulkanBackend.cpp index 26668022056..724265e9884 100644 --- a/src/Layers/xrRender/Backend/VulkanBackend.cpp +++ b/src/Layers/xrRender/Backend/VulkanBackend.cpp @@ -362,6 +362,14 @@ bool VulkanBackend::SelectPhysicalDevice() { m_capabilities.id_vendor = props.vendorID; m_capabilities.id_device = props.deviceID; Msg("* [VulkanBackend] Using GPU: %s", props.deviceName); + VkPhysicalDeviceMemoryProperties memProps{}; + vkGetPhysicalDeviceMemoryProperties(dev, &memProps); + for (u32 hi = 0; hi < memProps.memoryHeapCount; ++hi) + { + if (memProps.memoryHeaps[hi].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) + Msg("* [VulkanBackend] Device-local heap[%u]: %llu MB", + hi, (unsigned long long)(memProps.memoryHeaps[hi].size / (1024ull * 1024ull))); + } return true; } } @@ -372,6 +380,14 @@ bool VulkanBackend::SelectPhysicalDevice() { m_capabilities.id_vendor = props.vendorID; m_capabilities.id_device = props.deviceID; Msg("* [VulkanBackend] Using GPU (fallback): %s", props.deviceName); + VkPhysicalDeviceMemoryProperties memProps{}; + vkGetPhysicalDeviceMemoryProperties(m_physicalDevice, &memProps); + for (u32 hi = 0; hi < memProps.memoryHeapCount; ++hi) + { + if (memProps.memoryHeaps[hi].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) + Msg("* [VulkanBackend] Device-local heap[%u]: %llu MB", + hi, (unsigned long long)(memProps.memoryHeaps[hi].size / (1024ull * 1024ull))); + } return true; } @@ -794,7 +810,8 @@ bool VulkanBackend::CreateSwapChain(u32 width, u32 height) { swapchainInfo.imageColorSpace = colorSpace; swapchainInfo.imageExtent = extent; swapchainInfo.imageArrayLayers = 1; - swapchainInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + swapchainInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | + (surfaceCaps.supportedUsageFlags & (VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT)); swapchainInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; swapchainInfo.preTransform = surfaceCaps.currentTransform; swapchainInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; @@ -854,6 +871,7 @@ void VulkanBackend::CreateBackBufferTextures() { desc.height = m_backBufferHeight; desc.format = nvFormat; desc.isRenderTarget = true; + desc.isShaderResource = true; desc.debugName = "BackBuffer"; desc.keepInitialState = true; desc.initialState = nvrhi::ResourceStates::Present; @@ -1295,67 +1313,91 @@ void VulkanBackend::PresentInternal(bool outOfBand) bool VulkanBackend::PresentFrameGeneration(nvrhi::ITexture* interpolated, nvrhi::ITexture* real) { - (void)real; if (!interpolated || m_asyncSubmit) return false; ZoneScopedN("VulkanBackend::PresentFrameGeneration"); - const u32 prevFrame = (m_currentFrameIndex + BACK_BUFFER_COUNT - 1) % BACK_BUFFER_COUNT; - { - ZoneScopedN("VK::FG_WaitPrevPresentFence"); - vkWaitForFences(m_device, 1, &m_inFlightFence[prevFrame], VK_TRUE, UINT64_MAX); - } + nvrhi::ITexture* proto = m_backBuffers[0].Get(); + if (!proto || proto->getDesc().format != interpolated->getDesc().format || + proto->getDesc().width != interpolated->getDesc().width || + proto->getDesc().height != interpolated->getDesc().height) + return false; + + auto* cl = m_commandLists[1].Get(); + if (!cl) + return false; + + const u32 realSlot = m_currentFrameIndex; + const u32 realImage = m_currentImageIndex; + const u32 interpSlot = (realSlot + 1) % BACK_BUFFER_COUNT; + nvrhi::ITexture* realBb = + (realImage < BACK_BUFFER_COUNT) ? m_backBuffers[realImage].Get() : nullptr; { - ZoneScopedN("VK::FG_WaitInFlightFence"); - vkWaitForFences(m_device, 1, &m_inFlightFence[m_currentFrameIndex], VK_TRUE, UINT64_MAX); - vkResetFences(m_device, 1, &m_inFlightFence[m_currentFrameIndex]); + ZoneScopedN("VK::FG_WaitInterpSlotFence"); + vkWaitForFences(m_device, 1, &m_inFlightFence[interpSlot], VK_TRUE, UINT64_MAX); + vkResetFences(m_device, 1, &m_inFlightFence[interpSlot]); } + auto reSignalInterpFence = [&]() { + VkSubmitInfo fenceSubmit = {}; + fenceSubmit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + std::lock_guard qk(m_queueMutex); + vkQueueSubmit(m_graphicsQueue, 1, &fenceSubmit, m_inFlightFence[interpSlot]); + }; + { std::lock_guard sc(m_swapchainMutex); VkResult result = vkAcquireNextImageKHR( m_device, m_swapchain, UINT64_MAX, - m_imageAvailable[m_currentFrameIndex], VK_NULL_HANDLE, + m_imageAvailable[interpSlot], VK_NULL_HANDLE, &m_currentImageIndex); if (result == VK_ERROR_OUT_OF_DATE_KHR) { RequestSwapchainRecreate(true); + m_currentImageIndex = realImage; + reSignalInterpFence(); return false; } if (result == VK_SUBOPTIMAL_KHR) RequestSwapchainRecreate(false); } - nvrhi::ITexture* dst = GetBackBuffer(); - if (!dst || dst->getDesc().format != interpolated->getDesc().format || - dst->getDesc().width != interpolated->getDesc().width || - dst->getDesc().height != interpolated->getDesc().height) - return false; - - auto* cl = m_commandLists[1].Get(); - if (!cl) + nvrhi::ITexture* interpDst = GetBackBuffer(); + if (!interpDst) + { + m_currentImageIndex = realImage; + reSignalInterpFence(); return false; + } { auto* vkDevice = static_cast(m_nvrhiVulkanDevice.Get()); std::lock_guard qk(m_queueMutex); vkDevice->queueWaitForSemaphore( nvrhi::CommandQueue::Graphics, - m_imageAvailable[m_currentFrameIndex], 0); + m_imageAvailable[interpSlot], 0); vkDevice->queueSignalSemaphore( nvrhi::CommandQueue::Graphics, - m_renderFinished[m_currentFrameIndex], 0); + m_renderFinished[interpSlot], 0); cl->open(); nvrhi::TextureSlice slice; - cl->copyTexture(dst, slice, interpolated, slice); + cl->copyTexture(interpDst, slice, interpolated, slice); + if (real && realBb && real != realBb) + cl->copyTexture(realBb, slice, real, slice); cl->close(); m_nvrhiDevice->executeCommandList(cl); } + m_currentFrameIndex = interpSlot; PresentInternal(true); + + m_currentFrameIndex = realSlot; + m_currentImageIndex = realImage; + PresentInternal(false); + return true; } diff --git a/src/Layers/xrRender/Backend/VulkanBackend.h b/src/Layers/xrRender/Backend/VulkanBackend.h index 5c607bbb3e6..fcbeaec6052 100644 --- a/src/Layers/xrRender/Backend/VulkanBackend.h +++ b/src/Layers/xrRender/Backend/VulkanBackend.h @@ -50,6 +50,7 @@ class VulkanBackend : public IRenderBackend { std::pair GetBackBufferSize() const override { return {m_backBufferWidth, m_backBufferHeight}; } void Present(bool vsync) override; bool PresentFrameGeneration(nvrhi::ITexture* interpolated, nvrhi::ITexture* real) override; + u64 GetPresentCount() const override { return m_nextPresentId - 1; } void ResizeSwapChain(u32 width, u32 height) override; bool IsLowLatencyAvailable() const override { return m_latencyAvailable; } diff --git a/src/Layers/xrRender/Bindless/BindlessTypes.h b/src/Layers/xrRender/Bindless/BindlessTypes.h index e366034a33b..b0348c99e29 100644 --- a/src/Layers/xrRender/Bindless/BindlessTypes.h +++ b/src/Layers/xrRender/Bindless/BindlessTypes.h @@ -69,7 +69,7 @@ struct alignas(16) MaterialData { u32 sssMapIndex; // SSS thickness/strength map (R/G/B = thick/str/profile) float emissiveIntensity; - u32 _pad1; + u32 lmapIndex; // Baked hemi lightmap (lmap#N_2: a=hemi, g=sun) u32 _pad2; }; static_assert(sizeof(MaterialData) == 64, "MaterialData must be 64 bytes for GPU alignment"); diff --git a/src/Layers/xrRender/ClusteredLightManager.cpp b/src/Layers/xrRender/ClusteredLightManager.cpp index 6addfb5bf46..4451c67d09e 100644 --- a/src/Layers/xrRender/ClusteredLightManager.cpp +++ b/src/Layers/xrRender/ClusteredLightManager.cpp @@ -438,6 +438,20 @@ void ClusteredLightManager::CollectLightsParallel(const xr_vector& } } +void ClusteredLightManager::ClearLocalShadowAssignments() +{ + m_localShadowTiles.clear(); + m_localShadowCandidates = 0; + m_localShadowDropped = 0; + m_localShadowRedraw = 0; + m_shadowDataCPU.clear(); + for (u32 i = 0; i < m_numLights; ++i) + { + m_lightsCPU[i].localShadowRect.set(0.f, 0.f, 0.f, 0.f); + m_lightsCPU[i].spotParamsAndType.w = 0.f; + } +} + void ClusteredLightManager::AssignLocalShadowTiles(const Fvector& cameraPos) { ZoneScopedN("LocalShadow::AssignTiles"); @@ -450,6 +464,7 @@ void ClusteredLightManager::AssignLocalShadowTiles(const Fvector& cameraPos) if (ps_r_local_shadows == 0 || m_numLights == 0 || m_lightSources.size() != m_numLights) { m_localShadowSticky.clear(); + ClearLocalShadowAssignments(); return; } @@ -487,6 +502,8 @@ void ClusteredLightManager::AssignLocalShadowTiles(const Fvector& cameraPos) const u32 lt = L->flags.type; if (lt != IRender_Light::SPOT && lt != IRender_Light::OMNIPART) continue; + if (L->flags.bHudMode) + continue; L->spatial_move(); @@ -497,23 +514,18 @@ void ClusteredLightManager::AssignLocalShadowTiles(const Fvector& cameraPos) const Fvector& sp = L->spatial.sphere.P; const float sr = std::max(L->spatial.sphere.R, 0.01f); const float distCenter = cameraPos.distance_to(sp); - const bool hudLight = L->flags.bHudMode; const bool cameraInside = distCenter <= (sr * 1.01f + VIEWPORT_NEAR); const bool wasSticky = m_localShadowSticky.find(L) != m_localShadowSticky.end(); const float dist = std::max(0.f, distCenter - sr); - if (!hudLight && !cameraInside && lod <= EPS_L) + if (!cameraInside && lod <= EPS_L) continue; - if (!hudLight && !cameraInside) + if (!cameraInside) { const float dropDist = wasSticky ? 120.f : 100.f; if (dist > dropDist) continue; - } - - if (!hudLight && !cameraInside) - { const float frustumInflate = wasSticky ? 1.35f : 1.1f; if (!camFrustum.testSphere_dirty(sp, sr * frustumInflate)) continue; diff --git a/src/Layers/xrRender/ClusteredLightManager.h b/src/Layers/xrRender/ClusteredLightManager.h index 79697588fb6..89ad9652bc8 100644 --- a/src/Layers/xrRender/ClusteredLightManager.h +++ b/src/Layers/xrRender/ClusteredLightManager.h @@ -84,6 +84,7 @@ class ClusteredLightManager { void BuildLightBuffer(const light_Package& package); /// Pick top-N shadowed faces and pack tile index into GPULightData.w (call after collect, before Upload). void AssignLocalShadowTiles(const Fvector& cameraPos); + void ClearLocalShadowAssignments(); void RefreshHudSpotXForms(); void BuildDISampleTable(); void Upload(nvrhi::ICommandList* cmdList); diff --git a/src/Layers/xrRender/Denoising/IDenoiseBackend.h b/src/Layers/xrRender/Denoising/IDenoiseBackend.h index 36db7a0fdff..448b1ec33d4 100644 --- a/src/Layers/xrRender/Denoising/IDenoiseBackend.h +++ b/src/Layers/xrRender/Denoising/IDenoiseBackend.h @@ -38,7 +38,7 @@ struct DenoiseInputs float jitterPrevX = 0.f; float jitterPrevY = 0.f; float exposure = 1.f; - float nearZ = 0.2f; + float nearZ = 0.001f; float farZ = 500.f; float viewToClip[16] = {}; float viewToClipPrev[16] = {}; diff --git a/src/Layers/xrRender/Denoising/NRDDenoiseBackend.cpp b/src/Layers/xrRender/Denoising/NRDDenoiseBackend.cpp index f78fb29f380..5cf08f12e4c 100644 --- a/src/Layers/xrRender/Denoising/NRDDenoiseBackend.cpp +++ b/src/Layers/xrRender/Denoising/NRDDenoiseBackend.cpp @@ -174,7 +174,7 @@ class NRDDenoiseBackend final : public IDenoiseBackend bd.byteSize = 512; bd.isConstantBuffer = true; bd.isVolatile = true; - bd.maxVersions = 16; + bd.maxVersions = xray::render::fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; bd.debugName = "NRD_CB_v9"; bd.initialState = nvrhi::ResourceStates::ConstantBuffer; bd.keepInitialState = true; diff --git a/src/Layers/xrRender/FGDetailManager.cpp b/src/Layers/xrRender/FGDetailManager.cpp index e3aefa6813a..0de623350b8 100644 --- a/src/Layers/xrRender/FGDetailManager.cpp +++ b/src/Layers/xrRender/FGDetailManager.cpp @@ -2239,7 +2239,7 @@ bool FGDetailManager::CreateGraphicsPipeline(fg::RenderDevice* renderDevice, con } decalPipeDesc.renderState.depthStencilState.depthTestEnable = true; decalPipeDesc.renderState.depthStencilState.depthWriteEnable = true; - decalPipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + decalPipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; decalPipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; decalPipeDesc.renderState.rasterState.depthBias = -8; decalPipeDesc.renderState.rasterState.slopeScaledDepthBias = -1.f; diff --git a/src/Layers/xrRender/FGRenderBase.cpp b/src/Layers/xrRender/FGRenderBase.cpp index 64401fb7821..343f6313193 100644 --- a/src/Layers/xrRender/FGRenderBase.cpp +++ b/src/Layers/xrRender/FGRenderBase.cpp @@ -189,10 +189,9 @@ void FGRenderBase::End() const bool vsync = psDeviceFlags.test(rsVSync); nvrhi::ITexture* fgInterp = nullptr; nvrhi::ITexture* fgReal = nullptr; - if (Streamline_TakeFgPresent(fgInterp, fgReal)) + if (Streamline_TakeFgPresent(fgInterp, fgReal) && + GEnv.Backend->PresentFrameGeneration(fgInterp, fgReal)) { - GEnv.Backend->Present(vsync); - GEnv.Backend->PresentFrameGeneration(fgInterp, fgReal); return; } GEnv.Backend->Present(vsync); @@ -265,67 +264,9 @@ void FGRenderBase::ConvertLegacyAssetsToPBR() void FGRenderBase::RenderPBRConversionUI() { - RenderConversionProgressUI(); } void FGRenderBase::ConvertLegacyAssetsToPBRImpl() { - TextureScanConfig scanConfig; - scanConfig.texture_roots = {"$game_textures$"}; - scanConfig.recursive = true; - scanConfig.include_levels = true; - - TextureInventory inventory = BuildTextureInventory(scanConfig); - Msg("~ [PBR] Found %d legacy texture sets", static_cast(inventory.assets.size())); - - PBRConversionParams params; - params.generate_mipmaps = true; - params.default_metallic = 0.0f; - params.default_roughness = 0.5f; - params.default_ao = 1.0f; - - bool needsConversion = !VerifyPBROutputs(inventory, params); - - if (needsConversion) - { - Msg("~ [PBR] Starting texture conversion (outputs missing)..."); - ConversionProgress::Get().BeginJob(); - - PBRConversionStats stats; - bool success = ConvertTexturesToPBR(inventory, params, stats, nullptr); - - if (success) - { - Msg("~ [PBR] Conversion complete: %d converted, %d skipped, %d failed", - stats.textures_converted, stats.textures_skipped, stats.textures_failed); - } - else - { - Msg("! [PBR] Conversion failed!"); - } - } - else - { - Msg("~ [PBR] Textures already converted, skipping conversion."); - } - - Msg("~ [PBR] Checking for PBR texture consolidation..."); - ConsolidationStats consolidationStats; - if (ConsolidatePBRTextures("$game_textures$", consolidationStats, nullptr)) - { - if (consolidationStats.textures_consolidated > 0) - { - Msg("~ [PBR] Consolidation complete: %d packed, %d files deleted", - consolidationStats.textures_consolidated, consolidationStats.files_deleted); - } - else - { - Msg("~ [PBR] No textures needed consolidation."); - } - } - else - { - Msg("! [PBR] Consolidation had failures: %d failed", consolidationStats.textures_failed); - } } } diff --git a/src/Layers/xrRender/FrameGraph/FrameGraph.cpp b/src/Layers/xrRender/FrameGraph/FrameGraph.cpp index 0eef7d462cf..c5c97771ed0 100644 --- a/src/Layers/xrRender/FrameGraph/FrameGraph.cpp +++ b/src/Layers/xrRender/FrameGraph/FrameGraph.cpp @@ -993,15 +993,16 @@ void FrameGraph::AllocateResources() { resources::TextureHandle rmHandle = m_resourcePool->AllocateTexture(rmTexDesc); if (rmHandle.IsValid()) { - // Store ResourceManager handle for lifecycle management resource.resourceTexture = rmHandle; - // Get the NVRHI texture for immediate use resources::TextureManager* texManager = m_resourceManager->GetTextureManager(); resource.nvrhiTexture = texManager->GetNVRHITexture(rmHandle); - resource.isAllocated = (resource.nvrhiTexture != nullptr); - totalMemoryAllocated += resource.memorySize; - continue; + if (resource.nvrhiTexture) { + resource.isAllocated = true; + totalMemoryAllocated += resource.memorySize; + continue; + } + resource.resourceTexture = {}; } } diff --git a/src/Layers/xrRender/FrameGraph/PassResourceCache.cpp b/src/Layers/xrRender/FrameGraph/PassResourceCache.cpp index 58407efda1b..87146447a81 100644 --- a/src/Layers/xrRender/FrameGraph/PassResourceCache.cpp +++ b/src/Layers/xrRender/FrameGraph/PassResourceCache.cpp @@ -129,18 +129,34 @@ nvrhi::ISampler* PassResourceCache::GetShadowCmpSampler(nvrhi::IDevice* device) } nvrhi::ITexture* PassResourceCache::GetDummyShadowMap(nvrhi::IDevice* device) { - if (!m_dummyShadowMap) { + if (!m_dummyShadowMap && device) { nvrhi::TextureDesc desc; desc.width = 1; desc.height = 1; - desc.arraySize = 4; + desc.arraySize = 16; desc.format = nvrhi::Format::D32; desc.debugName = "DummyShadowMap"; desc.initialState = nvrhi::ResourceStates::ShaderResource; desc.keepInitialState = true; desc.dimension = nvrhi::TextureDimension::Texture2DArray; desc.isShaderResource = true; + desc.isRenderTarget = true; + desc.useClearValue = true; + desc.clearValue = nvrhi::Color(1.0f); m_dummyShadowMap = device->createTexture(desc); + if (m_dummyShadowMap) { + nvrhi::CommandListHandle cmd = device->createCommandList(); + if (cmd) { + cmd->open(); + for (u32 layer = 0; layer < desc.arraySize; ++layer) + cmd->clearDepthStencilTexture( + m_dummyShadowMap, + nvrhi::TextureSubresourceSet(0, 1, layer, 1), + true, 1.0f, false, 0); + cmd->close(); + device->executeCommandList(cmd); + } + } } return m_dummyShadowMap; } @@ -177,8 +193,9 @@ nvrhi::ITexture* PassResourceCache::GetDummyLocalShadowESM(nvrhi::IDevice* devic if (m_dummyLocalShadowESM) { nvrhi::CommandListHandle cmd = device->createCommandList(); cmd->open(); - cmd->clearTextureFloat( - m_dummyLocalShadowESM, nvrhi::AllSubresources, nvrhi::Color(1.f)); + uint16_t one = 0x3C00; + for (u32 layer = 0; layer < 4; ++layer) + cmd->writeTexture(m_dummyLocalShadowESM, layer, 0, &one, sizeof(one)); cmd->close(); device->executeCommandList(cmd); } @@ -236,6 +253,16 @@ nvrhi::ITexture* PassResourceCache::GetDummyContactHistory(nvrhi::IDevice* devic return m_dummyContactHistory; } +void PassResourceCache::EnsureShadowBindDummies(nvrhi::IDevice* device) { + if (!device) + return; + GetDummyShadowMap(device); + GetDummyShadowMap2D(device); + GetDummyLocalShadowESM(device); + GetDummyContactDepth(device); + GetDummyContactHistory(device); +} + nvrhi::ITexture* PassResourceCache::GetDummyCubeMap(nvrhi::IDevice* device) { if (!m_dummyCubeMap) { nvrhi::TextureDesc desc; @@ -540,8 +567,14 @@ nvrhi::IBuffer* PassResourceCache::GetOrCreateVolatileCB( u64 key = HashCombine(HashString(passName), HashString(bufferName)); auto it = m_volatileCBs.find(key); if (it != m_volatileCBs.end()) { - m_stats.bufferHits++; - return device->GetNativeBuffer(it->second); + nvrhi::IBuffer* existing = device->GetNativeBuffer(it->second); + if (existing && existing->getDesc().maxVersions >= maxVersions && + existing->getDesc().byteSize >= byteSize) + { + m_stats.bufferHits++; + return existing; + } + m_volatileCBs.erase(it); } m_stats.bufferMisses++; diff --git a/src/Layers/xrRender/FrameGraph/PassResourceCache.h b/src/Layers/xrRender/FrameGraph/PassResourceCache.h index 49a2674bbd3..789e8c2a831 100644 --- a/src/Layers/xrRender/FrameGraph/PassResourceCache.h +++ b/src/Layers/xrRender/FrameGraph/PassResourceCache.h @@ -56,6 +56,7 @@ class PassResourceCache { nvrhi::ITexture* GetDummyContactDepth(nvrhi::IDevice* device); /// 1x1 white (1.0 = lit) for g_ContactHistory @ t28 nvrhi::ITexture* GetDummyContactHistory(nvrhi::IDevice* device); + void EnsureShadowBindDummies(nvrhi::IDevice* device); nvrhi::ITexture* GetDummyCubeMap(nvrhi::IDevice* device); nvrhi::ITexture* GetDummyCubeArray(nvrhi::IDevice* device); nvrhi::IBuffer* GetDummyStructuredBuffer(nvrhi::IDevice* device); diff --git a/src/Layers/xrRender/FrameGraphPasses/AmbientOcclusionPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/AmbientOcclusionPassSetup.cpp index b8d9207ff7f..bf655518b88 100644 --- a/src/Layers/xrRender/FrameGraphPasses/AmbientOcclusionPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/AmbientOcclusionPassSetup.cpp @@ -182,9 +182,8 @@ static AOConstantsGPU MakeAOConstants(u32 width, u32 height, u32 quality) const float strength = 1.0f; // apply uses fixed ambient weight; modes bake their own contrast c.ao_params.set(radius, bias, strength, q); - // IX-Ray gtao_parameters = focal * 0.5; SSAO/HBAO use full focal via WorldRadiusToUV when needed const float focal = h / (2.0f * std::tan(Device.fFOV * 0.5f * (3.14159265f / 180.0f))); - c.ao_proj.set(focal * 0.5f, static_cast(Device.dwFrame), 0.f, 0.f); + c.ao_proj.set(focal * 0.5f, 0.f, 0.f, 0.f); return c; } diff --git a/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.cpp index c6dd38b24fe..1fdbeebaab4 100644 --- a/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/BloomPassSetup.cpp @@ -319,7 +319,7 @@ VirtualResourceHandle setupBloomPass( cmd->setTextureState(out, nvrhi::AllSubresources, nvrhi::ResourceStates::RenderTarget); cmd->setTextureState(bloomFull, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); - P4 p{intensity, 0.9995f, 0, 0}; + P4 p{intensity, 1e-7f, 0, 0}; cmd->writeBuffer(cb, &p, sizeof(p)); BindingSetBuilder bsb(*vsR, *cpR, nv, "Bloom.Comp"); bsb.ConstantBuffer("BloomParams", cb); diff --git a/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.cpp index 4008304d997..172b27dfb29 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.cpp @@ -67,7 +67,7 @@ void Init(nvrhi::IDevice* nv, ContactShadowsPassState& st) auto& cache = GetPassResourceCache(); st.marchLayout = cache.GetOrCreateBindingLayoutFromReflection( - "ContactMarch_v2", *vs.reflection, *psMarch.reflection, nv); + "ContactMarch_v6", *vs.reflection, *psMarch.reflection, nv); if (st.marchLayout) { nvrhi::GraphicsPipelineDesc desc; @@ -84,7 +84,7 @@ void Init(nvrhi::IDevice* nv, ContactShadowsPassState& st) desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); nvrhi::FramebufferInfoEx fb; fb.addColorFormat(nvrhi::Format::RGBA16_FLOAT); - st.marchPipeline = cache.GetOrCreatePipeline("ContactMarch_v2", desc, fb, nv); + st.marchPipeline = cache.GetOrCreatePipeline("ContactMarch_v6", desc, fb, nv); } st.initialized = true; } @@ -102,13 +102,14 @@ VirtualResourceHandle setupContactShadowsPass( fg::RenderDevice* device, VirtualResourceHandle sceneColor, VirtualResourceHandle depth, + VirtualResourceHandle worldPos, VirtualResourceHandle motionVectors, u32 width, u32 height, bool hasPrevFrame, ContactShadowsPassState& state) { - if (!ps_r_contact_shadows || !device || !device->GetNVRHIDevice()) + if (!ps_r_contact_shadows || !device || !device->GetNVRHIDevice() || !worldPos.is_valid()) return sceneColor; Init(device->GetNVRHIDevice(), state); @@ -145,7 +146,7 @@ VirtualResourceHandle setupContactShadowsPass( struct PassData { - VirtualResourceHandle depth, motion, hist, march; + VirtualResourceHandle depth, worldPos, motion, hist, march; ContactShadowsPassState* st = nullptr; fg::RenderDevice* device = nullptr; u32 width = 0, height = 0, writeIdx = 0; @@ -163,6 +164,7 @@ VirtualResourceHandle setupContactShadowsPass( data.writeIdx = writeIdx; data.useTemporal = useTemporal; data.depth = pb.read(depth, ResourceState::ShaderResource); + data.worldPos = pb.read(worldPos, ResourceState::ShaderResource); data.hist = pb.read(histRead, ResourceState::ShaderResource); if (useTemporal) data.motion = pb.read(motionVectors, ResourceState::ShaderResource); @@ -173,9 +175,10 @@ VirtualResourceHandle setupContactShadowsPass( auto* cmd = ctx->GetCommandList(); auto* nv = cmd ? cmd->getDevice() : nullptr; auto* depthTex = graph.GetPhysicalTexture(data.depth); + auto* worldPosTex = graph.GetPhysicalTexture(data.worldPos); auto* hist = graph.GetPhysicalTexture(data.hist); auto* march = graph.GetPhysicalTexture(data.march); - if (!cmd || !nv || !depthTex || !hist || !march || !data.st) + if (!cmd || !nv || !depthTex || !worldPosTex || !hist || !march || !data.st) return; nvrhi::ITexture* motionTex = nullptr; @@ -228,6 +231,7 @@ VirtualResourceHandle setupContactShadowsPass( if (marchCB) bsb.ConstantBuffer("ContactParams", marchCB); bsb.Texture("g_Depth", depthTex) + .Texture("g_WorldPos", worldPosTex) .Texture("g_Motion", motionTex) .Texture("g_History", hist); auto set = cache.GetOrCreateBindingSet(bsb.Build(), data.st->marchLayout, nv); diff --git a/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.h index 0ccbe022177..867e96df22a 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/ContactShadowsPassSetup.h @@ -30,6 +30,7 @@ framegraph::VirtualResourceHandle setupContactShadowsPass( fg::RenderDevice* device, framegraph::VirtualResourceHandle sceneColor, framegraph::VirtualResourceHandle depth, + framegraph::VirtualResourceHandle worldPos, framegraph::VirtualResourceHandle motionVectors, u32 width, u32 height, diff --git a/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.cpp index 2d0aff4968a..3d8762f365f 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DecalPassSetup.cpp @@ -57,8 +57,8 @@ static void InitializeDecalResources(fg::RenderDevice* device, const nvrhi::Fram posAttr.elementStride = sizeof(Fvector); state.inputLayout = nvDevice->createInputLayout(&posAttr, 1, state.vs); - state.bindingLayout = cache.GetOrCreateBindingLayoutFromReflection( - "Decal_v2", *vsResult.reflection, *psResult.reflection, nvDevice); + state.bindingLayout = cache.GetOrCreateBindingLayoutFromReflection( + "Decal_v3", *vsResult.reflection, *psResult.reflection, nvDevice); auto makePipe = [&](const char* name, bool multiply) -> nvrhi::GraphicsPipelineHandle { nvrhi::GraphicsPipelineDesc pipeDesc; @@ -98,8 +98,8 @@ static void InitializeDecalResources(fg::RenderDevice* device, const nvrhi::Fram return cache.GetOrCreatePipeline(name, pipeDesc, fbInfo, nvDevice); }; - state.multiplyPipeline = makePipe("DecalMult_v2", true); - state.alphaPipeline = makePipe("DecalAlpha_v2", false); + state.multiplyPipeline = makePipe("DecalMult_v3", true); + state.alphaPipeline = makePipe("DecalAlpha_v3", false); state.initialized = state.multiplyPipeline != nullptr && state.alphaPipeline != nullptr; } @@ -192,7 +192,7 @@ DefaultOutputLayout setupDecalPass( bsb.ConstantBuffer("static_globals", staticGlobalsCB); bsb.ConstantBuffer("DecalDrawCB", drawCB); bsb.BufferSRV("g_Decals", data.decalMgr->GetDecalBuffer()); - bsb.Texture("g_Depth", depthTex); + bsb.Texture("g_Depth", depthTex, nvrhi::Format::R32_FLOAT); bsb.Texture("g_WorldPos", worldPosTex); BindBindlessMaterialTables(bsb); diff --git a/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.cpp index 54ef454e62d..34fc13f64b0 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DepthPrepassSetup.cpp @@ -76,7 +76,7 @@ static void InitializeDepthPrepass(fg::RenderDevice* device, DepthPrepassState& pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; pipeDesc.renderState.depthStencilState.depthTestEnable = true; pipeDesc.renderState.depthStencilState.depthWriteEnable = true; - pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; pipeDesc.renderState.rasterState.frontCounterClockwise = false; pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; @@ -217,7 +217,7 @@ VirtualResourceHandle setupDepthPrepass( if (!cmdList || !nvDevice) return; - cmdList->clearDepthStencilTexture(depthRT, nvrhi::AllSubresources, true, 1.0f, false, 0); + cmdList->clearDepthStencilTexture(depthRT, nvrhi::AllSubresources, true, 0.0f, false, 0); if (data.materialCache) data.materialCache->FinalizePendingMaterials(ctx); diff --git a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp index d041e25f01c..e4603cb7ef0 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DetailPassSetup.cpp @@ -129,10 +129,8 @@ DefaultOutputLayout setupDetailPass( data.depth = passBuilder.readWrite(forwardInputs.depth, ResourceState::DepthStencilWrite); data.outputColor = passBuilder.write(forwardInputs.albedo); data.outputNormal = passBuilder.readWrite(forwardInputs.normal, ResourceState::RenderTarget); - if (forwardInputs.baseColor.is_valid()) - data.baseColor = passBuilder.readWrite(forwardInputs.baseColor, ResourceState::RenderTarget); - if (forwardInputs.worldPos.is_valid()) - data.worldPos = passBuilder.readWrite(forwardInputs.worldPos, ResourceState::RenderTarget); + data.baseColor = passBuilder.readWrite(forwardInputs.baseColor, ResourceState::RenderTarget); + data.worldPos = passBuilder.readWrite(forwardInputs.worldPos, ResourceState::RenderTarget); if (shadowMapHandle.is_valid()) data.shadowMap = passBuilder.read(shadowMapHandle, ResourceState::ShaderResource); if (perlinReady.is_valid()) @@ -184,15 +182,14 @@ DefaultOutputLayout setupDetailPass( nvrhi::ITexture* normalTexture = fg.GetPhysicalTexture(data.outputNormal); auto* baseColorRT = data.baseColor.is_valid() ? fg.GetPhysicalTexture(data.baseColor) : nullptr; auto* worldPosRT = data.worldPos.is_valid() ? fg.GetPhysicalTexture(data.worldPos) : nullptr; + if (!normalTexture || !baseColorRT || !worldPosRT) + return; nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(colorTexture); - if (normalTexture) - fbDesc.addColorAttachment(normalTexture); - if (baseColorRT) - fbDesc.addColorAttachment(baseColorRT); - if (worldPosRT) - fbDesc.addColorAttachment(worldPosRT); + fbDesc.addColorAttachment(normalTexture); + fbDesc.addColorAttachment(baseColorRT); + fbDesc.addColorAttachment(worldPosRT); fbDesc.setDepthAttachment(depthTexture); auto framebuffer = framegraph::GetPassResourceCache().GetOrCreateFramebuffer( diff --git a/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.cpp index 3050b7accdf..bb4212bdf84 100644 --- a/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/DistortionApplyPassSetup.cpp @@ -44,7 +44,7 @@ void InitializeDistortionApplyPass(nvrhi::IDevice* device, DistortionApplyPassSt auto& cache = GetPassResourceCache(); state.bindingLayout = cache.GetOrCreateBindingLayoutFromReflection( - "DistortionApply_v8_HudDilate", *vsResult.reflection, *psResult.reflection, device); + "DistortionApply_v10", *vsResult.reflection, *psResult.reflection, device); if (state.bindingLayout) { nvrhi::GraphicsPipelineDesc pipeDesc; @@ -60,7 +60,7 @@ void InitializeDistortionApplyPass(nvrhi::IDevice* device, DistortionApplyPassSt nvrhi::FramebufferInfoEx fbInfo; fbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); - state.pipeline = cache.GetOrCreatePipeline("DistortionApply_v8_HudDilate", pipeDesc, fbInfo, device); + state.pipeline = cache.GetOrCreatePipeline("DistortionApply_v10", pipeDesc, fbInfo, device); } state.initialized = true; } diff --git a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp index ed2fb88b08e..52ee5586517 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ForwardColorPassSetup.cpp @@ -63,7 +63,7 @@ void InitializeForwardResources(fg::RenderDevice* device, const nvrhi::Framebuff auto& cache = framegraph::GetPassResourceCache(); - state.bindlessLayout = cache.GetOrCreateBindingLayoutFromReflection("ForwardColor_PBR_v4_TranspCB", *vsResult.reflection, *psResult.reflection, nvDevice); + state.bindlessLayout = cache.GetOrCreateBindingLayoutFromReflection("ForwardColor_PBR_v5_DynHemi", *vsResult.reflection, *psResult.reflection, nvDevice); u32 attrCount = 0; auto* attrs = GetUnifiedVertexAttributes(attrCount); @@ -92,23 +92,22 @@ void InitializeForwardResources(fg::RenderDevice* device, const nvrhi::Framebuff pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; pipeDesc.renderState.depthStencilState.depthTestEnable = true; pipeDesc.renderState.depthStencilState.depthWriteEnable = true; - pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; pipeDesc.renderState.rasterState.frontCounterClockwise = false; pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; - state.bindlessPipeline = cache.GetOrCreatePipeline("ForwardColor_PBR_v4_TranspCB", pipeDesc, fbInfo, nvDevice); + state.bindlessPipeline = cache.GetOrCreatePipeline("ForwardColor_PBR_v5_DynHemi", pipeDesc, fbInfo, nvDevice); if (!state.bindlessPipeline) { Msg("! [BindlessForward] Failed to create pipeline"); return; } - // After DepthPrepass: Equal + no write → Hi-Z rejects overdraw, skips redundant depth stores { nvrhi::GraphicsPipelineDesc equalDesc = pipeDesc; equalDesc.renderState.depthStencilState.depthWriteEnable = false; - equalDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::Equal; + equalDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; state.bindlessEqualPipeline = cache.GetOrCreatePipeline( - "ForwardColor_PBR_Equal_v1", equalDesc, fbInfo, nvDevice); + "ForwardColor_PBR_AfterDepth_v1", equalDesc, fbInfo, nvDevice); } QueryBindingLayoutFromPipeline(state.bindlessPipeline, state.bindlessLayout); @@ -147,7 +146,7 @@ void InitializeForwardResources(fg::RenderDevice* device, const nvrhi::Framebuff if (terrainPsResult.handle) { state.terrainPS = terrainPsResult.handle; state.terrainLayout = cache.GetOrCreateBindingLayoutFromReflection( - "ForwardColor_Terrain_PBR_v3_CSMLadder", *vsResult.reflection, *terrainPsResult.reflection, nvDevice); + "ForwardColor_Terrain_PBR_v4_NoLmapRaster", *vsResult.reflection, *terrainPsResult.reflection, nvDevice); if (state.terrainLayout) { nvrhi::GraphicsPipelineDesc terrainPipeDesc; @@ -161,17 +160,17 @@ void InitializeForwardResources(fg::RenderDevice* device, const nvrhi::Framebuff terrainPipeDesc.primType = nvrhi::PrimitiveType::TriangleList; terrainPipeDesc.renderState.depthStencilState.depthTestEnable = true; terrainPipeDesc.renderState.depthStencilState.depthWriteEnable = true; - terrainPipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + terrainPipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; terrainPipeDesc.renderState.rasterState.frontCounterClockwise = false; terrainPipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; - state.terrainPipeline = cache.GetOrCreatePipeline("ForwardColor_Terrain_PBR_v3_CSMLadder", terrainPipeDesc, fbInfo, nvDevice); + state.terrainPipeline = cache.GetOrCreatePipeline("ForwardColor_Terrain_PBR_v4_NoLmapRaster", terrainPipeDesc, fbInfo, nvDevice); if (state.terrainPipeline) { nvrhi::GraphicsPipelineDesc terrainEqual = terrainPipeDesc; terrainEqual.renderState.depthStencilState.depthWriteEnable = false; - terrainEqual.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::Equal; + terrainEqual.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; state.terrainEqualPipeline = cache.GetOrCreatePipeline( - "ForwardColor_Terrain_Equal_v2_CSMLadder", terrainEqual, fbInfo, nvDevice); + "ForwardColor_Terrain_AfterDepth_v1", terrainEqual, fbInfo, nvDevice); state.terrainInitialized = true; } } @@ -217,14 +216,14 @@ static void renderBindlessForward( // ═══════════════════════════════════════════════════════ // SETUP FRAMEBUFFER AND RENDER STATE // ═══════════════════════════════════════════════════════ + if (!normalRT || !baseColorRT || !worldPosRT) + return; + nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(colorRT); - if (normalRT) - fbDesc.addColorAttachment(normalRT); - if (baseColorRT) - fbDesc.addColorAttachment(baseColorRT); - if (worldPosRT) - fbDesc.addColorAttachment(worldPosRT); + fbDesc.addColorAttachment(normalRT); + fbDesc.addColorAttachment(baseColorRT); + fbDesc.addColorAttachment(worldPosRT); fbDesc.setDepthAttachment(depthRT); auto& cache = framegraph::GetPassResourceCache(); auto framebuffer = cache.GetOrCreateFramebuffer("ForwardColor", fbDesc, nvDevice); @@ -670,10 +669,8 @@ framegraph::DefaultOutputLayout setupForwardColorPass( data.depth = passBuilder.readWrite(depthInput, ResourceState::DepthStencilWrite); data.color = passBuilder.readWrite(colorInput, ResourceState::RenderTarget); data.normal = passBuilder.write(normalInput, ResourceState::RenderTarget); - if (baseColorInput.is_valid()) - data.baseColor = passBuilder.write(baseColorInput, ResourceState::RenderTarget); - if (worldPosInput.is_valid()) - data.worldPos = passBuilder.write(worldPosInput, ResourceState::RenderTarget); + data.baseColor = passBuilder.write(baseColorInput, ResourceState::RenderTarget); + data.worldPos = passBuilder.write(worldPosInput, ResourceState::RenderTarget); if (drawArgsInput.is_valid()) { data.drawArgsBuffer = passBuilder.read(drawArgsInput, ResourceState::IndirectArgument); @@ -713,14 +710,14 @@ framegraph::DefaultOutputLayout setupForwardColorPass( auto* baseColorRT = data.baseColor.is_valid() ? fg.GetPhysicalTexture(data.baseColor) : nullptr; auto* worldPosRT = data.worldPos.is_valid() ? fg.GetPhysicalTexture(data.worldPos) : nullptr; - if (!depthRT || !colorRT) + if (!depthRT || !colorRT || !normalRT || !baseColorRT || !worldPosRT) return; nvrhi::ICommandList* cmdList = ctx->GetCommandList(); if (cmdList) { // DepthPrepass already cleared+filled depth — clearing here would kill early-Z. if (!ps_r_depth_prepass) - cmdList->clearDepthStencilTexture(depthRT, nvrhi::AllSubresources, true, 1.0f, false, 0); + cmdList->clearDepthStencilTexture(depthRT, nvrhi::AllSubresources, true, 0.0f, false, 0); if (normalRT) cmdList->clearTextureFloat(normalRT, nvrhi::AllSubresources, nvrhi::Color(0.0f)); if (baseColorRT) diff --git a/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.cpp index dd1f495f976..8369402af5c 100644 --- a/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.cpp @@ -31,7 +31,7 @@ namespace xray::render::fg::passes void InitializeLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& state) { - if (!device) + if (!device || state.allocFailed) return; const u32 res = xray::render::fg::LocalShadowAtlasSize(); @@ -41,8 +41,7 @@ void InitializeLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& s if (state.initialized && state.enabled && state.resolution == res && state.sliceCount == pages && state.atlas && state.staticCacheTex && state.cascadeCB && state.cascadeCBMaxVersions >= kLocalCascadeCBVersions && - state.clearDepthPipeline && state.localSkinIndirectArgs && state.esmCB && - state.esmConvertPipeline && state.esmBlurPipeline) + state.clearDepthPipeline && state.localSkinIndirectArgs && state.esmCB) return; if (state.initialized) @@ -75,20 +74,38 @@ void InitializeLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& s for (u32 i = 0; i < MAX_LOCAL_SHADOW_PAGES; ++i) state.pageFramebuffers[i] = nullptr; - auto* resMgr = device->GetFGResourceManager(); nvrhi::IDevice* nv = device->GetNVRHIDevice(); - if (!resMgr || !resMgr->GetRTFactory() || !resMgr->GetTextureManager() || !nv) + if (!nv) { state.initialized = true; state.enabled = false; + state.allocFailed = true; return; } state.resolution = res; state.sliceCount = pages; - state.atlasHandle = resMgr->GetRTFactory()->CreateCascadedShadowMap( - res, pages, true, "rt_LocalShadowAtlas"); - state.atlas = resMgr->GetTextureManager()->GetNVRHITexture(state.atlasHandle); + { + nvrhi::TextureDesc td; + td.width = res; + td.height = res; + td.depth = 1; + td.arraySize = pages; + td.mipLevels = 1; + td.format = nvrhi::Format::D32; + td.dimension = nvrhi::TextureDimension::Texture2DArray; + td.isRenderTarget = true; + td.isTypeless = true; + td.isShaderResource = true; + td.useClearValue = true; + td.clearValue = nvrhi::Color(1.0f); + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + td.debugName = "rt_LocalShadowAtlas"; + state.atlasOwned = nv->createTexture(td); + state.atlas = state.atlasOwned; + state.atlasHandle = {}; + } { nvrhi::TextureDesc td; @@ -266,14 +283,24 @@ void InitializeLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& s } state.initialized = true; - state.enabled = (state.atlas != nullptr && state.cascadeCB != nullptr && state.cullDoneMarker != nullptr); + state.enabled = (state.atlas != nullptr && state.cascadeCB != nullptr && state.cullDoneMarker != nullptr && + state.clearDepthPipeline != nullptr && state.staticCacheTex != nullptr); if (state.enabled) + { + if (nvrhi::IDevice* nvDev = device ? device->GetNVRHIDevice() : nullptr) + framegraph::GetPassResourceCache().EnsureShadowBindDummies(nvDev); Msg("* [LocalShadow] Init: OK (%ux%u × %u pages, CB versions %u, clear=%d, static=%d, esmTex=%d, esmPipe=%d/%d)", res, res, pages, state.cascadeCBMaxVersions, state.clearDepthPipeline ? 1 : 0, state.staticCacheTex ? 1 : 0, state.esmAtlasTex ? 1 : 0, state.esmConvertPipeline ? 1 : 0, state.esmBlurPipeline ? 1 : 0); + } else - Msg("! [LocalShadow] Failed to create local shadow atlas/CB"); + { + Msg("! [LocalShadow] Failed to create local shadow atlas/CB — lights stay unshadowed"); + state.allocFailed = true; + state.atlasOwned = nullptr; + state.atlas = nullptr; + } } void ShutdownLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& state) @@ -284,7 +311,9 @@ void ShutdownLocalShadowPass(fg::RenderDevice* device, LocalShadowPassState& sta device->GetFGResourceManager()->GetRTFactory()->ReleaseRenderTarget(state.atlasHandle); } state.atlasHandle = {}; + state.atlasOwned = nullptr; state.atlas = nullptr; + state.allocFailed = false; state.staticCache = nullptr; state.staticCacheTex = nullptr; state.esmAtlas = nullptr; @@ -345,6 +374,8 @@ LocalShadowOutputs setupLocalShadowPass( if (!state.enabled || !state.atlas || !state.cascadeCB || !state.cullDoneMarker || !shadowState.enabled || !shadowState.pipeline) return outputs; + if (!bindlessConfig.UseGPUCulling() || !bindlessConfig.UseMegaBuffers()) + return outputs; nvrhi::IDevice* nvInit = device->GetNVRHIDevice(); if (!gpuCulling || !nvInit || !gpuCulling->EnsureLightShadowCullPipeline(nvInit)) @@ -461,6 +492,12 @@ LocalShadowOutputs setupLocalShadowPass( if (!vsRefl || !psRefl || !drawIndexBuffer || !st.pipeline || !st.layout || !ls.cascadeCB) return; + { + ShadowCascadeCB priming{}; + priming.lightVP.identity(); + cmd->writeBuffer(ls.cascadeCB, &priming, sizeof(priming)); + } + auto& cache = GetPassResourceCache(); auto& matBuffer = bindless::MaterialBuffer::Instance(); matBuffer.Upload(ctx); @@ -521,6 +558,22 @@ LocalShadowOutputs setupLocalShadowPass( const u32 maxTilesThisFrame = static_cast(tilesRef.size()); u32 tilesDrawn = 0; + for (u32 slot = 0; slot < maxSlots; ++slot) + { + for (u32 si = 0; si < 3; ++si) + { + if (auto* b = data.gpuCulling->MutLocalShadowSlotDrawBinding(slot, si)) + *b = nullptr; + if (auto* s = data.gpuCulling->MutLocalShadowSlotDrawSrcInstance(slot, si)) + *s = nullptr; + } + } + ls.skin1w.bindingSet = nullptr; + ls.skinHq.bindingSet = nullptr; + ls.skin2w.bindingSet = nullptr; + ls.skin3w.bindingSet = nullptr; + ls.skin4w.bindingSet = nullptr; + struct LocalSkinDraw { nvrhi::IBuffer* vb = nullptr; diff --git a/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.h index 48c548b13ff..21cb3e6d50d 100644 --- a/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/LocalShadowPassSetup.h @@ -77,6 +77,8 @@ struct LocalShadowPassState u32 sliceCount = 0; bool initialized = false; bool enabled = false; + bool allocFailed = false; + nvrhi::TextureHandle atlasOwned; }; struct LocalShadowOutputs diff --git a/src/Layers/xrRender/FrameGraphPasses/LodPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/LodPassSetup.cpp index aaaee9efa2a..da091ddb0c4 100644 --- a/src/Layers/xrRender/FrameGraphPasses/LodPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/LodPassSetup.cpp @@ -72,7 +72,7 @@ void EnsureLodPipeline(nvrhi::IDevice* nv, LodPassState& st) desc.setPrimType(nvrhi::PrimitiveType::TriangleList); desc.renderState.depthStencilState.setDepthTestEnable(true); desc.renderState.depthStencilState.setDepthWriteEnable(true); - desc.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + desc.renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::GreaterOrEqual); desc.renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); desc.renderState.blendState.targets[0].setBlendEnable(false); @@ -141,10 +141,8 @@ DefaultOutputLayout setupLodPass( data.height = height; data.color = pb.readWrite(inputs.albedo, ResourceState::RenderTarget); data.normal = pb.readWrite(inputs.normal, ResourceState::RenderTarget); - if (inputs.baseColor.is_valid()) - data.baseColor = pb.readWrite(inputs.baseColor, ResourceState::RenderTarget); - if (inputs.worldPos.is_valid()) - data.worldPos = pb.readWrite(inputs.worldPos, ResourceState::RenderTarget); + data.baseColor = pb.readWrite(inputs.baseColor, ResourceState::RenderTarget); + data.worldPos = pb.readWrite(inputs.worldPos, ResourceState::RenderTarget); data.depth = pb.readWrite(inputs.depth, ResourceState::DepthStencilWrite); data.outputs.albedo = data.color; data.outputs.normal = data.normal; @@ -163,7 +161,7 @@ DefaultOutputLayout setupLodPass( auto* baseRT = data.baseColor.is_valid() ? graph.GetPhysicalTexture(data.baseColor) : nullptr; auto* worldRT = data.worldPos.is_valid() ? graph.GetPhysicalTexture(data.worldPos) : nullptr; auto* depthRT = graph.GetPhysicalTexture(data.depth); - if (!colorRT || !depthRT) + if (!colorRT || !depthRT || !normalRT || !baseRT || !worldRT) return; const u32 maxVerts = u32(data.impostors->size()) * 4; @@ -222,9 +220,9 @@ DefaultOutputLayout setupLodPass( nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(colorRT); - if (normalRT) fbDesc.addColorAttachment(normalRT); - if (baseRT) fbDesc.addColorAttachment(baseRT); - if (worldRT) fbDesc.addColorAttachment(worldRT); + fbDesc.addColorAttachment(normalRT); + fbDesc.addColorAttachment(baseRT); + fbDesc.addColorAttachment(worldRT); fbDesc.setDepthAttachment(depthRT); auto& cache = GetPassResourceCache(); auto fb = cache.GetOrCreateFramebuffer("LodPass", fbDesc, nv); @@ -259,10 +257,13 @@ DefaultOutputLayout setupLodPass( if (texMgr && inst.lod->textureName.size()) { - auto h = texMgr->LoadTexture(inst.lod->textureName.c_str()); + xr_string lodTexName = inst.lod->textureName.c_str(); + if (size_t comma = lodTexName.find(','); comma != xr_string::npos) + lodTexName.resize(comma); + auto h = texMgr->LoadTexture(lodTexName.c_str()); if (auto* t = texMgr->GetNVRHITexture(h)) baseTex = t; - xr_string hemiName = inst.lod->textureName.c_str(); + xr_string hemiName = lodTexName; hemiName += "_nm"; auto hh = texMgr->LoadTexture(hemiName.c_str()); if (auto* t = texMgr->GetNVRHITexture(hh)) @@ -271,7 +272,7 @@ DefaultOutputLayout setupLodPass( hemiTex = baseTex; pbrTex = baseTex; - shared_str pbrName = TextureDescr.GetPBRName(inst.lod->textureName); + shared_str pbrName = TextureDescr.GetPBRName(shared_str(lodTexName.c_str())); if (pbrName.size()) { auto hp = texMgr->LoadTexture(pbrName.c_str()); @@ -280,7 +281,7 @@ DefaultOutputLayout setupLodPass( } else { - xr_string fallback = inst.lod->textureName.c_str(); + xr_string fallback = lodTexName; fallback += "_pbr"; auto hp = texMgr->LoadTexture(fallback.c_str()); if (auto* t = texMgr->GetNVRHITexture(hp)) diff --git a/src/Layers/xrRender/FrameGraphPasses/PassCommon.cpp b/src/Layers/xrRender/FrameGraphPasses/PassCommon.cpp index 996b28c80f9..27d8c19fb0e 100644 --- a/src/Layers/xrRender/FrameGraphPasses/PassCommon.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/PassCommon.cpp @@ -148,12 +148,12 @@ void ResolveEnvSkyCubes(fg::RenderDevice* device, nvrhi::ITexture*& outSky0, nvr ? env.Current[1]->sky_texture_name : shared_str(); - if (name0.size() && (name0 != s_cachedName0 || !s_cachedTex0)) + if (name0.size() && name0 != s_cachedName0) { s_cachedName0 = name0; s_cachedTex0 = texManager->GetNVRHITexture(texManager->LoadTexture(name0.c_str())); } - if (name1.size() && (name1 != s_cachedName1 || !s_cachedTex1)) + if (name1.size() && name1 != s_cachedName1) { s_cachedName1 = name1; s_cachedTex1 = texManager->GetNVRHITexture(texManager->LoadTexture(name1.c_str())); diff --git a/src/Layers/xrRender/FrameGraphPasses/PathTracerPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/PathTracerPassSetup.cpp index 45249776e2b..698cac31ab2 100644 --- a/src/Layers/xrRender/FrameGraphPasses/PathTracerPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/PathTracerPassSetup.cpp @@ -56,7 +56,8 @@ struct PathTracerCB { u32 skinnedBatchStart; u32 grassBatchStart; u32 detailAtlasIndex; - u32 pad[2]; + u32 particleBatchStart; + u32 pad; }; static_assert(sizeof(PathTracerCB) == 160, "PathTracerCB must be 160 bytes"); @@ -275,9 +276,15 @@ PathTracerOutput setupPathTracerPass( else cbData.grassBatchStart = 0xFFFFFFFFu; + if (batchCounts.particles > 0) + cbData.particleBatchStart = batchCounts.identityStatic + batchCounts.terrain + + batchCounts.transparent + batchCounts.instancedTotal + + batchCounts.skinned + batchCounts.grass; + else + cbData.particleBatchStart = 0xFFFFFFFFu; + cbData.detailAtlasIndex = accelMgr->GetDetailAtlasIndex(); - cbData.pad[0] = 0; - cbData.pad[1] = 0; + cbData.pad = 0; auto& passData = fg.addCallbackPass( "Path Tracer", diff --git a/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.cpp index 6b3f4654c16..f246da6008e 100644 --- a/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.cpp @@ -166,7 +166,7 @@ VirtualResourceHandle setupPostProcessPass( td.height = height; td.format = outFmt; td.isRenderTarget = true; - td.isTransient = false; + td.isTransient = true; td.debugName = "rt_PostProcess"; data.output = pb.createTexture("rt_PostProcess", td); } @@ -245,14 +245,18 @@ VirtualResourceHandle setupPostProcessPass( } nvrhi::ITexture* noiseTex = data.st->noisePlaceholder.Get(); - auto* resMgr = data.device->GetFGResourceManager(); - auto* texMgr = resMgr ? resMgr->GetTextureManager() : nullptr; - if (texMgr) + if (!data.st->noiseTexture) { - auto handle = texMgr->LoadTexture("fx\\fx_noise2"); - if (auto* t = texMgr->GetNVRHITexture(handle)) - noiseTex = t; + auto* resMgr = data.device->GetFGResourceManager(); + auto* texMgr = resMgr ? resMgr->GetTextureManager() : nullptr; + if (texMgr) + { + auto handle = texMgr->LoadTexture("fx\\fx_noise2"); + data.st->noiseTexture = texMgr->GetNVRHITexture(handle); + } } + if (data.st->noiseTexture) + noiseTex = data.st->noiseTexture; nvrhi::ITexture* grad0 = data.st->noisePlaceholder.Get(); nvrhi::ITexture* grad1 = data.st->noisePlaceholder.Get(); diff --git a/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.h index 4ac001f1606..b22b1461583 100644 --- a/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/PostProcessPassSetup.h @@ -16,6 +16,7 @@ struct PostProcessPassState nvrhi::BindingLayoutHandle layout; nvrhi::Format pipelineFormat = nvrhi::Format::UNKNOWN; nvrhi::TextureHandle noisePlaceholder; + nvrhi::ITexture* noiseTexture = nullptr; bool initialized = false; }; diff --git a/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.cpp index 46f229b19f9..0fff6e4d6df 100644 --- a/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/RainShadowPassSetup.cpp @@ -49,7 +49,7 @@ void ComputeTopDownRainMatrices(float extent, float height, u32 smapRes, Fmatrix view.build_camera_dir(eye, dir, up); Fmatrix proj; - proj.build_projection_ortho(e * 2.f, e * 2.f, 1.f, height + 120.f); + proj.build_projection_ortho_stdz(e * 2.f, e * 2.f, 1.f, height + 120.f); outClipVP.mul(proj, view); @@ -213,6 +213,8 @@ RainShadowOutputs setupRainShadowPass( InitializeRainShadowPass(device, state); if (!state.enabled || !state.rainSM || !state.rainPipeline || !state.rainLayout || !state.rainCB) return outputs; + if (!bindlessConfig.UseGPUCulling() || !bindlessConfig.UseMegaBuffers()) + return outputs; const bool rtWet = (ps_r_rt_gi != 0) || (ps_r_path_tracer != 0); float requested; diff --git a/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.cpp index 34b922333fb..10251868db6 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SSRPassSetup.cpp @@ -19,7 +19,7 @@ using namespace framegraph; namespace { -constexpr u32 kSSRPipeVersion = 72; +constexpr u32 kSSRPipeVersion = 74; struct SSRQualityParams { @@ -146,13 +146,13 @@ void InitSSR(nvrhi::IDevice* nv, SSRPassState& st) auto& cache = GetPassResourceCache(); st.layout = cache.GetOrCreateBindingLayoutFromReflection( - "SSRResolve_v71", *vs.reflection, *ps.reflection, nv); + "SSRResolve_v74", *vs.reflection, *ps.reflection, nv); st.blurLayout = cache.GetOrCreateBindingLayoutFromReflection( - "SSRBlur_v71", *vs.reflection, *psBlur.reflection, nv); + "SSRBlur_v74", *vs.reflection, *psBlur.reflection, nv); st.temporalLayout = cache.GetOrCreateBindingLayoutFromReflection( - "SSRTemporal_v71", *vs.reflection, *psTemp.reflection, nv); + "SSRTemporal_v74", *vs.reflection, *psTemp.reflection, nv); st.applyLayout = cache.GetOrCreateBindingLayoutFromReflection( - "SSRApply_v72_MetalWet", *vs.reflection, *psApply.reflection, nv); + "SSRApply_v74", *vs.reflection, *psApply.reflection, nv); auto makeSingle = [&](auto& psSh, nvrhi::BindingLayoutHandle layout, nvrhi::GraphicsPipelineHandle& pipe, const char* name) { @@ -175,15 +175,15 @@ void InitSSR(nvrhi::IDevice* nv, SSRPassState& st) pipe = cache.GetOrCreatePipeline(name, desc, fb, nv); }; - makeSingle(ps, st.layout, st.pipeline, "SSRResolve_v71"); - makeSingle(psBlur, st.blurLayout, st.blurPipeline, "SSRBlur_v71"); - makeSingle(psTemp, st.temporalLayout, st.temporalPipeline, "SSRTemporal_v71"); - makeSingle(psApply, st.applyLayout, st.applyPipeline, "SSRApply_v72_MetalWet"); + makeSingle(ps, st.layout, st.pipeline, "SSRResolve_v73"); + makeSingle(psBlur, st.blurLayout, st.blurPipeline, "SSRBlur_v73"); + makeSingle(psTemp, st.temporalLayout, st.temporalPipeline, "SSRTemporal_v73"); + makeSingle(psApply, st.applyLayout, st.applyPipeline, "SSRApply_v73"); st.initialized = true; st.pipeVersion = kSSRPipeVersion; if (st.pipeline && st.blurPipeline && st.temporalPipeline && st.applyPipeline) - Msg("* [SSR] initialized v72 (SSR∪RT metal/wet)"); + Msg("* [SSR] initialized v74 (mirror hit + apply)"); else Msg("! [SSR] Pipeline create failed (resolve=%d blur=%d temp=%d apply=%d)", !!st.pipeline, !!st.blurPipeline, !!st.temporalPipeline, !!st.applyPipeline); @@ -446,6 +446,32 @@ VirtualResourceHandle setupSSRPass( else litSrv = color; } + + { + const auto& ddesc = depthTex->getDesc(); + if (!data.st->depthCopy || + data.st->depthCopy->getDesc().width != ddesc.width || + data.st->depthCopy->getDesc().height != ddesc.height) + { + nvrhi::TextureDesc td{}; + td.width = ddesc.width; + td.height = ddesc.height; + td.format = nvrhi::Format::D32; + td.debugName = "SSR_DepthCopy"; + td.isShaderResource = true; + td.isTypeless = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + data.st->depthCopy = nv->createTexture(td); + } + if (data.st->depthCopy) + { + cmd->setTextureState(depthTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(data.st->depthCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(data.st->depthCopy, nvrhi::TextureSlice(), depthTex, nvrhi::TextureSlice()); + cmd->setTextureState(data.st->depthCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + } + } #endif cmd->setTextureState(color, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); @@ -475,7 +501,7 @@ VirtualResourceHandle setupSSRPass( float(Device.dwFrame & 1023), q.steps, q.distance, q.thickness, q.refine, q.enableNearMarch, 0.f, 0.f, 0.f}; - auto* ssrCB = cache.GetOrCreateVolatileCB("SSR", "SSRParams_v71", sizeof(PCB), data.device); + auto* ssrCB = cache.GetOrCreateVolatileCB("SSR", "SSRParams_v74", sizeof(PCB), data.device); if (ssrCB) cmd->writeBuffer(ssrCB, &p, sizeof(p)); @@ -505,10 +531,8 @@ VirtualResourceHandle setupSSRPass( #if defined(XR_PLATFORM_APPLE) nvrhi::ITexture* sceneColorSrv = data.st->colorCopy ? data.st->colorCopy.Get() : reflection; nvrhi::ITexture* litSrv = data.st->litCopy ? data.st->litCopy.Get() : color; - nvrhi::ITexture* depthSrv = data.st->depthCopy ? data.st->depthCopy.Get() : depthTex; -#else - nvrhi::ITexture* depthSrv = depthTex; #endif + nvrhi::ITexture* depthSrv = data.st->depthCopy ? data.st->depthCopy.Get() : depthTex; cmd->clearTextureFloat(ssrTex, nvrhi::AllSubresources, nvrhi::Color(0.f)); { diff --git a/src/Layers/xrRender/FrameGraphPasses/ShadowCascadeFit.cpp b/src/Layers/xrRender/FrameGraphPasses/ShadowCascadeFit.cpp index 6bfd27bf78f..84fbfffca8a 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ShadowCascadeFit.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ShadowCascadeFit.cpp @@ -142,7 +142,7 @@ void ComputeFrustumFitCascadeMatrices( viewOff.mul(zOff, lightView); Fmatrix lightProj; - lightProj.build_projection_ortho(orthoSize, orthoSize, zn, zn + zf); + lightProj.build_projection_ortho_stdz(orthoSize, orthoSize, zn, zn + zf); outClipVP.mul(lightProj, viewOff); const float fRange = (cascadeIndex == 0) ? ps_r2_sun_depth_near_scale : ps_r2_sun_depth_far_scale; diff --git a/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.cpp index cd66a69e07a..a2a1c427d11 100644 --- a/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/ShadowPassSetup.cpp @@ -668,6 +668,8 @@ void InitializeShadowPass(fg::RenderDevice* device, ShadowPassState& state) state.initialized = true; state.enabled = true; + if (nvrhi::IDevice* nv = device ? device->GetNVRHIDevice() : nullptr) + framegraph::GetPassResourceCache().EnsureShadowBindDummies(nv); Msg("* [ShadowPass] Init: OK (CSM ladder %u/%u/%u, HUD %ux%u)", state.cascadeResolution[0], state.cascadeResolution[1], state.cascadeResolution[2], ShadowPassState::kHUDShadowResolution, ShadowPassState::kHUDShadowResolution); diff --git a/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.cpp index f3483191784..5a755de3cd8 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "SunShaftsPassSetup.h" #include "ShaderConstants.h" +#include "TAAPassSetup.h" #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/PassResourceCache.h" @@ -60,7 +61,7 @@ struct SunShaftsCombineData void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state) { - constexpr u32 kPipeVersion = 6; + constexpr u32 kPipeVersion = 16; if (!device) return; if (state.initialized && state.pipeline && state.combinePipeline && state.pipeVersion == kPipeVersion) @@ -101,8 +102,9 @@ void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state) } auto& cache = GetPassResourceCache(); + cache.EnsureShadowBindDummies(device); state.layout = cache.GetOrCreateBindingLayoutFromReflection( - "SunShafts_CSMLadder_v4", *state.marchVsReflection, *state.marchPsReflection, device); + "SunShafts_CSMLadder_v16", *state.marchVsReflection, *state.marchPsReflection, device); nvrhi::GraphicsPipelineDesc desc; desc.setVertexShader(vs.handle); @@ -116,14 +118,14 @@ void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state) nvrhi::FramebufferInfoEx fbInfo; fbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); - state.pipeline = cache.GetOrCreatePipeline("SunShafts_v4", desc, fbInfo, device); + state.pipeline = cache.GetOrCreatePipeline("SunShafts_v16", desc, fbInfo, device); auto combinePs = loader->LoadPixelShader("volumetric/sunshafts_combine"); if (combinePs.handle && combinePs.reflection) { state.combinePsReflection = loader->GetCachedReflection("volumetric/sunshafts_combine", ".ps"); state.combineLayout = cache.GetOrCreateBindingLayoutFromReflection( - "SunShaftsCombine_v4", *state.marchVsReflection, *state.combinePsReflection, device); + "SunShaftsCombine_v16", *state.marchVsReflection, *state.combinePsReflection, device); nvrhi::GraphicsPipelineDesc cdesc; cdesc.setVertexShader(vs.handle); @@ -144,7 +146,7 @@ void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state) nvrhi::FramebufferInfoEx cfbInfo; cfbInfo.addColorFormat(nvrhi::Format::RGBA16_FLOAT); - state.combinePipeline = cache.GetOrCreatePipeline("SunShaftsCombine_v4", cdesc, cfbInfo, device); + state.combinePipeline = cache.GetOrCreatePipeline("SunShaftsCombine_v16", cdesc, cfbInfo, device); } else { @@ -153,10 +155,8 @@ void InitializeSunShaftsPass(nvrhi::IDevice* device, SunShaftsPassState& state) state.initialized = true; state.pipeVersion = kPipeVersion; - Msg("* [SunShafts] Pass initialized v4 (march=%d combine=%d cbs=%u srvs=%u)", - state.pipeline ? 1 : 0, state.combinePipeline ? 1 : 0, - (u32)state.marchPsReflection->constantLayout.constantBuffers.buffers.size(), - (u32)state.marchPsReflection->rtBindings.inputTextures.size()); + Msg("* [SunShafts] Pass initialized v16 (R32 depth/CSM) march=%d combine=%d", + state.pipeline ? 1 : 0, state.combinePipeline ? 1 : 0); } float ResolveSunShaftsIntensity() @@ -172,7 +172,7 @@ float ResolveSunShaftsIntensity() if (SunshaftsIntensity > 0.f) intensity = SunshaftsIntensity; intensity *= std::max(ps_r_sun_shafts_scale, 0.f); - return intensity; + return std::max(intensity, 0.f); } static bool NeedSunShafts(float& outIntensity, u32& outQuality) @@ -269,19 +269,46 @@ VirtualResourceHandle setupSunShaftsPass( if (!depthTex || !worldPosTex || !outTex) return; + const auto& ddesc = depthTex->getDesc(); + if (!ps->depthCopy || + ps->depthCopy->getDesc().width != ddesc.width || + ps->depthCopy->getDesc().height != ddesc.height) + { + nvrhi::TextureDesc td{}; + td.width = ddesc.width; + td.height = ddesc.height; + td.format = nvrhi::Format::D32; + td.debugName = "SunShafts_DepthCopy"; + td.isShaderResource = true; + td.isTypeless = true; + td.initialState = nvrhi::ResourceStates::ShaderResource; + td.keepInitialState = true; + ps->depthCopy = nv->createTexture(td); + } + nvrhi::ITexture* depthSrv = depthTex; + if (ps->depthCopy) + { + cmd->setTextureState(depthTex, nvrhi::AllSubresources, nvrhi::ResourceStates::CopySource); + cmd->setTextureState(ps->depthCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::CopyDest); + cmd->copyTexture(ps->depthCopy, nvrhi::TextureSlice(), depthTex, nvrhi::TextureSlice()); + cmd->setTextureState(ps->depthCopy, nvrhi::AllSubresources, nvrhi::ResourceStates::ShaderResource); + depthSrv = ps->depthCopy; + } + auto& cache = GetPassResourceCache(); auto staticGlobalsCB = cache.GetOrCreateVolatileCB( "Frame", "StaticGlobals", sizeof(StaticGlobals), ctx->GetDevice()); auto shaftCB = cache.GetOrCreateVolatileCB( - "SunShafts", "SunShaftsCB_v4", sizeof(SunShaftsCBGPU), ctx->GetDevice()); + "SunShafts", "SunShaftsCB_v15", sizeof(SunShaftsCBGPU), ctx->GetDevice()); - StaticGlobals sg = BuildStaticGlobals(); + StaticGlobals sg = BuildStaticGlobals(1.0f); + sg.m_VP = g_taa_unjittered_full_transform; + sg.m_InvVP = g_taa_unjittered_inv_full_transform; cmd->writeBuffer(staticGlobalsCB, &sg, sizeof(sg)); SunShaftsCBGPU cb{}; const float samples = (data.quality >= 3) ? 40.f : 20.f; - const float scale = ps_r_sun_shafts_scale > 0.f ? ps_r_sun_shafts_scale : 1.f; - cb.shaft_params.set(data.intensity, float(data.quality), samples, scale); + cb.shaft_params.set(data.intensity, float(data.quality), samples, 1.f); cmd->writeBuffer(shaftCB, &cb, sizeof(cb)); if (!ps->marchVsReflection || !ps->marchPsReflection) @@ -290,12 +317,11 @@ VirtualResourceHandle setupSunShaftsPass( BindingSetBuilder bsb(*ps->marchVsReflection, *ps->marchPsReflection, nv, "SunShaftsMarch"); bsb.ConstantBuffer("static_globals", staticGlobalsCB) .ConstantBuffer("SunShaftsCB", shaftCB) - .Texture("g_Depth", depthTex) + .Texture("g_Depth", depthSrv, nvrhi::Format::R32_FLOAT) .Texture("g_WorldPos", worldPosTex); { static const char* kNames[3] = {"g_ShadowMap0", "g_ShadowMap1", "g_ShadowMap2"}; - auto& smpCache = GetPassResourceCache(); - nvrhi::ITexture* dummy2D = smpCache.GetDummyShadowMap2D(nv); + nvrhi::ITexture* dummy2D = cache.GetDummyShadowMap2D(nv); for (u32 i = 0; i < 3; ++i) { nvrhi::ITexture* t = data.shadowCascades[i] ? data.shadowCascades[i] @@ -304,13 +330,13 @@ VirtualResourceHandle setupSunShaftsPass( bsb.Texture(kNames[i], t); } static const char* kHzb[3] = {"g_ShadowHZB0", "g_ShadowHZB1", "g_ShadowHZB2"}; - nvrhi::ITexture* dummyHzb = smpCache.GetDummyContactDepth(nv); + nvrhi::ITexture* dummyHzb = cache.GetDummyContactDepth(nv); for (u32 i = 0; i < 3; ++i) { if (dummyHzb) bsb.Texture(kHzb[i], dummyHzb); } - nvrhi::ITexture* contactHist = smpCache.GetDummyContactHistory(nv); + nvrhi::ITexture* contactHist = cache.GetDummyContactHistory(nv); if (contactHist) bsb.Texture("g_ContactHistory", contactHist); } @@ -320,7 +346,7 @@ VirtualResourceHandle setupSunShaftsPass( nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(outTex); - auto fb = cache.GetOrCreateFramebuffer("SunShaftsMarch_v4", fbDesc, nv); + auto fb = cache.GetOrCreateFramebuffer("SunShaftsMarch_v15", fbDesc, nv); nvrhi::Viewport vp(0.f, float(data.width), 0.f, float(data.height), 0.f, 1.f); nvrhi::GraphicsState state; @@ -366,7 +392,7 @@ VirtualResourceHandle setupSunShaftsPass( auto& cache = GetPassResourceCache(); auto combineCB = cache.GetOrCreateVolatileCB( - "SunShafts", "SunShaftsCombineCB_v4", sizeof(SunShaftsCombineCBGPU), ctx->GetDevice()); + "SunShafts", "SunShaftsCombineCB_v15", sizeof(SunShaftsCombineCBGPU), ctx->GetDevice()); SunShaftsCombineCBGPU cb{}; cb.combine_params.set( @@ -385,7 +411,7 @@ VirtualResourceHandle setupSunShaftsPass( nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(outTex); - auto fb = cache.GetOrCreateFramebuffer("SunShaftsCombine_v4", fbDesc, nv); + auto fb = cache.GetOrCreateFramebuffer("SunShaftsCombine_v15", fbDesc, nv); nvrhi::Viewport vp(0.f, float(data.width), 0.f, float(data.height), 0.f, 1.f); nvrhi::GraphicsState state; diff --git a/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h b/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h index c3a81dc450a..71c1f6524b8 100644 --- a/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h +++ b/src/Layers/xrRender/FrameGraphPasses/SunShaftsPassSetup.h @@ -24,6 +24,7 @@ struct SunShaftsPassState nvrhi::BindingLayoutHandle layout; nvrhi::GraphicsPipelineHandle combinePipeline; nvrhi::BindingLayoutHandle combineLayout; + nvrhi::TextureHandle depthCopy; framegraph::ExtractedReflection* marchVsReflection = nullptr; framegraph::ExtractedReflection* marchPsReflection = nullptr; framegraph::ExtractedReflection* combinePsReflection = nullptr; diff --git a/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.cpp index 5df18949069..571ba2b81e6 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/TAAPassSetup.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "TAAPassSetup.h" +#include "ShaderConstants.h" #include "Layers/xrRender/FrameGraph/FrameGraph.h" #include "Layers/xrRender/FrameGraph/PassResourceCache.h" #include "Layers/xrRender/FrameGraph/BindingSetBuilder.h" @@ -40,7 +41,7 @@ static float Halton(u32 index, u32 base) void ApplyTAAJitter() { g_taa_unjittered_full_transform = Device.mFullTransform; - g_taa_unjittered_inv_full_transform = Device.mInvFullTransform; + g_taa_unjittered_inv_full_transform.invert_44(g_taa_unjittered_full_transform); g_taa_jitter_prev_px = g_taa_jitter_px; g_taa_jitter_prev_py = g_taa_jitter_py; @@ -59,14 +60,14 @@ void ApplyTAAJitter() g_taa_jitter_px = jx; g_taa_jitter_py = jy; - const float w = float(std::max(1u, Device.dwWidth)); - const float h = float(std::max(1u, Device.dwHeight)); + const float w = float(std::max(1u, GetRenderWidth())); + const float h = float(std::max(1u, GetRenderHeight())); Fmatrix jitterMat; jitterMat.identity(); jitterMat._31 = (jx * 2.f) / w; jitterMat._32 = (jy * 2.f) / h; Device.mFullTransform.mul(jitterMat, g_taa_unjittered_full_transform); - Device.mInvFullTransform.invert(Device.mFullTransform); + Device.mInvFullTransform.invert_44(Device.mFullTransform); } namespace diff --git a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp index 86b9529a5a5..0373b1ac02a 100644 --- a/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/TransparentPassSetup.cpp @@ -76,11 +76,11 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame pipeDesc.primType = nvrhi::PrimitiveType::TriangleList; pipeDesc.renderState.depthStencilState.depthTestEnable = true; pipeDesc.renderState.depthStencilState.depthWriteEnable = false; - pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + pipeDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; pipeDesc.renderState.rasterState.frontCounterClockwise = false; pipeDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::Back; - pipeDesc.renderState.rasterState.depthBias = -16; - pipeDesc.renderState.rasterState.slopeScaledDepthBias = -2.f; + pipeDesc.renderState.rasterState.depthBias = 16; + pipeDesc.renderState.rasterState.slopeScaledDepthBias = 2.f; auto& rt0 = pipeDesc.renderState.blendState.targets[0]; rt0.blendEnable = true; @@ -93,7 +93,7 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame for (u32 rt = 1; rt < 4; ++rt) pipeDesc.renderState.blendState.targets[rt].setColorWriteMask(nvrhi::ColorMask(0)); - state.pipeline = cache.GetOrCreatePipeline("TransparentPass_v3_AlphaBias", pipeDesc, fbInfo, nvDevice); + state.pipeline = cache.GetOrCreatePipeline("TransparentPass_v4_RevZBias", pipeDesc, fbInfo, nvDevice); if (!state.pipeline) return; @@ -107,7 +107,7 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame mulRt.srcBlendAlpha = nvrhi::BlendFactor::Zero; mulRt.destBlendAlpha = nvrhi::BlendFactor::One; mulRt.blendOpAlpha = nvrhi::BlendOp::Add; - state.multiplyPipeline = cache.GetOrCreatePipeline("TransparentPass_v3_MultiplyBias", mulDesc, fbInfo, nvDevice); + state.multiplyPipeline = cache.GetOrCreatePipeline("TransparentPass_v4_MultiplyRevZBias", mulDesc, fbInfo, nvDevice); } QueryBindingLayoutFromPipeline(state.pipeline, state.layout); @@ -121,7 +121,7 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame state.waterVs = waterVs.handle; state.waterPs = waterPs.handle; state.waterLayout = cache.GetOrCreateBindingLayoutFromReflection( - "TransparentWater_v18_Murk", *waterVs.reflection, *waterPs.reflection, nvDevice); + "TransparentWater_v19_SSR", *waterVs.reflection, *waterPs.reflection, nvDevice); if (state.waterLayout) { nvrhi::GraphicsPipelineDesc waterDesc = pipeDesc; @@ -140,7 +140,7 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame nvrhi::ColorMask::Red | nvrhi::ColorMask::Green | nvrhi::ColorMask::Blue | nvrhi::ColorMask::Alpha); } - state.waterPipeline = cache.GetOrCreatePipeline("TransparentWater_v18_Murk", waterDesc, fbInfo, nvDevice); + state.waterPipeline = cache.GetOrCreatePipeline("TransparentWater_v19_SSR", waterDesc, fbInfo, nvDevice); } waterVs.reflection = nullptr; waterPs.reflection = nullptr; @@ -173,7 +173,7 @@ void InitializeTransparentResources(fg::RenderDevice* device, const nvrhi::Frame distortDesc.primType = nvrhi::PrimitiveType::TriangleList; distortDesc.renderState.depthStencilState.depthTestEnable = true; distortDesc.renderState.depthStencilState.depthWriteEnable = false; - distortDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::LessOrEqual; + distortDesc.renderState.depthStencilState.depthFunc = nvrhi::ComparisonFunc::GreaterOrEqual; distortDesc.renderState.rasterState.cullMode = nvrhi::RasterCullMode::None; auto& drt = distortDesc.renderState.blendState.targets[0]; drt.blendEnable = true; @@ -247,10 +247,8 @@ framegraph::DefaultOutputLayout setupTransparentPass( data.color = passBuilder.readWrite(inputs.albedo, ResourceState::RenderTarget); data.normal = passBuilder.readWrite(inputs.normal, ResourceState::RenderTarget); data.depth = passBuilder.read(inputs.depth, ResourceState::DepthStencilRead); - if (inputs.baseColor.is_valid()) - data.baseColor = passBuilder.readWrite(inputs.baseColor, ResourceState::RenderTarget); - if (inputs.worldPos.is_valid()) - data.worldPos = passBuilder.readWrite(inputs.worldPos, ResourceState::RenderTarget); + data.baseColor = passBuilder.readWrite(inputs.baseColor, ResourceState::RenderTarget); + data.worldPos = passBuilder.readWrite(inputs.worldPos, ResourceState::RenderTarget); for (u32 i = 0; i < 3; ++i) { if (config.shadowHZBHandles[i].is_valid()) passBuilder.read(config.shadowHZBHandles[i], ResourceState::ShaderResource); @@ -289,15 +287,14 @@ framegraph::DefaultOutputLayout setupTransparentPass( auto* baseColorRT = data.baseColor.is_valid() ? fg.GetPhysicalTexture(data.baseColor) : nullptr; auto* worldPosRT = data.worldPos.is_valid() ? fg.GetPhysicalTexture(data.worldPos) : nullptr; + if (!normalRT || !baseColorRT || !worldPosRT) + return; nvrhi::FramebufferDesc fbDesc; fbDesc.addColorAttachment(colorRT); - if (normalRT) - fbDesc.addColorAttachment(normalRT); - if (baseColorRT) - fbDesc.addColorAttachment(baseColorRT); - if (worldPosRT) - fbDesc.addColorAttachment(worldPosRT); + fbDesc.addColorAttachment(normalRT); + fbDesc.addColorAttachment(baseColorRT); + fbDesc.addColorAttachment(worldPosRT); fbDesc.setDepthAttachment(depthRT); auto& cache = framegraph::GetPassResourceCache(); auto framebuffer = cache.GetOrCreateFramebuffer("TransparentPass", fbDesc, nvDevice); diff --git a/src/Layers/xrRender/FrameGraphPasses/UIPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/UIPassSetup.cpp index ce795484070..cfa32964a31 100644 --- a/src/Layers/xrRender/FrameGraphPasses/UIPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/UIPassSetup.cpp @@ -127,6 +127,9 @@ framegraph::VirtualResourceHandle setupUIPass( if (!uiRender->GetBatches().empty()) { StaticGlobals staticGlobalsCB = {}; FillGlobalConstants(staticGlobalsCB); + const float uiW = float(std::max(1u, data.width)); + const float uiH = float(std::max(1u, data.height)); + staticGlobalsCB.screen_res.set(uiW, uiH, 1.0f / uiW, 1.0f / uiH); for (const auto& batch : uiRender->GetBatches()) { if (batch.uiShader && uiMatCache) { @@ -215,6 +218,9 @@ framegraph::VirtualResourceHandle setupCursorPass( if (!uiRender->GetBatches().empty()) { StaticGlobals staticGlobalsCB = {}; FillGlobalConstants(staticGlobalsCB); + const float uiW = float(std::max(1u, data.width)); + const float uiH = float(std::max(1u, data.height)); + staticGlobalsCB.screen_res.set(uiW, uiH, 1.0f / uiW, 1.0f / uiH); for (const auto& batch : uiRender->GetBatches()) { if (batch.uiShader && uiMatCache) { diff --git a/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.cpp b/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.cpp index 863097abb34..d2f95df2a6a 100644 --- a/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.cpp +++ b/src/Layers/xrRender/FrameGraphPasses/VolumetricPassSetup.cpp @@ -198,7 +198,7 @@ void InitializeVolumetricPass(nvrhi::IDevice* device, VolumetricPassState& state cb.byteSize = sizeof(FroxelParamsCB); cb.isConstantBuffer = true; cb.isVolatile = true; - cb.maxVersions = 16; + cb.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; cb.debugName = "FroxelParamsCB"; state.paramsCB = device->createBuffer(cb); } @@ -207,7 +207,7 @@ void InitializeVolumetricPass(nvrhi::IDevice* device, VolumetricPassState& state cb.byteSize = sizeof(TemporalParamsCB); cb.isConstantBuffer = true; cb.isVolatile = true; - cb.maxVersions = 16; + cb.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; cb.debugName = "VolTemporalCB"; state.temporalCB = device->createBuffer(cb); } @@ -216,7 +216,7 @@ void InitializeVolumetricPass(nvrhi::IDevice* device, VolumetricPassState& state cb.byteSize = sizeof(ParticleInjectParamsCB); cb.isConstantBuffer = true; cb.isVolatile = true; - cb.maxVersions = 16; + cb.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; cb.debugName = "VolParticleParamsCB"; state.particleParamsCB = device->createBuffer(cb); } @@ -235,7 +235,7 @@ void InitializeVolumetricPass(nvrhi::IDevice* device, VolumetricPassState& state cb.byteSize = sizeof(LightShaftParamsCB); cb.isConstantBuffer = true; cb.isVolatile = true; - cb.maxVersions = 16; + cb.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; cb.debugName = "VolLightShaftCB"; state.lightShaftCB = device->createBuffer(cb); } diff --git a/src/Layers/xrRender/GPUCullingManager.cpp b/src/Layers/xrRender/GPUCullingManager.cpp index eeb7d511167..e8f31d7a15a 100644 --- a/src/Layers/xrRender/GPUCullingManager.cpp +++ b/src/Layers/xrRender/GPUCullingManager.cpp @@ -24,6 +24,9 @@ #include "Layers/xrRender/RayTracing/RTAccelStructManager.h" #include "Layers/xrRender/FrameGraph/ShaderLoader.h" +extern ENGINE_API int ps_r_rt_gi; +extern ENGINE_API int ps_r_path_tracer; + namespace fg { extern xray::render::FrameGraphRenderer RImplementation; @@ -58,6 +61,17 @@ struct CullParamsCB { u32 padding[2]; }; +struct ParticleCullParamsCB { + Fmatrix prevViewProj; + Fvector4 frustumPlanes[6]; + Fvector4 cameraPos; + u32 slotCount; + u32 hiZWidth; + u32 hiZHeight; + u32 hiZMipLevels; +}; +static_assert(sizeof(ParticleCullParamsCB) == 192, "ParticleCullParamsCB must match particle_cull.cs"); + // ═══════════════════════════════════════════════════════ // DEBUG CONSTANT BUFFER (must match HLSL) // ═══════════════════════════════════════════════════════ @@ -167,13 +181,11 @@ void GPUCullingManager::Initialize(fg::RenderDevice* device) td.keepInitialState = true; td.debugName = "GPUCull_DummyHiZ"; m_dummyHizTexture = nvDevice->createTexture(td); - // Far depth (1.0) so HiZTestSphere always passes — all-visible path for - // DetailCull / frame-0 / forceDisableHiz when no real pyramid is bound. if (m_dummyHizTexture) { nvrhi::CommandListHandle cmd = nvDevice->createCommandList(); cmd->open(); - const float farD = 1.0f; + const float farD = 0.0f; cmd->writeTexture(m_dummyHizTexture, 0, 0, &farD, sizeof(farD)); cmd->close(); nvDevice->executeCommandList(cmd); @@ -660,21 +672,21 @@ void GPUCullingManager::CreateSkinnedCullingBuffers(fg::RenderDevice* device) m_skinnedObjectData.reserve(m_maxSkinnedObjects); m_skinnedDrawArgsData.reserve(m_maxSkinnedObjects); - // Global bone buffer for GPU-driven skinned rendering - { + for (u32 i = 0; i < 2; ++i) { nvrhi::BufferDesc desc; - desc.debugName = "GlobalBoneBuffer"; - desc.byteSize = MAX_TOTAL_BONES * BONE_STRIDE; // 8192 * 64 = 512KB + desc.debugName = (i == 0) ? "GlobalBoneBuffer0" : "GlobalBoneBuffer1"; + desc.byteSize = MAX_TOTAL_BONES * BONE_STRIDE; desc.structStride = BONE_STRIDE; desc.initialState = nvrhi::ResourceStates::ShaderResource; desc.keepInitialState = true; - m_globalBoneBuffer = nvDevice->createBuffer(desc); - if (!m_globalBoneBuffer) { - Msg("! [GPUCulling] Failed to create global bone buffer"); + m_globalBoneBuffers[i] = nvDevice->createBuffer(desc); + if (!m_globalBoneBuffers[i]) { + Msg("! [GPUCulling] Failed to create global bone buffer %u", i); return; } } + m_currentBoneBuffer = 0; // Pre-allocate staging buffer for max skeleton size m_boneStagingBuffer.resize(256); // Max bones per skeleton (typically 78) @@ -754,7 +766,10 @@ void GPUCullingManager::EnsureSkinnedBufferCapacity(u32 count) void GPUCullingManager::EnsureSkinnedBucketCapacity(SkinnedBucket& bucket, const char* name, u32 capacity) { if (bucket.objectBuffer - && bucket.objectBuffer->getDesc().byteSize >= capacity * sizeof(GPUObjectData)) + && bucket.objectBuffer->getDesc().byteSize >= capacity * sizeof(GPUObjectData) + && bucket.recordsBuffer + && bucket.recordsBuffer->getDesc().byteSize >= capacity * sizeof(SkinnedDrawRecord) + && bucket.recordsBuffer->getDesc().structStride == sizeof(SkinnedDrawRecord)) return; nvrhi::IDevice* nvDevice = m_device->GetNVRHIDevice(); @@ -2132,10 +2147,9 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr const u32 matCount = bindless::MaterialBuffer::Instance().GetMaterialCount(); if (batch.bindlessMaterialID == UINT32_MAX || batch.bindlessMaterialID >= matCount) return false; + if (!batch.megaBufferAlloc.valid) + return false; - // ───────────────────────────────────────────────────── - // BUILD OBJECT DATA (for culling) - // ───────────────────────────────────────────────────── GPUObjectData obj; obj.position = batch.worldBoundsCenter; obj.radius = batch.worldBoundsRadius; @@ -2148,25 +2162,19 @@ void GPUCullingManager::UploadSceneObjects(fg::RenderContext* ctx, const Geometr obj.flags |= GPU_OBJECT_ALPHA_TEST; if (batch.IsStrictB2F()) obj.flags |= GPU_OBJECT_TRANSPARENT; + if (!batch.isStatic) + obj.flags |= GPU_OBJECT_DYNAMIC; obj.pad0 = 0.0f; obj.pad1 = 0.0f; objectData.push_back(obj); - // ───────────────────────────────────────────────────── - // BUILD DRAW ARGS (for indirect draw) - // ───────────────────────────────────────────────────── IndirectDrawArgs args; args.indexCountPerInstance = batch.indexCount; - args.instanceCount = 1; // Compaction uses visibility buffer - if (batch.megaBufferAlloc.valid) { - args.startIndexLocation = batch.megaBufferAlloc.indexOffset; - args.baseVertexLocation = static_cast(batch.megaBufferAlloc.vertexOffset); - } else { - args.startIndexLocation = batch.startIndex; - args.baseVertexLocation = batch.baseVertex; - } + args.instanceCount = 1; + args.startIndexLocation = batch.megaBufferAlloc.indexOffset; + args.baseVertexLocation = static_cast(batch.megaBufferAlloc.vertexOffset); // startInstanceLocation carries the per-draw DRAWINDEX for the view-independent // "cast all" shadow path, which draws these raw args directly (no compaction). // The forward path ignores this: batch_compact.cs overwrites it with the compacted @@ -2826,6 +2834,22 @@ void GPUCullingManager::UploadSkinnedObjects(fg::RenderContext* ctx, const Geome rec.splatCount = sr.count; } rec.pad = 0; + if (skeleton && skeleton->fg_prev_bone_valid && + skeleton->fg_prev_world_frame + 1 == m_boneUploadFrameId) { + rec.prevWorld = skeleton->fg_prev_world; + rec.prevBoneOffset = skeleton->fg_prev_bone_upload_offset; + rec.prevValid = 1; + } else { + rec.prevWorld = batch.worldMatrix; + rec.prevBoneOffset = rec.boneOffset; + rec.prevValid = 0; + } + rec.pad1 = 0; + rec.pad2 = 0; + if (skeleton) { + skeleton->fg_curr_world = batch.worldMatrix; + skeleton->fg_curr_world_frame = m_boneUploadFrameId; + } bucket.records.push_back(rec); bucket.materialIDs.push_back(batch.bindlessMaterialID); } else { @@ -2927,42 +2951,68 @@ bool GPUCullingManager::EnsureSkinnedArgsGatePipeline(nvrhi::IDevice* nvDevice) void GPUCullingManager::BeginSkinnedFrame() { - // Reset bone buffer allocations for new frame ++m_boneUploadFrameId; + m_currentBoneBuffer ^= 1; m_currentBoneOffset = 0; } +u32 GPUCullingManager::GetPrevBoneOffset(CKinematics* skeleton) const +{ + if (!skeleton || !skeleton->fg_prev_bone_valid) + return 0; + return skeleton->fg_prev_bone_upload_offset; +} + +bool GPUCullingManager::HasPrevBones(CKinematics* skeleton) const +{ + return skeleton && skeleton->fg_prev_bone_valid != 0; +} + +void GPUCullingManager::SetSkeletonCurrWorld(CKinematics* skeleton, const Fmatrix& world) +{ + if (!skeleton) + return; + skeleton->fg_curr_world = world; + skeleton->fg_curr_world_frame = m_boneUploadFrameId; +} + u32 GPUCullingManager::GetOrUploadSkeleton(nvrhi::ICommandList* cmdList, CKinematics* skeleton) { if (!m_boneBufferInitialized || !skeleton || !cmdList) return 0; - // Check if already uploaded this frame if (skeleton->fg_bone_upload_frame == m_boneUploadFrameId) { return skeleton->fg_bone_upload_offset; } - // Allocate space for this skeleton u32 boneCount = skeleton->LL_BoneCount(); if (boneCount == 0) return 0; - // Check capacity if (m_currentBoneOffset + boneCount > MAX_TOTAL_BONES) { Msg("! [GPUCulling] Bone buffer full: need %u bones, have %u remaining", boneCount, MAX_TOTAL_BONES - m_currentBoneOffset); - return 0; // Return 0 offset, will render with identity bones + return 0; + } + + if (skeleton->fg_bone_upload_frame + 1 == m_boneUploadFrameId) { + skeleton->fg_prev_bone_upload_offset = skeleton->fg_bone_upload_offset; + skeleton->fg_prev_bone_valid = 1; + } else { + skeleton->fg_prev_bone_valid = 0; + } + + if (skeleton->fg_curr_world_frame + 1 == m_boneUploadFrameId) { + skeleton->fg_prev_world = skeleton->fg_curr_world; + skeleton->fg_prev_world_frame = skeleton->fg_curr_world_frame; } - // Record the offset for this skeleton u32 boneOffset = m_currentBoneOffset; skeleton->fg_bone_upload_frame = m_boneUploadFrameId; skeleton->fg_bone_upload_offset = boneOffset; - // Upload bones UploadSkeletonBones(cmdList, skeleton, boneOffset); - // Advance allocation pointer m_currentBoneOffset += boneCount; return boneOffset; @@ -2972,22 +3022,18 @@ void GPUCullingManager::UploadSkeletonBones(nvrhi::ICommandList* cmdList, CKinem { u32 boneCount = skeleton->LL_BoneCount(); - // Ensure staging buffer is large enough if (m_boneStagingBuffer.size() < boneCount) { m_boneStagingBuffer.resize(boneCount); } - // Slang uses column_major — raw row-major Fmatrix bytes are naturally transposed. - // No explicit transpose needed. for (u32 i = 0; i < boneCount; i++) { m_boneStagingBuffer[i] = skeleton->LL_GetTransform_R(u16(i)); } - // Upload to GPU at the correct offset u64 byteOffset = static_cast(boneOffset) * BONE_STRIDE; u64 byteSize = static_cast(boneCount) * BONE_STRIDE; - cmdList->writeBuffer(m_globalBoneBuffer, m_boneStagingBuffer.data(), byteSize, byteOffset); + cmdList->writeBuffer(m_globalBoneBuffers[m_currentBoneBuffer], m_boneStagingBuffer.data(), byteSize, byteOffset); } // ═══════════════════════════════════════════════════════ @@ -3907,14 +3953,12 @@ GPUCullOutput GPUCullingManager::SetupCullingPass( bindless::MaterialBuffer::Instance().Upload(ctx); - if (mgr->m_rtAccelMgr) { + if (mgr->m_rtAccelMgr && (ps_r_rt_gi || ps_r_path_tracer)) { + if (!mgr->m_rtAccelMgr->GetMaterialBuffer()) + mgr->m_rtAccelMgr->SetMaterialBuffer(bindless::MaterialBuffer::Instance().GetBuffer()); + if (!mgr->m_rtAccelMgr->GetTerrainMaterialBuffer()) + mgr->m_rtAccelMgr->SetTerrainMaterialBuffer(bindless::TerrainMaterialBuffer::Instance().GetBuffer()); mgr->m_rtAccelMgr->BuildIfNeeded(cmdList, mgr); - if (mgr->m_rtAccelMgr->IsReady()) { - if (!mgr->m_rtAccelMgr->GetMaterialBuffer()) - mgr->m_rtAccelMgr->SetMaterialBuffer(bindless::MaterialBuffer::Instance().GetBuffer()); - if (!mgr->m_rtAccelMgr->GetTerrainMaterialBuffer()) - mgr->m_rtAccelMgr->SetTerrainMaterialBuffer(bindless::TerrainMaterialBuffer::Instance().GetBuffer()); - } } } @@ -5112,11 +5156,12 @@ void GPUCullingManager::CreateParticleResources(fg::RenderDevice* device) nvrhi::BufferDesc desc; desc.debugName = "GPUCull_ParticleDrawArgs"; desc.byteSize = m_maxParticles * sizeof(IndirectDrawArgs); + desc.structStride = sizeof(IndirectDrawArgs); desc.canHaveUAVs = true; desc.canHaveRawViews = true; desc.isDrawIndirectArgs = true; desc.initialState = nvrhi::ResourceStates::UnorderedAccess; - desc.keepInitialState = true; // Let NVRHI handle state transitions + desc.keepInitialState = true; m_particleDrawArgsBuffer = nvDevice->createBuffer(desc); if (!m_particleDrawArgsBuffer) { @@ -5127,9 +5172,8 @@ void GPUCullingManager::CreateParticleResources(fg::RenderDevice* device) { nvrhi::BufferDesc desc; - desc.debugName = "GPUCull_ParticleVisibleCount"; - desc.byteSize = sizeof(u32); - desc.structStride = sizeof(u32); + desc.debugName = "GPUCull_ParticleCullStats"; + desc.byteSize = sizeof(u32) * 2; desc.canHaveUAVs = true; desc.canHaveRawViews = true; desc.initialState = nvrhi::ResourceStates::UnorderedAccess; @@ -5137,7 +5181,7 @@ void GPUCullingManager::CreateParticleResources(fg::RenderDevice* device) m_particleVisibleCountBuffer = nvDevice->createBuffer(desc); if (!m_particleVisibleCountBuffer) { - Msg("! [GPUCulling] Failed to create particle visible count buffer"); + Msg("! [GPUCulling] Failed to create particle cull stats buffer"); return; } } @@ -5145,7 +5189,7 @@ void GPUCullingManager::CreateParticleResources(fg::RenderDevice* device) { fg::RenderDevice::BufferDesc desc; desc.debugName = "GPUCull_ParticleParams"; - desc.byteSize = sizeof(CullParamsCB); + desc.byteSize = sizeof(ParticleCullParamsCB); desc.isConstantBuffer = true; desc.isVolatile = true; desc.maxVersions = fg::RenderDevice::BufferDesc::VOLATILE_CB_MAX_VERSIONS; @@ -5160,7 +5204,7 @@ void GPUCullingManager::CreateParticleResources(fg::RenderDevice* device) { auto& cache = framegraph::GetPassResourceCache(); auto* particleCullRefl = GEnv.Render->GetShaderLoader()->GetCachedReflection("particle_cull", ".cs"); - m_particleCullLayout = cache.GetOrCreateBindingLayoutFromReflection("GPUCull_ParticleCull", *particleCullRefl, nvDevice); + m_particleCullLayout = cache.GetOrCreateBindingLayoutFromReflection("GPUCull_ParticleCull_v2", *particleCullRefl, nvDevice); if (!m_particleCullLayout) { Msg("! [GPUCulling] Failed to create particle binding layout"); return; @@ -5345,27 +5389,18 @@ GPUParticleCullOutput GPUCullingManager::SetupParticleCullingPass( return; } - u32 zero = 0; - cmdList->writeBuffer(mgr->m_particleVisibleCountBuffer, &zero, sizeof(u32)); - - u32 frameId = Device.dwFrame + 1u; - if (frameId == 0) - frameId = 1; - - CullParamsCB cb; - cb.viewProj = Device.mFullTransform; - cb.cameraPos = Device.vCameraPosition; - float farPlane = g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.far_plane : 300.0f; - cb.maxDistanceSq = farPlane * farPlane; - cb.objectCount = data.particleCount; - cb.hizWidth = data.hizWidth; - cb.hizHeight = data.hizHeight; - cb.hizMipLevels = data.hizMipLevels; - cb.frameId = frameId; - cb.hizEnable = (data.forceDisableHiz || ps_r_hiz_occlusion == 0) ? 0u : 1u; - cb.padding[0] = cb.padding[1] = 0; + const u32 statsZero[2] = { 0, 0 }; + cmdList->writeBuffer(mgr->m_particleVisibleCountBuffer, statsZero, sizeof(statsZero)); + ParticleCullParamsCB cb{}; + cb.prevViewProj = Device.mFullTransform; mgr->ExtractFrustumPlanes(Device.mFullTransform, cb.frustumPlanes); + cb.cameraPos.set(Device.vCameraPosition.x, Device.vCameraPosition.y, + Device.vCameraPosition.z, 0.0f); + cb.slotCount = data.particleCount; + cb.hiZWidth = data.hizWidth; + cb.hiZHeight = data.hizHeight; + cb.hiZMipLevels = (data.forceDisableHiz || ps_r_hiz_occlusion == 0) ? 0u : data.hizMipLevels; cmdList->writeBuffer(mgr->m_device->GetNativeBuffer(mgr->m_particleCullParamsCB), &cb, sizeof(cb)); @@ -5374,8 +5409,8 @@ GPUParticleCullOutput GPUCullingManager::SetupParticleCullingPass( bsb.ConstantBuffer("ParticleCullParams", mgr->m_device->GetNativeBuffer(mgr->m_particleCullParamsCB)) .BufferSRV("g_ParticleData", mgr->m_particleBuffer) .Texture("g_HiZPyramid", hizTexture) - .BufferUAV("g_VisibleIndices", mgr->m_particleVisibleCountBuffer) - .BufferUAV("g_VisibleCount", mgr->m_particleDrawArgsBuffer); + .BufferUAV("g_DrawArgs", mgr->m_particleDrawArgsBuffer) + .BufferUAV("g_CullStats", mgr->m_particleVisibleCountBuffer); nvrhi::BindingSetHandle bindingSet = framegraph::GetPassResourceCache().GetOrCreateBindingSet(bsb.Build(), mgr->m_particleCullLayout, nvDevice); if (!bindingSet) { diff --git a/src/Layers/xrRender/GPUCullingManager.h b/src/Layers/xrRender/GPUCullingManager.h index 143e64752c4..d4943eb5b6c 100644 --- a/src/Layers/xrRender/GPUCullingManager.h +++ b/src/Layers/xrRender/GPUCullingManager.h @@ -65,6 +65,7 @@ enum GPUObjectFlags : u32 { GPU_OBJECT_OPAQUE = 0x1, GPU_OBJECT_ALPHA_TEST = 0x2, GPU_OBJECT_TRANSPARENT = 0x4, + GPU_OBJECT_DYNAMIC = 0x8, }; // ═══════════════════════════════════════════════════════ @@ -488,8 +489,13 @@ class GPUCullingManager { u32 splatOffset; u32 splatCount; u32 pad; + Fmatrix prevWorld; + u32 prevBoneOffset; + u32 prevValid; + u32 pad1; + u32 pad2; }; - static_assert(sizeof(SkinnedDrawRecord) == 80, "SkinnedDrawRecord must be 80 bytes"); + static_assert(sizeof(SkinnedDrawRecord) == 160, "SkinnedDrawRecord must be 160 bytes"); struct SkinnedBucket { nvrhi::BufferHandle objectBuffer; @@ -526,15 +532,16 @@ class GPUCullingManager { // Global bone buffer pool - all skeleton bones are uploaded here each frame. // Each skeleton gets a contiguous range: g_BoneMatrices[offset + boneIndex] - // Call at frame start to reset bone buffer allocations void BeginSkinnedFrame(); - // Get bone offset for a skeleton, uploading if not already done this frame - // Returns offset (in bone count) into global buffer u32 GetOrUploadSkeleton(nvrhi::ICommandList* cmdList, CKinematics* skeleton); - // Get the global bone buffer for shader binding - nvrhi::IBuffer* GetGlobalBoneBuffer() const { return m_globalBoneBuffer.Get(); } + nvrhi::IBuffer* GetGlobalBoneBuffer() const { return m_globalBoneBuffers[m_currentBoneBuffer].Get(); } + nvrhi::IBuffer* GetPrevBoneBuffer() const { return m_globalBoneBuffers[m_currentBoneBuffer ^ 1].Get(); } + u32 GetPrevBoneOffset(CKinematics* skeleton) const; + bool HasPrevBones(CKinematics* skeleton) const; + u32 GetBoneUploadFrameId() const { return m_boneUploadFrameId; } + void SetSkeletonCurrWorld(CKinematics* skeleton, const Fmatrix& world); // Process readback results from previous frame (call at frame start) void ProcessStatsReadback(); @@ -799,11 +806,10 @@ class GPUCullingManager { void DispatchSkinnedBucketCompaction(nvrhi::ICommandList* cmdList, nvrhi::IDevice* nvDevice, SkinnedBucket& bucket, u32 frameId); - // Global bone buffer for GPU-driven skinned rendering - // All skeleton bones are uploaded here each frame, indexed by per-instance offset - static constexpr u32 MAX_TOTAL_BONES = 8192; // ~100 skeletons * 78 bones - static constexpr u32 BONE_STRIDE = sizeof(Fmatrix); // 64 bytes - nvrhi::BufferHandle m_globalBoneBuffer; + static constexpr u32 MAX_TOTAL_BONES = 8192; + static constexpr u32 BONE_STRIDE = sizeof(Fmatrix); + nvrhi::BufferHandle m_globalBoneBuffers[2]; + u32 m_currentBoneBuffer = 0; u32 m_boneUploadFrameId = 0; xr_vector m_boneStagingBuffer; u32 m_currentBoneOffset = 0; diff --git a/src/Layers/xrRender/Geometry/MaterialCache.cpp b/src/Layers/xrRender/Geometry/MaterialCache.cpp index 1da87af9f8b..988e395d850 100644 --- a/src/Layers/xrRender/Geometry/MaterialCache.cpp +++ b/src/Layers/xrRender/Geometry/MaterialCache.cpp @@ -1067,7 +1067,7 @@ u32 MaterialCache::RegisterBindlessMaterial(MaterialPSO* matPSO) matData.tessMethod = 0; matData.sssMapIndex = INVALID_TEXTURE_INDEX; matData.emissiveIntensity = 0.f; - matData._pad1 = 0; + matData.lmapIndex = INVALID_TEXTURE_INDEX; matData._pad2 = 0; if (matPSO->pass) { @@ -1457,11 +1457,16 @@ u32 MaterialCache::PreRegisterBindlessMaterial(dxRender_Visual* visual) matData.tessMethod = 0; matData.sssMapIndex = INVALID_TEXTURE_INDEX; matData.emissiveIntensity = 0.f; - matData._pad1 = 0; + matData.lmapIndex = INVALID_TEXTURE_INDEX; matData._pad2 = 0; if (visual->textureName.size() > 0) { - matData.detailScale = GetDetailScale(visual->textureName); + shared_str baseForScale = visual->textureName; + const char* p = baseForScale.c_str(); + if (const char* comma = strchr(p, ',')) + baseForScale = shared_str(xr_string(p, comma - p).c_str()); + if (baseForScale.size()) + matData.detailScale = GetDetailScale(baseForScale); } if (visual->shaderName.size() > 0) { @@ -1579,23 +1584,16 @@ u32 MaterialCache::PreRegisterParticleMaterial( if (!textureName.size() || !textureName[0]) return UINT32_MAX; - u32 colorMode = 0; bool hard = false; if (shaderName && shaderName[0]) { - if (strstr(shaderName, "particle_s-aadd") || strstr(shaderName, "s-aadd")) - colorMode = 3; - else if (strstr(shaderName, "particle_s-add") || strstr(shaderName, "s-add")) - colorMode = 2; - else if (strstr(shaderName, "particle_s-blend") || strstr(shaderName, "s-blend")) - colorMode = 1; if (strstr(shaderName, "particle_hard") || strstr(shaderName, "_hard")) hard = true; } string256 cacheKey; - xr_sprintf(cacheKey, "%s#%u#%u#%u#%s#%s", - textureName.c_str(), (u32)blendMode, colorMode, hard ? 1u : 0u, + xr_sprintf(cacheKey, "%s#%u#%u#%s#%s", + textureName.c_str(), (u32)blendMode, hard ? 1u : 0u, sixWayPosXYZ ? sixWayPosXYZ : "", sixWayNegXYZ ? sixWayNegXYZ : ""); shared_str key(cacheKey); @@ -1616,6 +1614,7 @@ u32 MaterialCache::PreRegisterParticleMaterial( matData.pbrIndex = INVALID_TEXTURE_INDEX; matData.detailScale = 1.0f; matData.sssMapIndex = INVALID_TEXTURE_INDEX; + matData.lmapIndex = INVALID_TEXTURE_INDEX; if (blendMode == kParticleBlendSet) { matData.alphaRef = 200.0f / 255.0f; @@ -1628,17 +1627,12 @@ u32 MaterialCache::PreRegisterParticleMaterial( } if (hard) matData.flags |= MAT_FLAG_PARTICLE_HARD; - if (colorMode >= 2) - { - matData.flags |= MAT_FLAG_EMISSIVE; - matData.emissiveIntensity = (colorMode >= 3) ? 6.0f : 3.5f; - } - else if (shaderName && (strstr(shaderName, "glow") || strstr(shaderName, "emissive"))) + if (shaderName && (strstr(shaderName, "glow") || strstr(shaderName, "emissive"))) { matData.flags |= MAT_FLAG_EMISSIVE; matData.emissiveIntensity = 4.0f; } - matData.tessMethod = colorMode; + matData.tessMethod = 0; matData.shaderVariant = 0; u32 materialID = materialBuffer.RegisterMaterial(matData); @@ -1723,9 +1717,42 @@ void MaterialCache::FinalizePendingMaterials(fg::RenderContext* ctx) if (!diffuseName.size() || !diffuseName[0]) continue; - // Classic texture lists are "s_base[,s_distort|lmap|...]". Particle - // registration already picks the correct slot; still strip commas here - // so any leftover "a,b" names cannot hit the loader as one path. + // Classic texture lists are "s_base[,lmap#N_1[,lmap#N_2]]". Slot [2] + // starting with "lmap" is the baked hemi map (uber_deffer USE_LM_HEMI). + if (!isWater) + { + xr_vector texSlots; + const char* p = diffuseName.c_str(); + while (p && *p) + { + while (*p == ' ') + ++p; + const char* start = p; + while (*p && *p != ',') + ++p; + if (p > start) + texSlots.emplace_back(start, p - start); + if (*p == ',') + ++p; + } + if (texSlots.size() > 2 && 0 == strncmp(texSlots[2].c_str(), "lmap", 4)) + { + resources::TextureHandle handle = texManager->LoadTexture(texSlots[2].c_str()); + if (handle.IsValid()) { + if (nvrhi::ITexture* nvrhiTex = texManager->GetNVRHITexture(handle)) { + u32 idx = backend->RegisterBindlessTexture(nvrhiTex); + if (idx != INVALID_TEXTURE_INDEX) { + matData.lmapIndex = idx; + matData.flags |= MAT_FLAG_HAS_LMAP; + updated = true; + } + } + } + } + } + + // Strip commas so the remaining single-texture loads below cannot hit + // the loader with "a,b" as one path. { const char* p = diffuseName.c_str(); const char* comma = strchr(p, ','); diff --git a/src/Layers/xrRender/Light_Render_Direct_ComputeXFS.cpp b/src/Layers/xrRender/Light_Render_Direct_ComputeXFS.cpp index 34bf58b8b8a..8eb60ed8e19 100644 --- a/src/Layers/xrRender/Light_Render_Direct_ComputeXFS.cpp +++ b/src/Layers/xrRender/Light_Render_Direct_ComputeXFS.cpp @@ -99,7 +99,7 @@ void CLight_Compute_XFORM_and_VIS::compute_xf_spot(light* L) else tan_shift = deg2rad(3.5f); - L->X.S.project.build_projection(L->cone + tan_shift, 1.f, L->virtual_size, L->range + EPS_S); + L->X.S.project.build_projection_stdz(L->cone + tan_shift, 1.f, L->virtual_size, L->range + EPS_S); L->X.S.combine.mul(L->X.S.project, L->X.S.view); } } // namespace xray::render::fg diff --git a/src/Layers/xrRender/Materials/MaterialSystem.cpp b/src/Layers/xrRender/Materials/MaterialSystem.cpp index 591aa7bec65..cfa13bddbdb 100644 --- a/src/Layers/xrRender/Materials/MaterialSystem.cpp +++ b/src/Layers/xrRender/Materials/MaterialSystem.cpp @@ -136,8 +136,12 @@ const MaterialSystem::MaterialInfo& MaterialSystem::GetMaterialInfo(const shared } break; } - if (blendInfo.strictB2F) + if (blendInfo.strictB2F && + blendInfo.mode != BlendMode::Opaque && + blendInfo.mode != BlendMode::AlphaTest) + { info.transparent = true; + } m_stats.materialsFromBlender++; } diff --git a/src/Layers/xrRender/PBRConverter/ONNXModelRunner.cpp b/src/Layers/xrRender/PBRConverter/ONNXModelRunner.cpp index 4527b7fff25..0dda0b871bd 100644 --- a/src/Layers/xrRender/PBRConverter/ONNXModelRunner.cpp +++ b/src/Layers/xrRender/PBRConverter/ONNXModelRunner.cpp @@ -260,43 +260,16 @@ bool ONNXModelRunner::LoadModel(const char* model_path, bool use_gpu, const char Msg("! [ONNXModelRunner] CoreML provider unavailable, falling back to CPU: %s", e.what()); } #else - // Use TensorRT V2 API for NVIDIA GPUs (has advanced options like build logging) - Ort::TensorRTProviderOptions trt_options{}; - - // Build options map - std::unordered_map trt_opts; - - // Basic settings - trt_opts["device_id"] = "0"; - trt_opts["trt_max_workspace_size"] = "2147483648"; // 2GB - trt_opts["trt_fp16_enable"] = "1"; // Enable FP16 tensor cores - - if (trt_profile_min && trt_profile_min[0]) trt_opts["trt_profile_min_shapes"] = trt_profile_min; - if (trt_profile_max && trt_profile_max[0]) trt_opts["trt_profile_max_shapes"] = trt_profile_max; - if (trt_profile_opt && trt_profile_opt[0]) trt_opts["trt_profile_opt_shapes"] = trt_profile_opt; - - // Engine caching - trt_opts["trt_engine_cache_enable"] = "1"; - if (trt_cache_path && trt_cache_path[0] != '\0') { - trt_opts["trt_engine_cache_path"] = trt_cache_path; + try { + OrtCUDAProviderOptions cuda_options{}; + cuda_options.device_id = 0; + session_options.AppendExecutionProvider_CUDA(cuda_options); + Msg("[ONNXModelRunner] CUDA GPU acceleration enabled"); + use_gpu_ = true; } - - trt_opts["trt_builder_optimization_level"] = "0"; - - trt_opts["trt_build_heuristics_enable"] = "1"; - - trt_opts["trt_timing_cache_enable"] = "1"; - if (trt_cache_path && trt_cache_path[0] != '\0') { - std::string cache_str(trt_cache_path); - auto last_sep = cache_str.find_last_of("\\/"); - std::string timing_path = (last_sep != std::string::npos) ? cache_str.substr(0, last_sep) : cache_str; - trt_opts["trt_timing_cache_path"] = timing_path; + catch (const Ort::Exception& e) { + Msg("! [ONNXModelRunner] CUDA provider unavailable, falling back to CPU: %s", e.what()); } - - trt_options.Update(trt_opts); - session_options.AppendExecutionProvider_TensorRT_V2(*trt_options); - Msg("[ONNXModelRunner] TensorRT V2 GPU acceleration enabled (FP16, profile shapes constrained)"); - use_gpu_ = true; #endif } diff --git a/src/Layers/xrRender/PBRConverter/PBRTextureConverter.cpp b/src/Layers/xrRender/PBRConverter/PBRTextureConverter.cpp index 358bc149ea9..4532b4b1b10 100644 --- a/src/Layers/xrRender/PBRConverter/PBRTextureConverter.cpp +++ b/src/Layers/xrRender/PBRConverter/PBRTextureConverter.cpp @@ -760,13 +760,12 @@ static void ZeroMetallicIfBlacklisted(ConvertedPBRTextures& converted, const xr_ static bool InitializeAIPipeline() { if (g_ai_pipeline) { - return true; // Already initialized + return true; } - // Initialize pipeline PBRPipelineConfig config; config.use_gpu = true; - config.verbose = true; // Set to true for debugging + config.verbose = true; g_ai_pipeline.reset(xr_new()); if (!g_ai_pipeline->Initialize(config)) { @@ -781,6 +780,15 @@ static bool InitializeAIPipeline() { return true; } +static void ShutdownAIPipeline() { + ScopeLock lock{ &g_ai_pipeline_mutex }; + if (!g_ai_pipeline) + return; + g_ai_pipeline.reset(); + g_ai_available = false; + Msg("[PBRTextureConverter] AI pipeline shut down (VRAM released)"); +} + static xr_vector NearestNeighborUpscaleRGBA( const xr_vector& src, u32 srcW, u32 srcH, u32 dstW, u32 dstH) { @@ -1369,6 +1377,13 @@ bool ConvertTexturesToPBR( PBRConversionStats& out_stats, ProgressCallback progress_callback) { + (void)params; + (void)progress_callback; + out_stats.textures_scanned = static_cast(inventory.assets.size()); + out_stats.textures_skipped = out_stats.textures_scanned; + return true; + +#if 0 if (inventory.assets.empty()) { return true; } @@ -1725,7 +1740,12 @@ bool ConvertTexturesToPBR( out_stats.textures_failed, out_stats.conversion_time_seconds); +#ifdef USE_AI_PBR + ShutdownAIPipeline(); +#endif + return out_stats.textures_failed == 0; +#endif } // ══════════════════════════════════════════════════════════ @@ -1968,47 +1988,13 @@ bool ConvertSingleTextureToPBR( const char* relative_path, const PBRConversionParams& params) { - if (!FileExists(root_alias, relative_path)) - { - Msg("! [PBRTextureConverter] Texture not found: %s/%s", root_alias, relative_path); - return false; - } - - xr_string rel(relative_path); - xr_string base_name = rel; - if (const auto dot = base_name.find_last_of('.'); dot != xr_string::npos) - base_name = base_name.substr(0, dot); - - xr_string pbr_path = base_name + "_pbr.dds"; - if (FileExists(root_alias, pbr_path.c_str())) - { - Msg("~ [PBRTextureConverter] %s already exists, skipping", pbr_path.c_str()); - return true; - } - - LegacyTextureAsset asset; - asset.base_name = base_name; - asset.diffuse.root_alias = root_alias; - asset.diffuse.relative_path = relative_path; - - TextureInventory inventory; - inventory.assets.push_back(std::move(asset)); - inventory.total_textures = 1; - - PBRConversionParams localParams = params; - localParams.output_root = root_alias; - - PBRConversionStats stats; - if (!ConvertTexturesToPBR(inventory, localParams, stats, nullptr)) - { - Msg("! [PBRTextureConverter] Conversion failed: %s", relative_path); - return false; - } - - ConsolidationStats consolidationStats; - ConsolidatePBRTextures(root_alias, consolidationStats, nullptr); - - return stats.textures_failed == 0; + (void)params; + xr_string pbr_path(relative_path); + if (const auto dot = pbr_path.find_last_of('.'); dot != xr_string::npos) + pbr_path = pbr_path.substr(0, dot) + "_pbr.dds"; + else + pbr_path += "_pbr.dds"; + return FileExists(root_alias, pbr_path.c_str()); } } // namespace xray::render::pbr diff --git a/src/Layers/xrRender/Profiler/StatsOverlay.cpp b/src/Layers/xrRender/Profiler/StatsOverlay.cpp index 8b9ffef3cb0..2fddfbb83bf 100644 --- a/src/Layers/xrRender/Profiler/StatsOverlay.cpp +++ b/src/Layers/xrRender/Profiler/StatsOverlay.cpp @@ -8,6 +8,9 @@ #include #include +extern ENGINE_API int ps_r_upscale; +extern ENGINE_API int ps_r_dlss_fg; + static bool FormatSubmitThreadLine(char* buf, size_t size) { IRenderBackend::SubmitThreadTimings t; @@ -124,6 +127,27 @@ void StatsOverlay::Render() float fps = cpuFrameTime > 0.0f ? 1000.0f / cpuFrameTime : 0.0f; ImGui::Text("Frame: %s (%.1f FPS)", FormatTime(cpuFrameTime), fps); + if (ps_r_upscale == 2 && ps_r_dlss_fg && GEnv.Backend) + { + static u64 s_lastPresents = 0; + static float s_accumTime = 0.f; + static float s_fgFps = 0.f; + const u64 presents = GEnv.Backend->GetPresentCount(); + if (s_lastPresents == 0 || presents < s_lastPresents) + { + s_lastPresents = presents; + s_accumTime = 0.f; + } + s_accumTime += Device.fTimeDeltaReal; + if (s_accumTime >= 0.5f) + { + s_fgFps = float(presents - s_lastPresents) / s_accumTime; + s_lastPresents = presents; + s_accumTime = 0.f; + } + if (s_fgFps > 0.f) + ImGui::Text("After FG: %.1f FPS presented", s_fgFps); + } if (!ideActive) { ImGui::TextDisabled("(Press editor key to interact)"); diff --git a/src/Layers/xrRender/RenderContext/RenderDevice.cpp b/src/Layers/xrRender/RenderContext/RenderDevice.cpp index b1ad79d4450..25a39098d06 100644 --- a/src/Layers/xrRender/RenderContext/RenderDevice.cpp +++ b/src/Layers/xrRender/RenderContext/RenderDevice.cpp @@ -3,6 +3,10 @@ #include "RenderContext.h" #include "../ResourceManager/FGResourceManager.h" #include "../Shaders/SlangCompiler.h" +#include "Layers/xrRender/Backend/VulkanBackend.h" +#include "xrEngine/IRenderBackend.h" +#include +#include namespace xray::render::fg { @@ -141,6 +145,31 @@ bool RenderDevice::InitializeFromBackend(IRenderBackend* backend) { m_modernResourceManager = xr_make_unique(this); Msg("* [RenderDevice] FGResourceManager initialized"); + if (auto* texMgr = m_modernResourceManager->GetTextureManager()) + { + u64 deviceLocal = 0; + if (GEnv.Backend && GEnv.Backend->GetAPI() == IRenderBackend::API::Vulkan) + { + if (auto* vk = dynamic_cast(GEnv.Backend)) + { + VkPhysicalDeviceMemoryProperties memProps{}; + vkGetPhysicalDeviceMemoryProperties(vk->GetVkPhysicalDevice(), &memProps); + for (u32 hi = 0; hi < memProps.memoryHeapCount; ++hi) + { + if (memProps.memoryHeaps[hi].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) + deviceLocal = std::max(deviceLocal, (u64)memProps.memoryHeaps[hi].size); + } + } + } + if (deviceLocal > 0) + { + const u64 budget = std::max(512ull * 1024ull * 1024ull, (deviceLocal * 45ull) / 100ull); + texMgr->SetMemoryBudget(budget); + Msg("* [RenderDevice] Texture budget %llu MB (device-local heap %llu MB)", + budget / (1024ull * 1024ull), deviceLocal / (1024ull * 1024ull)); + } + } + // Reserve initial capacity m_textures.reserve(256); m_buffers.reserve(512); @@ -207,9 +236,24 @@ TextureHandle RenderDevice::CreateTexture( nvrhi::TextureHandle nvrhiTexture = GetNativeDevice()->createTexture(nvrhiDesc); if (!nvrhiTexture) { - Msg("! [RenderDevice] Failed to create texture: %s (%ux%u depth=%u arr=%u mips=%u fmt=%d)", + const nvrhi::FormatInfo& fmtInfo = nvrhi::getFormatInfo(nvrhiDesc.format); + const u32 blockW = fmtInfo.blockSize > 1 ? fmtInfo.blockSize : 1; + const u32 blockH = fmtInfo.blockSize > 1 ? fmtInfo.blockSize : 1; + nvrhi::TextureDesc probeDesc = nvrhiDesc; + probeDesc.width = blockW; + probeDesc.height = blockH; + probeDesc.depth = 1; + probeDesc.arraySize = 1; + probeDesc.mipLevels = 1; + probeDesc.debugName = "CreateTexture_OomProbe"; + nvrhi::TextureHandle sameFmtProbe = GetNativeDevice()->createTexture(probeDesc); + const bool likelyOom = (bool)sameFmtProbe; + if (sameFmtProbe) + sameFmtProbe = nullptr; + Msg("! [RenderDevice] Failed to create texture: %s (%ux%u depth=%u arr=%u mips=%u fmt=%d) [%s]", desc.debugName.c_str(), desc.width, desc.height, desc.depth, desc.arraySize, - desc.mipLevels, (int)desc.format); + desc.mipLevels, (int)desc.format, + likelyOom ? "likely VRAM/device-memory exhaustion" : "desc/format rejected"); return TextureHandle{}; } diff --git a/src/Layers/xrRender/RenderContext/RenderDevice.h b/src/Layers/xrRender/RenderContext/RenderDevice.h index a3f7b2f29ad..ade4fc42b46 100644 --- a/src/Layers/xrRender/RenderContext/RenderDevice.h +++ b/src/Layers/xrRender/RenderContext/RenderDevice.h @@ -163,7 +163,7 @@ class RenderDevice { bool isVolatile = false; u32 maxVersions = 0; - static constexpr u32 VOLATILE_CB_MAX_VERSIONS = 16; + static constexpr u32 VOLATILE_CB_MAX_VERSIONS = 64; shared_str debugName; }; diff --git a/src/Layers/xrRender/ResourceManager.cpp b/src/Layers/xrRender/ResourceManager.cpp index 324b90c0084..bea9e3bb66b 100644 --- a/src/Layers/xrRender/ResourceManager.cpp +++ b/src/Layers/xrRender/ResourceManager.cpp @@ -122,18 +122,19 @@ bool CResourceManager::GetBlenderProperties(LPCSTR shaderName, BlenderProperties outProps.writesDepth = !b->oBlend.value; return true; } - // B_MODEL - model blender (oAREF, oBlend) if (cls == B_MODEL) { - auto* b = static_cast(B); + auto* b = static_cast(B); outProps.alphaRef = b->oAREF.value; - if (b->oBlend.value) + if (b->oBlend.value && b->oAREF.value < 16) outProps.blendMode = BlendMode::AlphaBlend; - else if (b->oAREF.value > 0) + else if (b->oBlend.value) outProps.blendMode = BlendMode::AlphaTest; else outProps.blendMode = BlendMode::Opaque; outProps.writesDepth = (outProps.blendMode != BlendMode::AlphaBlend); + if (outProps.blendMode == BlendMode::Opaque) + outProps.strictB2F = false; return true; } // B_MODEL_EbB - model with env (oBlend) diff --git a/src/Layers/xrRender/ResourceManager/TextureManager.cpp b/src/Layers/xrRender/ResourceManager/TextureManager.cpp index 103312e9682..c37526bcd38 100644 --- a/src/Layers/xrRender/ResourceManager/TextureManager.cpp +++ b/src/Layers/xrRender/ResourceManager/TextureManager.cpp @@ -342,29 +342,21 @@ TextureHandle TextureManager::CreateTexture( nvrhiDesc.clearValue = nvrhi::Color(0.0f); } - // Create texture meta.nvrhiTexture = m_device->GetNVRHIDevice()->createTexture(nvrhiDesc); - if (meta.nvrhiTexture) { - meta.state = TextureState::Resident; - meta.residentMips = desc.mipLevels; - meta.requestedMips = desc.mipLevels; - - // Calculate memory - meta.memoryUsed = desc.CalculateMemorySize(); - m_stats.texturesResident++; - m_stats.totalMemoryUsed += meta.memoryUsed; - - // Msg("~ [TextureManager] Created runtime texture '%s': %ux%ux%u, %.2f MB", - // desc.debugName.c_str(), - // desc.width, desc.height, desc.depth, - // meta.memoryUsed / (1024.0f * 1024.0f)); - } else { + if (!meta.nvrhiTexture) { Msg("! [TextureManager] Failed to create NVRHI texture '%s'", desc.debugName.c_str()); + FreeHandle(handle); + return TextureHandle{}; } + meta.state = TextureState::Resident; + meta.residentMips = desc.mipLevels; + meta.requestedMips = desc.mipLevels; + meta.memoryUsed = desc.CalculateMemorySize(); + m_stats.texturesResident++; + m_stats.totalMemoryUsed += meta.memoryUsed; m_stats.texturesTotal++; - return handle; } @@ -406,28 +398,22 @@ nvrhi::ITexture* TextureManager::GetNVRHITexture(TextureHandle handle) { TextureMetadata& meta = m_textures[handle.index]; - // ═══════════════════════════════════════════════════ - // AUTO-TOUCH FOR LRU - // ═══════════════════════════════════════════════════ - - meta.lastAccessTime = 0.0f; // Reset LRU timer - meta.accessCount++; - - // Mark video as active for this frame (enables decoding) if (meta.videoTextureData) { meta.videoActiveThisFrame = true; } - // ═══════════════════════════════════════════════════ - // AUTO-RELOAD IF EVICTED - // ═══════════════════════════════════════════════════ + if (meta.state == TextureState::Failed) + return nullptr; if (meta.state == TextureState::Unloaded || meta.state == TextureState::Evicted) { - // Msg("! [TextureManager] ⚠️ Accessing evicted texture: %s - reloading...", - // meta.filePath.c_str()); + if (meta.state == TextureState::Evicted && meta.lastAccessTime < 1.5f) + return nullptr; LoadTextureSync(handle); } + meta.lastAccessTime = 0.0f; + meta.accessCount++; + return meta.nvrhiTexture.Get(); } @@ -564,81 +550,62 @@ bool TextureManager::EnforceMemoryBudget(u64 requiredBytes) { bool TextureManager::EvictTextures(u64 bytesNeeded) { u64 bytesFreed = 0; - // ═══════════════════════════════════════════════════ - // BUILD EVICTION CANDIDATES (LRU + Priority) - // ═══════════════════════════════════════════════════ - struct EvictionCandidate { TextureHandle handle; - float score; // Higher = evict first + float score; u64 memoryUsed; bool operator<(const EvictionCandidate& other) const { - return score > other.score; // Descending order + return score > other.score; } }; - xr_vector candidates; - - for (u32 i = 0; i < m_textures.size(); i++) { - const TextureMetadata& meta = m_textures[i]; - - if (!meta.isAlive) continue; - if (!meta.CanEvict()) continue; // Check priority, refCount - - // Calculate eviction score - // Higher score = more likely to evict + auto scoreMeta = [](const TextureMetadata& meta) -> float { float score = 0.0f; - - // Factor 1: Time since last access (LRU) score += meta.lastAccessTime * 10.0f; - - // Factor 2: Priority (low priority = evict first) score += (float)meta.priority * 100.0f; - - // Factor 3: Access count (less used = evict first) score -= (float)meta.accessCount * 0.1f; - - // Factor 4: Memory used (larger = prefer to evict for space) score += (float)meta.memoryUsed / (1024.0f * 1024.0f); + return score; + }; - EvictionCandidate candidate; - candidate.handle = TextureHandle(i, meta.generation); - candidate.score = score; - candidate.memoryUsed = meta.memoryUsed; - - candidates.push_back(candidate); - } - - // Sort by score - std::sort(candidates.begin(), candidates.end()); - - // Msg("! [TextureManager] Found %u eviction candidates", candidates.size()); - - // ═══════════════════════════════════════════════════ - // EVICT UNTIL WE HAVE ENOUGH SPACE - // ═══════════════════════════════════════════════════ + auto collectAndEvict = [&](bool force) { + xr_vector candidates; + for (u32 i = 0; i < m_textures.size(); i++) { + const TextureMetadata& meta = m_textures[i]; + if (!meta.isAlive) continue; + if (force) { + if (!meta.CanForceEvict()) continue; + } else { + if (!meta.CanEvict()) continue; + } - for (const auto& candidate : candidates) { - if (bytesFreed >= bytesNeeded) { - break; // Freed enough + EvictionCandidate candidate; + candidate.handle = TextureHandle(i, meta.generation); + candidate.score = scoreMeta(meta); + candidate.memoryUsed = meta.memoryUsed; + candidates.push_back(candidate); } - const TextureMetadata* meta = GetMetadata(candidate.handle); + std::sort(candidates.begin(), candidates.end()); - // Msg("! Evicting: %s (score=%.2f, %llu KB)", - // meta->filePath.c_str(), - // candidate.score, - // candidate.memoryUsed / 1024); + for (const auto& candidate : candidates) { + if (bytesFreed >= bytesNeeded) + break; - EvictTextureInternal(candidate.handle); + const TextureMetadata* meta = GetMetadata(candidate.handle); + if (!meta || meta->state != TextureState::Resident) + continue; - bytesFreed += candidate.memoryUsed; - } + const u64 before = meta->memoryUsed; + EvictTextureInternal(candidate.handle); + bytesFreed += before; + } + }; - // Msg("! [TextureManager] Freed %llu MB (needed %llu MB)", - // bytesFreed / (1024 * 1024), - // bytesNeeded / (1024 * 1024)); + collectAndEvict(false); + if (bytesFreed < bytesNeeded) + collectAndEvict(true); return bytesFreed >= bytesNeeded; } @@ -649,24 +616,19 @@ void TextureManager::EvictTextureInternal(TextureHandle handle) { TextureMetadata& meta = m_textures[handle.index]; if (meta.state != TextureState::Resident) { - return; // Already evicted + return; } - // Release NVRHI texture if (meta.nvrhiTexture) { - // NVRHI will destroy when ref count reaches zero meta.nvrhiTexture = nullptr; } - // Update state meta.state = TextureState::Evicted; meta.residentMips = 0; + meta.lastAccessTime = 0.0f; - // Update memory tracking m_memoryUsed -= meta.memoryUsed; meta.memoryUsed = 0; - - // Msg("! [TextureManager] Evicted: %s", meta.filePath.c_str()); } // ═══════════════════════════════════════════════════ @@ -768,12 +730,9 @@ void TextureManager::LoadTextureSync(TextureHandle handle) { TextureMetadata& meta = m_textures[handle.index]; - // Already loaded? - if (meta.state == TextureState::Resident) { + if (meta.state == TextureState::Resident || meta.state == TextureState::Failed) return; - } - // Mark as loading meta.state = TextureState::Loading; // ═══════════════════════════════════════════════════ @@ -786,7 +745,7 @@ void TextureManager::LoadTextureSync(TextureHandle handle) { DDSData ddsData; if (!DDSLoader::LoadFromFile(meta.filePath.c_str(), ddsData)) { Msg("! [TextureManager] Failed to load texture: %s", meta.filePath.c_str()); - meta.state = TextureState::Unloaded; + meta.state = TextureState::Failed; return; } @@ -817,12 +776,13 @@ void TextureManager::LoadTextureSync(TextureHandle handle) { u64 requiredMemory = ddsData.totalDataSize; if (!EnforceMemoryBudget(requiredMemory)) { - Msg("! [TextureManager] ❌ Cannot load %s - out of memory! (need %llu MB, have %llu / %llu MB used)", + Msg("! [TextureManager] Cannot load %s - out of memory! (need %llu MB, have %llu / %llu MB used)", meta.filePath.c_str(), requiredMemory / (1024 * 1024), m_memoryUsed / (1024 * 1024), m_memoryBudget / (1024 * 1024)); - meta.state = TextureState::Unloaded; + meta.state = TextureState::Evicted; + meta.lastAccessTime = 0.0f; return; } @@ -852,11 +812,23 @@ void TextureManager::LoadTextureSync(TextureHandle handle) { break; } - // Create texture (no initial data - we'll upload separately) fg::TextureHandle deviceHandle = m_device->CreateTexture(deviceDesc, nullptr); if (!deviceHandle.IsValid()) { - Msg("! [TextureManager] Failed to create NVRHI texture: %s", meta.filePath.c_str()); - meta.state = TextureState::Unloaded; + const u64 evictTarget = std::max(requiredMemory, 64ull * 1024ull * 1024ull); + if (EvictTextures(evictTarget)) { + if (nvrhi::IDevice* nvDev = m_device->GetNVRHIDevice()) { + nvDev->runGarbageCollection(); + } + deviceHandle = m_device->CreateTexture(deviceDesc, nullptr); + } + } + if (!deviceHandle.IsValid()) { + Msg("! [TextureManager] Failed to create NVRHI texture: %s (used %llu / %llu MB)", + meta.filePath.c_str(), + m_memoryUsed / (1024 * 1024), + m_memoryBudget / (1024 * 1024)); + meta.state = TextureState::Evicted; + meta.lastAccessTime = 0.0f; return; } @@ -1054,6 +1026,7 @@ const char* TextureStateToString(TextureState state) { case TextureState::Resident: return "Resident"; case TextureState::Evicting: return "Evicting"; case TextureState::Evicted: return "Evicted"; + case TextureState::Failed: return "Failed"; default: return "Unknown"; } } diff --git a/src/Layers/xrRender/ResourceManager/TextureManager.h b/src/Layers/xrRender/ResourceManager/TextureManager.h index 69865012800..9fc2200027e 100644 --- a/src/Layers/xrRender/ResourceManager/TextureManager.h +++ b/src/Layers/xrRender/ResourceManager/TextureManager.h @@ -140,7 +140,13 @@ struct TextureMetadata { } bool CanEvict() const { - return priority >= TexturePriority::Low && + return priority >= TexturePriority::Medium && + refCount == 0 && + state == TextureState::Resident; + } + + bool CanForceEvict() const { + return priority >= TexturePriority::High && refCount == 0 && state == TextureState::Resident; } diff --git a/src/Layers/xrRender/fgEnvironmentRender.cpp b/src/Layers/xrRender/fgEnvironmentRender.cpp index 6cdbf665dfc..bc762c02b0f 100644 --- a/src/Layers/xrRender/fgEnvironmentRender.cpp +++ b/src/Layers/xrRender/fgEnvironmentRender.cpp @@ -355,12 +355,25 @@ void FGEnvironmentRender::DrawSky(nvrhi::ICommandList* cmdList, nvrhi::IFramebuf ? renderDevice->GetFGResourceManager()->GetTextureManager() : nullptr; if (texManager && environment->Current[0] && environment->Current[1]) { + static shared_str s_skyName0; + static shared_str s_skyName1; + static nvrhi::ITexture* s_skyTex0 = nullptr; + static nvrhi::ITexture* s_skyTex1 = nullptr; + const shared_str& skyName0 = environment->Current[0]->sky_texture_name; const shared_str& skyName1 = environment->Current[1]->sky_texture_name; - if (skyName0.size()) - sky0Tex = texManager->GetNVRHITexture(texManager->LoadTexture(skyName0.c_str())); - if (skyName1.size()) - sky1Tex = texManager->GetNVRHITexture(texManager->LoadTexture(skyName1.c_str())); + if (skyName0.size() && skyName0 != s_skyName0) + { + s_skyName0 = skyName0; + s_skyTex0 = texManager->GetNVRHITexture(texManager->LoadTexture(skyName0.c_str())); + } + if (skyName1.size() && skyName1 != s_skyName1) + { + s_skyName1 = skyName1; + s_skyTex1 = texManager->GetNVRHITexture(texManager->LoadTexture(skyName1.c_str())); + } + sky0Tex = s_skyTex0; + sky1Tex = s_skyTex1; } if (!sky0Tex) sky0Tex = m_skyPlaceholderCube.Get(); if (!sky1Tex) sky1Tex = m_skyPlaceholderCube.Get(); @@ -582,12 +595,25 @@ void FGEnvironmentRender::DrawClouds(nvrhi::ICommandList* cmdList, nvrhi::IFrame ? renderDevice->GetFGResourceManager()->GetTextureManager() : nullptr; if (texManager && environment->Current[0] && environment->Current[1]) { + static shared_str s_cloudsName0; + static shared_str s_cloudsName1; + static nvrhi::ITexture* s_cloudsTex0 = nullptr; + static nvrhi::ITexture* s_cloudsTex1 = nullptr; + const shared_str& name0 = environment->Current[0]->clouds_texture_name; const shared_str& name1 = environment->Current[1]->clouds_texture_name; - if (name0.size()) - clouds0Tex = texManager->GetNVRHITexture(texManager->LoadTexture(name0.c_str())); - if (name1.size()) - clouds1Tex = texManager->GetNVRHITexture(texManager->LoadTexture(name1.c_str())); + if (name0.size() && name0 != s_cloudsName0) + { + s_cloudsName0 = name0; + s_cloudsTex0 = texManager->GetNVRHITexture(texManager->LoadTexture(name0.c_str())); + } + if (name1.size() && name1 != s_cloudsName1) + { + s_cloudsName1 = name1; + s_cloudsTex1 = texManager->GetNVRHITexture(texManager->LoadTexture(name1.c_str())); + } + clouds0Tex = s_cloudsTex0; + clouds1Tex = s_cloudsTex1; } if (!clouds0Tex) clouds0Tex = m_cloudsPlaceholderTex.Get(); if (!clouds1Tex) clouds1Tex = m_cloudsPlaceholderTex.Get(); @@ -679,7 +705,7 @@ void FGEnvironmentRender::InitSunResources() auto& cache = framegraph::GetPassResourceCache(); m_sunBindingLayout = cache.GetOrCreateBindingLayoutFromReflection( - "FGEnv_Sun", *vsResult.reflection, *psResult.reflection, m_device); + "FGEnv_Sun_v2", *vsResult.reflection, *psResult.reflection, m_device); R_ASSERT(m_sunBindingLayout); nvrhi::VertexAttributeDesc attribs[] = { @@ -703,13 +729,15 @@ void FGEnvironmentRender::InitSunResources() nvrhi::RenderState renderState; renderState.blendState.targets[0].setBlendEnable(true); - renderState.blendState.targets[0].setSrcBlend(nvrhi::BlendFactor::One); + renderState.blendState.targets[0].setSrcBlend(nvrhi::BlendFactor::SrcAlpha); renderState.blendState.targets[0].setDestBlend(nvrhi::BlendFactor::One); renderState.blendState.targets[0].setBlendOp(nvrhi::BlendOp::Add); - // Depth-test so mountains/objects occlude the disc (classic). No depth write. + renderState.blendState.targets[0].setSrcBlendAlpha(nvrhi::BlendFactor::One); + renderState.blendState.targets[0].setDestBlendAlpha(nvrhi::BlendFactor::One); + renderState.blendState.targets[0].setBlendOpAlpha(nvrhi::BlendOp::Add); renderState.depthStencilState.setDepthTestEnable(true); renderState.depthStencilState.setDepthWriteEnable(false); - renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::LessOrEqual); + renderState.depthStencilState.setDepthFunc(nvrhi::ComparisonFunc::GreaterOrEqual); renderState.rasterState.setCullMode(nvrhi::RasterCullMode::None); nvrhi::GraphicsPipelineDesc pipelineDesc; @@ -724,7 +752,7 @@ void FGEnvironmentRender::InitSunResources() fbInfo.colorFormats.push_back(nvrhi::Format::RGBA16_FLOAT); fbInfo.depthFormat = nvrhi::Format::D32; - m_sunPipeline = cache.GetOrCreatePipeline("FGEnv_Sun", pipelineDesc, fbInfo, m_device); + m_sunPipeline = cache.GetOrCreatePipeline("FGEnv_Sun_v3", pipelineDesc, fbInfo, m_device); R_ASSERT(m_sunPipeline); m_sunInitialized = true; @@ -758,10 +786,11 @@ void FGEnvironmentRender::DrawSun(nvrhi::ICommandList* cmdList, nvrhi::IFramebuf return; float fDistance = env.far_plane * 0.75f; + const float fDotClamped = std::max(fDot, 0.25f); Fvector vecLight; vecLight.set(vSunDir); - vecLight.mul(fDistance / fDot); + vecLight.mul(fDistance / fDotClamped); vecLight.add(Device.vCameraPosition); Fvector vecX, vecY; @@ -816,7 +845,10 @@ void FGEnvironmentRender::DrawSun(nvrhi::ICommandList* cmdList, nvrhi::IFramebuf auto* renderDevice = fgRenderer->GetRenderDevice(); auto dynamicCBBuffer = cache.GetOrCreateVolatileCB( - "Frame", "DynamicTransforms", sizeof(passes::DynamicTransforms), renderDevice); + "FGEnv_Sun", "DynamicTransforms", sizeof(passes::DynamicTransforms), renderDevice); + passes::DynamicTransforms dynamicCB{}; + passes::FillDynamicTransforms(dynamicCB, Fidentity); + cmdList->writeBuffer(dynamicCBBuffer, &dynamicCB, sizeof(dynamicCB)); nvrhi::ITexture* sunTex = nullptr; const shared_str& sunTexName = flareDesc->m_Source.texture; diff --git a/src/Layers/xrRender/fgLensFlareRender.cpp b/src/Layers/xrRender/fgLensFlareRender.cpp index e95d9917302..8bbfcebe19c 100644 --- a/src/Layers/xrRender/fgLensFlareRender.cpp +++ b/src/Layers/xrRender/fgLensFlareRender.cpp @@ -275,12 +275,14 @@ void FGLensFlareRender::Render(CLensFlare& owner, BOOL bSun, BOOL bFlares, BOOL Device.mFullTransform.transform(clip, owner.vecLight); if (clip.w > 0.f) { + const float rw = float(passes::GetRenderWidth()); + const float rh = float(passes::GetRenderHeight()); m_sunValid = true; - m_sunPosPx.set((clip.x * 0.5f + 0.5f) * float(Device.dwWidth), - (1.f - (clip.y * 0.5f + 0.5f)) * float(Device.dwHeight)); + m_sunPosPx.set((clip.x * 0.5f + 0.5f) * rw, + (1.f - (clip.y * 0.5f + 0.5f)) * rh); const float radius = owner.m_Current->m_Flags.is(CLensFlareDescriptor::flSource) ? owner.m_Current->m_Source.fRadius : 0.15f; - m_sunRadiusPx = radius * 0.25f * float(Device.dwHeight) / tanf(deg2rad(Device.fFOV) * 0.5f); + m_sunRadiusPx = radius * 0.25f * rh / tanf(deg2rad(Device.fFOV) * 0.5f); clamp(m_sunRadiusPx, 4.f, 96.f); } diff --git a/src/Layers/xrRender/r4_rendertarget.cpp b/src/Layers/xrRender/r4_rendertarget.cpp index 0221e753dca..c9a71f950ec 100644 --- a/src/Layers/xrRender/r4_rendertarget.cpp +++ b/src/Layers/xrRender/r4_rendertarget.cpp @@ -69,7 +69,22 @@ Ivector vpack(const Fvector& src) CRenderTarget::CRenderTarget() { - return; + im_noise_time = 1.f; + im_noise_shift_w = 0; + im_noise_shift_h = 0; + param_blur = 0.f; + param_gray = 0.f; + param_duality_h = 0.f; + param_duality_v = 0.f; + param_noise = 0.f; + param_noise_scale = 1.f; + param_noise_fps = 25.f; + param_color_base = color_rgba(127, 127, 127, 0); + param_color_gray = color_rgba(85, 85, 85, 0); + param_color_add.set(0.f, 0.f, 0.f); + param_color_map_influence = 0.f; + param_color_map_interpolate = 0.f; + m_bHasActiveVolumetric = false; } CRenderTarget::~CRenderTarget() {} diff --git a/src/Layers/xrRender/r_FrameGraphRenderer.cpp b/src/Layers/xrRender/r_FrameGraphRenderer.cpp index 1237648a26e..54385439d5f 100644 --- a/src/Layers/xrRender/r_FrameGraphRenderer.cpp +++ b/src/Layers/xrRender/r_FrameGraphRenderer.cpp @@ -597,7 +597,7 @@ void FrameGraphRenderer::Render() { if (clm.IsReady() && clm.GetLightCount() > 0) { float zNear = VIEWPORT_NEAR; float zFar = g_pGamePersistent->Environment().CurrentEnv.far_plane; - auto ccb = clm.BuildClusterCB(Device.dwWidth, Device.dwHeight, zNear, zFar); + auto ccb = clm.BuildClusterCB(passes::GetRenderWidth(), passes::GetRenderHeight(), zNear, zFar); staticGlobalsData.cluster_params.set(ccb.gridDims.x, ccb.gridDims.y, ccb.gridDims.z, ccb.gridDims.w); staticGlobalsData.cluster_scales.set(ccb.depthParams.x, ccb.depthParams.y, ccb.depthParams.z, ccb.depthParams.w); } @@ -669,6 +669,7 @@ void FrameGraphRenderer::RenderMenu() { const u32 width = Device.dwWidth; const u32 height = Device.dwHeight; + passes::SetRenderResolution(width, height); nvrhi::ITexture* backbufferTexture = GEnv.Backend->GetBackBuffer(); framegraph::VirtualResourceHandle backbufferHandle; @@ -742,7 +743,7 @@ void FrameGraphRenderer::RenderStatsOverlay() const auto& batches = m_geometryCollector->GetBatches(); stats.totalBatches = static_cast(batches.size()); - xr_set uniqueSkeletons; + xr_set uniqueSkeletons; for (const auto& batch : batches) { @@ -754,25 +755,8 @@ void FrameGraphRenderer::RenderStatsOverlay() stats.skinnedBatches++; stats.skinnedTriangles += triangles; - if (batch.renderable) - { - IRenderVisual* rootVisual = batch.renderable->GetRenderData().visual; - if (rootVisual && uniqueSkeletons.find(rootVisual) == uniqueSkeletons.end()) - { - uniqueSkeletons.insert(rootVisual); - stats.skinnedMeshes++; - - // Get bone count from kinematics - IKinematics* K = rootVisual->dcast_PKinematics(); - if (K) - { - u32 boneCount = K->LL_BoneCount(); - stats.totalBones += boneCount; - if (boneCount > stats.maxBonesPerMesh) - stats.maxBonesPerMesh = boneCount; - } - } - } + if (batch.renderable && uniqueSkeletons.insert(batch.renderable).second) + stats.skinnedMeshes++; } else if (batch.isTerrain) { @@ -1033,8 +1017,38 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { passes::ApplyTAAJitter(); passes::ResetMdiDrawCounters(); - if ((ps_r_upscale != 0 || ps_r_dlss != 0) && !g_upscaleBackend) - g_upscaleBackend.reset(fg::CreateUpscaleBackendAuto()); + { + if (ps_r_dlss != 0 && ps_r_upscale == 0) + ps_r_upscale = 2; + const int reqUpscale = ps_r_upscale; + const fg::UpscaleBackendType wantType = + (reqUpscale == 2) ? fg::UpscaleBackendType::DLSS : + (reqUpscale == 1) ? fg::UpscaleBackendType::FSR : + fg::UpscaleBackendType::None; + const fg::UpscaleBackendType haveType = + g_upscaleBackend ? g_upscaleBackend->GetType() : fg::UpscaleBackendType::None; + const bool haveOk = g_upscaleBackend && g_upscaleBackend->IsAvailable(); + const bool needSwitch = + (wantType == fg::UpscaleBackendType::None && haveType != fg::UpscaleBackendType::None) || + (wantType != fg::UpscaleBackendType::None && (!haveOk || haveType != wantType)); + if (needSwitch) + { + if (GEnv.Backend) + GEnv.Backend->WaitForIdle(); + if (g_upscaleBackend) + { + if (haveType == fg::UpscaleBackendType::DLSS && + wantType != fg::UpscaleBackendType::DLSS) + fg::Streamline_ReleaseFeatures(); + g_upscaleBackend->Shutdown(); + g_upscaleBackend.reset(); + } + if (wantType != fg::UpscaleBackendType::None) + g_upscaleBackend.reset(fg::CreateUpscaleBackendAuto()); + m_hasPrevFrameData = false; + } + } + const bool upscaleBackendOk = g_upscaleBackend && g_upscaleBackend->IsAvailable(); { static int s_denoiseCvar = -1; @@ -1044,7 +1058,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { static int s_rrAvail = -1; static u32 s_denoiseW = 0; static u32 s_denoiseH = 0; - UpdateUpscaleState(g_upscaleState, Device.dwWidth, Device.dwHeight); + UpdateUpscaleState(g_upscaleState, Device.dwWidth, Device.dwHeight, upscaleBackendOk); const u32 denoiseW = g_upscaleState.renderWidth ? g_upscaleState.renderWidth : Device.dwWidth; const u32 denoiseH = g_upscaleState.renderHeight ? g_upscaleState.renderHeight : Device.dwHeight; const int rrAvail = fg::Streamline_IsRRAvailable() ? 1 : 0; @@ -1056,6 +1070,8 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { s_rrAvail != rrAvail || (g_denoiseBackend && (s_denoiseW != denoiseW || s_denoiseH != denoiseH)); if (denoiseDirty && g_denoiseBackend) { + if (GEnv.Backend) + GEnv.Backend->WaitForIdle(); g_denoiseBackend->Shutdown(); g_denoiseBackend.reset(); } @@ -1072,28 +1088,44 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { s_denoiseH = denoiseH; } - // Recompute after backend auto-fallback may clear r_upscale - UpdateUpscaleState(g_upscaleState, Device.dwWidth, Device.dwHeight); + UpdateUpscaleState(g_upscaleState, Device.dwWidth, Device.dwHeight, upscaleBackendOk); g_upscaleState.jitterX = passes::g_taa_jitter_px; g_upscaleState.jitterY = passes::g_taa_jitter_py; g_upscaleState.prevJitterX = passes::g_taa_jitter_prev_px; g_upscaleState.prevJitterY = passes::g_taa_jitter_prev_py; - g_upscaleState.resetHistory = !m_hasPrevFrameData; + g_upscaleState.resetHistory = !m_hasPrevFrameData || fg::Streamline_ConsumeFeatureReset(); const u32 displayWidth = g_upscaleState.displayWidth; const u32 displayHeight = g_upscaleState.displayHeight; const u32 width = g_upscaleState.renderWidth; const u32 height = g_upscaleState.renderHeight; + passes::SetRenderResolution(width, height); + static bool s_rtGiWasOn = false; + if (s_rtGiWasOn && !ps_r_rt_gi) + passes::ShutdownReSTIRGI(m_blackboard->get_or_add()); + s_rtGiWasOn = ps_r_rt_gi; + + bool rtgiActive = IsRTGIActive(); + if (rtgiActive) + { + auto& rtgiState = m_blackboard->get_or_add(); + if (!passes::EnsureReSTIRGITextures(m_device, rtgiState, width, height)) + rtgiActive = false; + } + const bool rtGiWanted = + ps_r_rt_gi && !passes::IsRTGIAllocFailed() && + m_rtAccelMgr && m_rtAccelMgr->IsSupported(); const bool rtLightingWanted = - (ps_r_rt_gi || ps_r_path_tracer) && m_rtAccelMgr && m_rtAccelMgr->IsSupported(); - const bool rtgiActive = IsRTGIActive(); + (ps_r_path_tracer || rtGiWanted) && + m_rtAccelMgr && m_rtAccelMgr->IsSupported(); const bool wantSunShaftCsm = ps_r_sun_shafts != 0; + const bool wantSkinnedCsm = ps_r_skinned_shadows != 0; if (rtLightingWanted) { auto& localShadow = m_blackboard->get_or_add(); if (localShadow.initialized) passes::ShutdownLocalShadowPass(m_device, localShadow); - if (!wantSunShaftCsm) + if (!wantSunShaftCsm && !wantSkinnedCsm) { auto& shadowState = m_blackboard->get_or_add(); if (shadowState.initialized) @@ -1166,7 +1198,18 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { for (int i = 0; i < 2; i++) { desc.debugName = (i == 0) ? "WorldPos_A" : "WorldPos_B"; m_worldPos[i] = nvDevice->createTexture(desc); + if (!m_worldPos[i]) + Msg("! [FG] Failed to create %s (%ux%u RGBA32_FLOAT)", desc.debugName.c_str(), width, height); + } + } + + if (!m_worldPos[writeIdx]) { + static bool s_worldPosFailLogged = false; + if (!s_worldPosFailLogged) { + Msg("! [FG] rt_WorldPos unavailable — aborting frame graph build (further spam suppressed)"); + s_worldPosFailLogged = true; } + return; } framegraph::ResourceDesc normalImportDesc; @@ -1625,8 +1668,9 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { // ═══════════════════════════════════════════════════════ // CASCADED SHADOW MAPS (sun CSM, before Forward) + // Keep CSM under RTGI when skinned casters or sunshafts need it. // ═══════════════════════════════════════════════════════ - if (!rtLightingWanted) + if (!rtLightingWanted || wantSunShaftCsm || wantSkinnedCsm) { Fvector sunDir(0.3f, 0.8f, 0.2f); if (g_pGamePersistent) @@ -1680,6 +1724,14 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { bindlessConfig.localShadowHandle = localOut.atlas; m_framegraph->GetRTRegistry().RegisterRT("rt_LocalShadowAtlas", localOut.atlas); } + else + { + clmLocal.ClearLocalShadowAssignments(); + } + } + else + { + clmLocal.ClearLocalShadowAssignments(); } } @@ -2112,6 +2164,20 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { width, height, m_blackboard->get_or_add() ); + if (motionOutput.motionVectors.is_valid()) { + motionOutput.motionVectors = passes::setupSkinnedVelocityPass( + *m_framegraph, m_device, + motionOutput.motionVectors, + rtgiGuideDepth, + m_geometryCollector.get(), + m_hudBatches.empty() ? nullptr : &m_hudBatches, + m_gpuCullingManager.get(), + &m_blackboard->get_or_add(), + passes::g_taa_unjittered_full_transform, + m_prevViewProj, + width, height + ); + } } // Wet G-buffer snapshot before RTGI (particles deferred until after denoise). @@ -2124,7 +2190,11 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { // ═══════════════════════════════════════════════════════ // DYNAMIC BLAS BUILD (before ReSTIR / path tracer) // ═══════════════════════════════════════════════════════ - bool needsRT = (ps_r_path_tracer || ps_r_rt_gi) && m_rtAccelMgr && m_rtAccelMgr->IsSupported(); + bool needsRT = rtLightingWanted; + static bool s_rtWasWanted = false; + if (s_rtWasWanted && !needsRT && m_rtAccelMgr) + m_rtAccelMgr->ReleaseAccelerationStructures(); + s_rtWasWanted = needsRT; if (needsRT && m_rtAccelMgr->IsReady()) { bool ptNeedsBLAS = ps_r_path_tracer && m_ptSampleIndex == 0; @@ -2137,6 +2207,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { FGDetailManager* detailMgr; const GeometryCollector* geometry; const xr_vector* hudBatches; + const xr_vector* worldParticleBatches; }; m_framegraph->addCallbackPass( @@ -2149,6 +2220,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { data.detailMgr = m_detailManager.get(); data.geometry = m_geometryCollector.get(); data.hudBatches = &m_hudBatches; + data.worldParticleBatches = &m_worldParticleBatches; }, [](const DynamicBLASData& data, const framegraph::FrameGraph&, fg::RenderContext* ctx) { nvrhi::ICommandList* cmdList = ctx->GetCommandList(); @@ -2182,6 +2254,8 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { data.accelMgr->BuildSkinnedBLAS(cmdList, data.gpuCulling, worldSkinned, hudSkinned); data.accelMgr->BuildGrassBLAS(cmdList, data.detailMgr); + if (data.worldParticleBatches) + data.accelMgr->BuildParticleBLAS(cmdList, *data.worldParticleBatches); data.accelMgr->RebuildDynamic(cmdList, data.gpuCulling); } ); @@ -2193,16 +2267,22 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { // ═══════════════════════════════════════════════════════ if (rtgiActive) { - const bool useDlssRR = ps_r_dlss_rr != 0 && ps_r_upscale == 2 && fg::Streamline_IsRRAvailable(); const bool vendorDenoise = fg::IsVendorDenoiseActive(g_denoiseBackend.get()); - const bool useVendorDenoise = vendorDenoise && ps_r_nrd_apply != 0 && !useDlssRR; - const bool skipInTreeDenoise = useVendorDenoise || useDlssRR; + const bool useVendorDenoise = vendorDenoise && ps_r_nrd_apply != 0; + const bool skipInTreeDenoise = useVendorDenoise; auto sceneColorBeforeRtgi = sceneColor; auto& rtgiState = m_blackboard->get_or_add(); { auto& tpState = m_blackboard->get_or_add(); rtgiState.waterUnderWorldPos = tpState.waterSceneWorldPos.Get(); } + framegraph::VirtualResourceHandle particleOpacityRT; + if (rtgiGuideWorldPos.is_valid() && !m_worldParticleBatches.empty()) { + auto& occState = m_blackboard->get_or_add(); + particleOpacityRT = passes::setupParticleOcclusionPass( + *m_framegraph, m_device, rtgiGuideWorldPos, + &m_worldParticleBatches, width, height, occState); + } auto rtgiOutput = passes::setupReSTIRGIPass( *m_framegraph, m_device, m_rtAccelMgr.get(), rtgiGuideDepth, rtgiGuideNormal, @@ -2216,7 +2296,8 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { rtgiState, m_hasPrevFrameData, skipInTreeDenoise, transparentOutputs.worldPos, - useDlssRR + false, + particleOpacityRT ); sceneColor = rtgiOutput.sceneColor; @@ -2292,7 +2373,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { in.jitterY = passes::g_taa_jitter_py; in.jitterPrevX = passes::g_taa_jitter_prev_px; in.jitterPrevY = passes::g_taa_jitter_prev_py; - in.nearZ = 0.2f; + in.nearZ = RENDER_VIEWPORT_NEAR; in.farZ = g_pGamePersistent ? g_pGamePersistent->Environment().CurrentEnv.far_plane : 500.f; copyMat(in.viewToClip, Device.mProject); copyMat(in.viewToClipPrev, Device.mProjectSaved); @@ -2391,7 +2472,8 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { hizOutput.mipLevels, m_hasPrevFrameData ? &m_prevViewProj : nullptr, particlePrevDepth, - &m_blackboard->get_or_add() + &m_blackboard->get_or_add(), + transparentOutputs.distortion ); auto trailChainLayout = particleOutputs.layout; @@ -2619,22 +2701,33 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { ? rtgiGuideDepth : (transparentOutputs.depth.is_valid() ? transparentOutputs.depth : depthBuffer); - if (!rtgiActive) { - auto shaftDepth = occludeDepth; - auto shaftWorldPos = gbufferForWet.worldPos.is_valid() ? gbufferForWet.worldPos : worldPosBuffer; - sceneColor = passes::setupSunShaftsPass( - *m_framegraph, - m_device, - sceneColor, - shaftDepth, - shaftWorldPos, - bindlessConfig.shadowMapArray, - bindlessConfig.shadowMapHandle, - width, - height, - m_blackboard->get_or_add(), - bindlessConfig.shadowCascades); + bool useRasterShafts = !rtgiActive; + if (rtgiActive) + { + auto& rtgiState = m_blackboard->get_or_add(); + if (rtgiState.volAllocFailed) + useRasterShafts = true; + } + if (useRasterShafts) + { + auto shaftDepth = occludeDepth; + auto shaftWorldPos = (rtgiActive && rtgiGuideWorldPos.is_valid()) + ? rtgiGuideWorldPos + : (gbufferForWet.worldPos.is_valid() ? gbufferForWet.worldPos : worldPosBuffer); + sceneColor = passes::setupSunShaftsPass( + *m_framegraph, + m_device, + sceneColor, + shaftDepth, + shaftWorldPos, + bindlessConfig.shadowMapArray, + bindlessConfig.shadowMapHandle, + width, + height, + m_blackboard->get_or_add(), + bindlessConfig.shadowCascades); + } } sceneColor = passes::setupSunPass( @@ -2690,36 +2783,6 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { &volLighting); } - if (!rtgiActive && !m_hudBatches.empty()) - { - sceneColor = passes::setupHudOverlayPass( - *m_framegraph, - m_device, - sceneColor, - transparentOutputs.depth, - transparentOutputs.normal, - transparentOutputs.baseColor, - transparentOutputs.worldPos, - &m_hudBatches, - m_materialCache.get(), - width, - height, - m_gpuCullingManager.get(), - &m_blackboard->get_or_add(), - bindlessConfig.shadowMapArray, - bindlessConfig.contactDepth, - bindlessConfig.contactHistory, - bindlessConfig.envSky0, - bindlessConfig.envSky1, - bindlessConfig.hudShadowMap, - bindlessConfig.shadowCascades, - bindlessConfig.localShadowAtlas, - bindlessConfig.shadowHZB, - bindlessConfig.shadowMask, - bindlessConfig.localShadowESM - ); - } - auto postDepth = occludeDepth; auto postNormal = wetNormal.is_valid() ? wetNormal : (transparentOutputs.normal.is_valid() ? transparentOutputs.normal : gbufferForWet.normal); @@ -2732,6 +2795,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { m_device, sceneColor, postDepth, + transparentOutputs.worldPos.is_valid() ? transparentOutputs.worldPos : worldPosBuffer, motionOutput.motionVectors, width, height, @@ -2826,6 +2890,44 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { m_framegraph->GetRTRegistry().RegisterRT("rt_TAA", sceneColor); } + if (!m_hudBatches.empty()) + { + const auto hudDepth = (rtgiActive && rtgiGuideDepth.is_valid()) + ? rtgiGuideDepth : transparentOutputs.depth; + const auto hudNormal = (rtgiActive && rtgiGuideNormal.is_valid()) + ? rtgiGuideNormal : transparentOutputs.normal; + const auto hudBase = (rtgiActive && rtgiGuideBaseColor.is_valid()) + ? rtgiGuideBaseColor : transparentOutputs.baseColor; + const auto hudWorldPos = (rtgiActive && rtgiGuideWorldPos.is_valid()) + ? rtgiGuideWorldPos : transparentOutputs.worldPos; + sceneColor = passes::setupHudOverlayPass( + *m_framegraph, + m_device, + sceneColor, + hudDepth, + hudNormal, + hudBase, + hudWorldPos, + &m_hudBatches, + m_materialCache.get(), + width, + height, + m_gpuCullingManager.get(), + &m_blackboard->get_or_add(), + bindlessConfig.shadowMapArray, + bindlessConfig.contactDepth, + bindlessConfig.contactHistory, + bindlessConfig.envSky0, + bindlessConfig.envSky1, + bindlessConfig.hudShadowMap, + bindlessConfig.shadowCascades, + bindlessConfig.localShadowAtlas, + bindlessConfig.shadowHZB, + bindlessConfig.shadowMask, + bindlessConfig.localShadowESM + ); + } + // Classic CoP MiddleGray exposure (r2_tonemap*); NOT photographic histogram EV passes::ExposureConfig exposureConfig = passes::GetDefaultExposureConfig(); auto exposureOutput = passes::setupExposurePass( @@ -2840,11 +2942,25 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { ); m_exposureTexture = exposureOutput.exposureTexture; + if (pendingDistortRT.is_valid() && o.distortion) + { + sceneColor = passes::setupDistortionApplyPass( + *m_framegraph, + m_device, + sceneColor, + pendingDistortRT, + pendingDistortWorldPos, + pendingDistortBaseColor, + width, + height, + m_blackboard->get_or_add()); + } + // FSR/DLSS or 1:1 resolve to display resolution before bloom/tonemap/UI { passes::UpscaleRRGuides rrGuides{}; passes::UpscaleRRGuides* rrGuidesPtr = nullptr; - if (ps_r_upscale == 2 && ps_r_dlss_rr) + if (ps_r_upscale == 2 && ps_r_dlss_rr && !ps_r_rt_gi) { auto& rtgiState = m_blackboard->get_or_add(); rrGuides.normals = rtgiGuideNormal; @@ -2856,7 +2972,7 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { rrGuides.hitDistance = rtgiState.hitDistance.Get(); rrGuidesPtr = &rrGuides; } - const auto upscaleDepth = (ps_r_upscale == 2 && ps_r_dlss_rr && rtgiGuideDepth.is_valid()) + const auto upscaleDepth = (ps_r_upscale == 2 && ps_r_dlss_rr && !ps_r_rt_gi && rtgiGuideDepth.is_valid()) ? rtgiGuideDepth : postDepth; sceneColor = passes::setupUpscaleOrResolvePass( @@ -2877,20 +2993,6 @@ void FrameGraphRenderer::SetupFrameGraphPasses() { const u32 postH = upscaleResolved ? displayHeight : height; const bool upscaleOwnsTemporal = ps_r_upscale != 0; - if (pendingDistortRT.is_valid() && o.distortion) - { - sceneColor = passes::setupDistortionApplyPass( - *m_framegraph, - m_device, - sceneColor, - pendingDistortRT, - pendingDistortWorldPos, - pendingDistortBaseColor, - postW, - postH, - m_blackboard->get_or_add()); - } - if (rtgiActive && m_rtAccelMgr) { const auto volWorldPos = rtgiGuideWorldPos.is_valid() @@ -4263,11 +4365,19 @@ class CGlow : public IRender_Glow } } + static bool TooCloseToCamera(const Fvector& pos, float radius) + { + const float minDist = _max(0.45f, radius * 2.f); + return Device.vCameraPosition.distance_to_sqr(pos) < minDist * minDist; + } + static bool CollectBillboard(void* glow, fg::passes::GlowBillboard& out) { auto* g = static_cast(glow); if (!g || !g->bActive) return false; + if (TooCloseToCamera(g->m_pos, g->m_radius)) + return false; out.pos = g->m_pos; out.radius = g->m_radius; out.color = g->m_color; @@ -4283,7 +4393,8 @@ class CGlow : public IRender_Glow m_light->set_rotation(m_dir, m_dir); m_light->set_range(_max(m_radius * 2.5f, 0.5f)); m_light->set_color(m_color); - m_light->set_active(bActive); + const bool useLight = bActive && !TooCloseToCamera(m_pos, m_radius); + m_light->set_active(useLight); } void set_active(bool b) override diff --git a/src/Layers/xrRender/r_FrameGraphRenderer_Loader.cpp b/src/Layers/xrRender/r_FrameGraphRenderer_Loader.cpp index 13bd55604b4..3618cae9f8a 100644 --- a/src/Layers/xrRender/r_FrameGraphRenderer_Loader.cpp +++ b/src/Layers/xrRender/r_FrameGraphRenderer_Loader.cpp @@ -19,7 +19,6 @@ #include "Layers/xrRender/FrameGraph/ShaderLoader.h" #include "Layers/xrRender/Materials/MaterialSystem.h" #include "Layers/xrRender/FGDetailManager.h" -#include "Layers/xrRender/PBRConverter/PBRTextureConverter.h" #include "Layers/xrRender/Light_DB.h" #include "Layers/xrRender/ModelPool.h" #include "Layers/xrRender/r__sector.h" @@ -97,16 +96,10 @@ void FrameGraphRenderer::level_Load(IReader* fs) *delim = 0; xr_strcpy(n_tlist, delim + 1); - // Extract first texture name - string256 firstTexture; - xr_strcpy(firstTexture, n_tlist); - if (pstr comma = strchr(firstTexture, ',')) - *comma = 0; // Truncate at first comma - - // D3D12: Compile NVRHI shaders directly (NO legacy ref_shader!) - if (true) { - CompileLevelShader(i, n_sh, firstTexture); - } + // Keep the full classic list "base[,lmap#N_1[,lmap#N_2]]" — consumers + // that need a single texture strip commas themselves (MaterialCache), + // and the lmap slots are required for baked static lightmaps. + CompileLevelShader(i, n_sh, n_tlist); } chunk->close(); } @@ -144,9 +137,6 @@ void FrameGraphRenderer::level_Load(IReader* fs) if (detailMgr && detailMgr->Load()) { detailMgr->BakeHeightmap(); detailMgr->LoadHeightmapTexture(GetRenderDevice()->GetNVRHIDevice()); - pbr::PBRConversionParams pbrParams; - pbrParams.generate_mipmaps = true; - pbr::ConvertSingleTextureToPBR("$level$", "build_details.dds", pbrParams); detailMgr->LoadBuildDetailsTexture(GetRenderDevice()->GetNVRHIDevice()); detailMgr->ComputeSlotAABBs(); detailMgr->CreateGPUBuffers(GetRenderDevice()->GetNVRHIDevice()); diff --git a/src/xrCore/_matrix.h b/src/xrCore/_matrix.h index 5ef1f547473..4536f12d010 100644 --- a/src/xrCore/_matrix.h +++ b/src/xrCore/_matrix.h @@ -324,6 +324,57 @@ struct Fmatrix return *this; } + IC SelfRef build_projection_stdz(float fFOV, float fAspect, float fNearPlane, float fFarPlane) + { + float HAT = tanf(fFOV / 2.f); + VERIFY(_abs(fFarPlane - fNearPlane) > EPS_S); + VERIFY(_abs(HAT) > EPS_S); + + float cot = 1.0f / HAT; + float w = fAspect * cot; + float h = 1.0f * cot; + float Q = fFarPlane / (fFarPlane - fNearPlane); + + _11 = w; + _12 = 0; + _13 = 0; + _14 = 0; + _21 = 0; + _22 = h; + _23 = 0; + _24 = 0; + _31 = 0; + _32 = 0; + _33 = Q; + _34 = 1.0f; + _41 = 0; + _42 = 0; + _43 = -Q * fNearPlane; + _44 = 0; + return *this; + } + + IC SelfRef build_projection_ortho_stdz(float w, float h, float zn, float zf) + { + _11 = 2.0f / w; + _12 = 0; + _13 = 0; + _14 = 0; + _21 = 0; + _22 = 2.0f / h; + _23 = 0; + _24 = 0; + _31 = 0; + _32 = 0; + _33 = 1.0f / (zf - zn); + _34 = 0; + _41 = 0; + _42 = 0; + _43 = zn / (zn - zf); + _44 = 1.0f; + return *this; + } + IC SelfRef build_projection_ortho(float w, float h, float zn, float zf) { _11 = 2.0f / w; diff --git a/src/xrEngine/IRenderBackend.h b/src/xrEngine/IRenderBackend.h index 9bc340c0ca5..f35d31a40bc 100644 --- a/src/xrEngine/IRenderBackend.h +++ b/src/xrEngine/IRenderBackend.h @@ -83,6 +83,7 @@ class ENGINE_API IRenderBackend virtual u32 GetBackBufferCount() const { return 1; } virtual void Present(bool vsync) = 0; virtual bool PresentFrameGeneration(nvrhi::ITexture* interpolated, nvrhi::ITexture* real) { (void)interpolated; (void)real; return false; } + virtual u64 GetPresentCount() const { return 0; } virtual std::pair GetBackBufferSize() const = 0; virtual void ResizeSwapChain(u32 width, u32 height) {} From 300a38f308b5bdfba51e7ad061f5e0bcb6aabd19 Mon Sep 17 00:00:00 2001 From: FoxConED Date: Thu, 30 Jul 2026 00:01:51 +0300 Subject: [PATCH 261/261] docs: document FrameGraph lighting and ReSTIR RT stack --- framegraph_docs/FRAMEGRAPH_LIGHTING_RT.md | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 framegraph_docs/FRAMEGRAPH_LIGHTING_RT.md diff --git a/framegraph_docs/FRAMEGRAPH_LIGHTING_RT.md b/framegraph_docs/FRAMEGRAPH_LIGHTING_RT.md new file mode 100644 index 00000000000..02d84f4f49c --- /dev/null +++ b/framegraph_docs/FRAMEGRAPH_LIGHTING_RT.md @@ -0,0 +1,49 @@ +# FrameGraph Lighting & ReSTIR RT + +English notes for the FoxConED lighting / ReSTIR / upscaling work on top of `yohji/feat/framegraph`. + +## Scope + +- Target PR: `OpenXRay/xray-16` base branch `yohji/feat/framegraph` +- Head: FoxConED fork branch carrying this stack +- Out of scope for the commit set: local `Externals/` dumps, `run/`, build logs, onnx tarballs, `tmp/`, `tools/` + +## Historical FoxConED commits (already on the branch) + +These landed earlier as larger sync commits. Content map for review (not rewritten in place, to avoid touching other authors' history): + +| Commit theme | What it contains | +|---|---| +| FrameGraph lighting stack (CSM / local atlas / postFX) | Sun CSM + spot/omni local shadow atlas into forward+, sticky tile assignment, classic-style PCF; supporting FG passes (SSR/SSGI/AO/bloom/TAA/volumetrics/rain) | +| Sync lighting / RT / upscaling stack | ReSTIR DI/GI path, NRD wiring, DLSS/Streamline hooks, RT BLAS/TLAS integration, skinned RT paths, console knobs | +| Restore lighting after OpenXRay FG merge | Re-apply lighting stack after merging newer OpenXRay framegraph tip | +| Merge OpenXRay framegraph tip / dev mainline | Integration merges only | +| Small build / engine fixes | GCC16 `_Countof`, Thunderbolt ED duplicate, `session_name.c_str()`, ancestry note | + +## Split follow-up commits (this PR tip) + +| Commit | Intent | +|---|---| +| stabilize ReSTIR temporal on camera motion | Motion-vector history sample uses `currUV + mv` (`mv = prev - curr`); relaxed turn rejection gates | +| light CHAR mutants in ReSTIR like world geometry | `SURF_MARK_CHAR` skinned self-shadow soft-skip; VariantPSO remap to lit `bindless_skinned*` | +| improve RT water reflections lighting and gloss | Terrain/lmap albedo path, GGX glossy, lower roughness blur | +| fix DLAA with RT and tighten DLSS feature recreate | Force DLAA render scale 1; after feature recreate evaluate with `reset` instead of skipping the frame | +| add skinned velocity shaders and every-frame BLAS | Skinned velocity VS/PS set; per-frame skinned BLAS updates | +| harden particle occlusion and glow forward paths | Particle occlusion PS + glow forward shaders / pass wiring | +| extend ReSTIR GI stack and RT quality console knobs | GI pass setup, `r_rt_quality` presets, related constants | +| integrate FrameGraph lighting passes and shared shaders | Remaining FG pass / shared shader / culling / material glue | + +## Runtime notes + +- DLSS Frame Generation display FPS is not the same as the rendered/base FPS counter; FG doubles presented frames, RT cost still hits the base path. +- `r_rt_quality` is console-driven; it is intentionally not exposed in the options menu. +- CHAR/mutant darkness was caused by skinned hits zeroing shadow attenuation; self-skip now allows partial lighting on character surfaces. + +## Suggested review order + +1. ReSTIR temporal filter MV sign + turn gates +2. `rt_visibility.h` skinned self-max + VariantPSO skinned remap +3. `restir_water_rt.cs` +4. `UpscaleState` / `NgxDLSS` DLAA + feature recreate +5. Skinned velocity + BLAS update path +6. Broader FG / console integration

    ov5z5ZafZfB1^lbgqE3Z; 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