feat(vcs): add configurable CloudWorks volumetric clouds

This commit is contained in:
Jessica_Natalia
2026-08-13 16:21:22 -03:00
parent 98d6b05477
commit a950e9c218
12 changed files with 1032 additions and 15 deletions
+6 -1
View File
@@ -222,6 +222,8 @@ endif()
add_custom_command(TARGET VCSNative POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${VCS_PROFILE_DIR}/config/VCSNative.ini" "$<TARGET_FILE_DIR:VCSNative>/VCSNative.ini"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${VCS_PROFILE_DIR}/config/ProperShaders.ini" "$<TARGET_FILE_DIR:VCSNative>/ProperShaders.ini"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${VCS_PROFILE_DIR}/data/VCSProject2DFX_Lights.bin"
"$<TARGET_FILE_DIR:VCSNative>/VCSProject2DFX_Lights.bin"
@@ -230,7 +232,10 @@ add_custom_command(TARGET VCSNative POST_BUILD
"$<TARGET_FILE_DIR:VCSNative>/PROJECT2DFX_ATTRIBUTION.md"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${VCS_PROFILE_DIR}/third_party/project2dfx/LICENSE.txt"
"$<TARGET_FILE_DIR:VCSNative>/PROJECT2DFX_LICENSE.txt")
"$<TARGET_FILE_DIR:VCSNative>/PROJECT2DFX_LICENSE.txt"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${VCS_PROFILE_DIR}/third_party/cloudworks/ATTRIBUTION.md"
"$<TARGET_FILE_DIR:VCSNative>/CLOUDWORKS_ATTRIBUTION.md")
# Profile-specific generator. The root psp_recomp target stays game-neutral.
add_executable(vcs_recomp tools/vcs_codegen_main.cpp)
+2
View File
@@ -6,5 +6,7 @@ The VCS profile includes components that are distributed under licenses separate
- SMAA shader resources: see `third_party/smaa/LICENSE.txt`.
- Project2DFX-derived VCS LOD-light data/behavior reference by ThirteenAG: MIT. See `third_party/project2dfx/LICENSE.txt` and `third_party/project2dfx/ATTRIBUTION.md`.
- HDR shader material under `shaders/hdr`: see `shaders/hdr/LICENSE_SIMULATEHDR.txt`.
- CloudWorks Alpha 4.0 volumetric-cloud density/noise model by Brian Tu (RTU):
CC BY-NC-SA 3.0. See `third_party/cloudworks/ATTRIBUTION.md`.
Commercial GTA assets and executables are not part of the repository.
+10
View File
@@ -0,0 +1,10 @@
; Standalone CloudWorks sky for VCSNative. This does not use the game's
; timecycle or weather. Set Enabled=false to restore the unmodified game sky.
[VolumetricClouds]
Enabled=true
MarchSteps=24
Coverage=0.72
Opacity=0.84
; Keep the density field completely static while validating camera anchoring.
; A small positive value can be restored after the camera test is confirmed.
Speed=0.0
+167
View File
@@ -0,0 +1,167 @@
#pragma once
#include <array>
#include <cstddef>
#include <limits>
namespace vcs {
// GE affine matrices use the same layout as ge_renderer::transform_4x3:
//
// view = A * world + t
// A = [m0 m3 m6; m1 m4 m7; m2 m5 m8], t = [m9 m10 m11].
//
// view_to_world is A^-1 in row-major order. The three named axes are its
// columns, so A^-1 * view_direction is exactly
// right*x + up*y + forward*z. They are intentionally not normalized: retaining
// scale/shear makes the result a true inverse for every nonsingular GE view.
struct GeCloudCameraFrame {
std::array<float, 9> view_to_world{};
std::array<float, 3> position{};
std::array<float, 3> right{};
std::array<float, 3> up{};
std::array<float, 3> forward{};
};
namespace ge_cloud_camera_detail {
constexpr double absolute(double value) noexcept {
return value < 0.0 ? -value : value;
}
constexpr bool finite(double value) noexcept {
return value == value &&
value <= std::numeric_limits<double>::max() &&
value >= -std::numeric_limits<double>::max();
}
constexpr bool finite_float_result(double value) noexcept {
return finite(value) &&
absolute(value) <= static_cast<double>(std::numeric_limits<float>::max());
}
} // namespace ge_cloud_camera_detail
// Computes a full inverse of the GE view's 3x3 linear part. Returns false for
// non-finite or (numerically) singular input and leaves `out` unchanged.
[[nodiscard]] inline constexpr bool ge_cloud_invert_view_linear(
const std::array<float, 12> &view,
std::array<float, 9> &out) noexcept {
using ge_cloud_camera_detail::absolute;
using ge_cloud_camera_detail::finite;
using ge_cloud_camera_detail::finite_float_result;
for (float value : view) {
if (!finite(static_cast<double>(value))) return false;
}
const double a = view[0], b = view[3], c = view[6];
const double d = view[1], e = view[4], f = view[7];
const double g = view[2], h = view[5], i = view[8];
double scale = 0.0;
for (std::size_t index = 0; index < 9; ++index) {
const double value = absolute(static_cast<double>(view[index]));
if (value > scale) scale = value;
}
if (scale == 0.0) return false;
const double determinant =
a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
const double singular_threshold =
8.0 * static_cast<double>(std::numeric_limits<float>::epsilon()) *
scale * scale * scale;
if (!finite(determinant) || absolute(determinant) <= singular_threshold)
return false;
const double inverse_determinant = 1.0 / determinant;
const std::array<double, 9> inverse{
(e * i - f * h) * inverse_determinant,
(c * h - b * i) * inverse_determinant,
(b * f - c * e) * inverse_determinant,
(f * g - d * i) * inverse_determinant,
(a * i - c * g) * inverse_determinant,
(c * d - a * f) * inverse_determinant,
(d * h - e * g) * inverse_determinant,
(b * g - a * h) * inverse_determinant,
(a * e - b * d) * inverse_determinant,
};
std::array<float, 9> result{};
for (std::size_t index = 0; index < inverse.size(); ++index) {
if (!finite_float_result(inverse[index])) return false;
result[index] = static_cast<float>(inverse[index]);
}
out = result;
return true;
}
// Builds the world-space camera origin and the exact view-to-world basis from
// a GE world-to-view matrix. Returns false and leaves `out` unchanged if the
// matrix cannot be inverted or any derived value is non-finite.
[[nodiscard]] inline constexpr bool ge_cloud_camera_frame_from_view(
const std::array<float, 12> &view,
GeCloudCameraFrame &out) noexcept {
std::array<float, 9> inverse{};
if (!ge_cloud_invert_view_linear(view, inverse)) return false;
const double tx = view[9], ty = view[10], tz = view[11];
const std::array<double, 3> position{
-(static_cast<double>(inverse[0]) * tx +
static_cast<double>(inverse[1]) * ty +
static_cast<double>(inverse[2]) * tz),
-(static_cast<double>(inverse[3]) * tx +
static_cast<double>(inverse[4]) * ty +
static_cast<double>(inverse[5]) * tz),
-(static_cast<double>(inverse[6]) * tx +
static_cast<double>(inverse[7]) * ty +
static_cast<double>(inverse[8]) * tz),
};
for (double value : position) {
if (!ge_cloud_camera_detail::finite_float_result(value)) return false;
}
GeCloudCameraFrame result{};
result.view_to_world = inverse;
result.position = {static_cast<float>(position[0]),
static_cast<float>(position[1]),
static_cast<float>(position[2])};
result.right = {inverse[0], inverse[3], inverse[6]};
result.up = {inverse[1], inverse[4], inverse[7]};
result.forward = {inverse[2], inverse[5], inverse[8]};
out = result;
return true;
}
namespace ge_cloud_camera_detail {
// Compile-time contract check using an exact 90-degree rotation and
// translation. This also catches accidental row/column transposition.
static_assert([] {
constexpr std::array<float, 12> view{
0.0f, 1.0f, 0.0f,
-1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f,
3.0f, -2.0f, -4.0f,
};
GeCloudCameraFrame frame{};
return ge_cloud_camera_frame_from_view(view, frame) &&
frame.position == std::array<float, 3>{2.0f, 3.0f, 4.0f} &&
frame.right == std::array<float, 3>{0.0f, -1.0f, 0.0f} &&
frame.up == std::array<float, 3>{1.0f, 0.0f, 0.0f} &&
frame.forward == std::array<float, 3>{0.0f, 0.0f, 1.0f};
}());
static_assert([] {
constexpr std::array<float, 12> singular{
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f,
};
GeCloudCameraFrame frame{};
return !ge_cloud_camera_frame_from_view(singular, frame);
}());
} // namespace ge_cloud_camera_detail
} // namespace vcs
@@ -0,0 +1,324 @@
#pragma once
namespace vcs {
// Standalone, single-present-pass adaptation of the low CloudWorks Alpha 4.0
// cloud profile by Brian Tu (RTU). CloudWorks is licensed CC BY-NC-SA 3.0.
//
// Root-constant contract (b1, 21 DWORDs total):
// 0..3 unused draw-state padding
// 4 unused draw-state padding
// 5..7 CloudRayRight.xyz (NDC-x world-ray coefficient)
// 8 CloudTime (seconds * configured speed)
// 9..11 CloudRayUp.xyz (NDC-y world-ray coefficient)
// 12 CloudCoverage [0, 1]
// 13..15 CloudRayForward.xyz (world ray at NDC 0,0)
// 16 CloudOpacity [0, 1]
// 17..19 CloudCameraPosition.xyz (world space, Z up)
// 20 CloudSettings: bits 0..7 = march steps [4,64], bit 8 = enabled
//
// CloudRayRight/Up are deliberately not normalized camera axes. The host must
// bake inverse projection into them. CloudRayForward must come directly from
// unprojection (never cross(right, up)); this keeps the noise field fixed in
// world space even for a reflected/scaled GE view basis.
inline constexpr char kCloudWorksPresentShaderHlsl[] = R"CLOUD_HLSL(
Texture2D<float4> SourceTexture : register(t0);
SamplerState SourceSampler : register(s0);
cbuffer CloudPresentState : register(b1) {
uint4 UnusedDrawState0;
uint UnusedDrawState1;
float3 CloudRayRight;
float CloudTime;
float3 CloudRayUp;
float CloudCoverage;
float3 CloudRayForward;
float CloudOpacity;
float3 CloudCameraPosition;
uint CloudSettings;
};
struct PresentVertexOutput {
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
};
PresentVertexOutput PresentVS(uint id : SV_VertexID) {
PresentVertexOutput output;
if (id == 0u) {
output.position = float4(-1.0, -1.0, 0.0, 1.0);
output.uv = float2(0.0, 1.0);
} else if (id == 1u) {
output.position = float4(-1.0, 3.0, 0.0, 1.0);
output.uv = float2(0.0, -1.0);
} else {
output.position = float4(3.0, -1.0, 0.0, 1.0);
output.uv = float2(2.0, 1.0);
}
return output;
}
// CloudWorks' original scalar sine hash and linearly interpolated value noise.
float CwHash(float value) {
return frac(sin(value / 1873.1873) * 1618.03398875);
}
float CwNoise2(float3 p) {
float3 cell = floor(p);
float3 f = frac(p);
float n = 1153.0 * cell.x + 2381.0 * cell.y + p.z;
float right = n + 1153.0;
float down = n + 2381.0;
float opposite = right + 2381.0;
return lerp(lerp(CwHash(n), CwHash(right), f.x),
lerp(CwHash(down), CwHash(opposite), f.x), f.y);
}
float CwNoise3(float3 p) {
float3 cell = floor(p);
float3 f = frac(p);
float n = 1153.0 * cell.x + 2381.0 * cell.y + cell.z;
float right = n + 1153.0;
float down = n + 2381.0;
float opposite = right + 2381.0;
float a = lerp(CwHash(n), CwHash(n + 1.0), f.z);
float b = lerp(CwHash(right), CwHash(right + 1.0), f.z);
float c = lerp(CwHash(down), CwHash(down + 1.0), f.z);
float d = lerp(CwHash(opposite), CwHash(opposite + 1.0), f.z);
return lerp(lerp(a, b, f.x), lerp(c, d, f.x), f.y);
}
float CwSmooth(float edge0, float edge1, float value) {
float x = saturate((value - edge0) / (edge1 - edge0));
return x * x * (3.0 - 2.0 * x);
}
// x = shape factor, y = high density threshold, z = low threshold.
float3 CwCloudShape(float height, float coverage) {
const float bottom = 300.0;
const float middle = 450.0;
const float top = 700.0;
float body = CwSmooth(0.0, middle, height) *
(1.0 - CwSmooth(middle, top, height));
// The handheld sky is a very small target after upscale. Use the same
// CloudWorks profile but broaden its occupied threshold range so the deck
// reads as a cloud mass rather than a few isolated wisps.
float bottomRange = 0.24 + coverage * 0.52;
float soft = (height - top) / (middle - top) *
(bottomRange - 0.1) + 0.1;
float total = rcp(0.92 + coverage * 0.48);
return float3(body, total + soft, total - soft);
}
float3 CwOffsetA(float flow) { return float3(-1.8, 1.0, 0.0) * flow; }
float3 CwOffsetB(float flow) { return float3(-2.0, -0.2, 0.0) * flow; }
float3 CwOffsetC(float flow) { return float3(-3.0, 0.0, -0.5) * flow; }
float3 CwOffsetD(float flow) { return float3(-3.5, 0.0, 0.1) * flow; }
float CwChunk(float3 worldPosition, float shapeFactor, float flow) {
float3 p = worldPosition;
p += float3(-0.5, 0.0, 0.0) * p.z;
float largeNoise = CwNoise3((p + CwOffsetA(flow)) * 0.0008);
float smallNoise = CwNoise3((p + CwOffsetB(flow)) * 0.005);
return largeNoise * (smallNoise * 0.5 + 0.3) * shapeFactor;
}
float3 CwDistortion(float lump) {
return float3(cos(lump * 1.6) * 60.0, 0.0, -lump * 8.0);
}
float CwDetail(float lump, float3 worldPosition, float flow) {
float3 distortion = CwDistortion(lump);
float detail = 0.3 * CwNoise3(
(worldPosition + CwOffsetC(flow) + distortion) * 0.02);
distortion.z -= detail * 16.0;
float3 detailPosition = worldPosition + CwOffsetD(flow);
detail += 0.2 * CwNoise3((detailPosition + distortion / 3.0) * 0.04);
detail += detail * 0.6 *
CwNoise3((detailPosition + distortion * 8.0) * 0.1);
return detail;
}
// x = raw density field, y = extinction density, z = low threshold.
float3 CwDensity(float3 worldPosition, float coverage, float flow) {
float3 shape = CwCloudShape(worldPosition.z, coverage);
float lump = CwChunk(worldPosition, shape.x, flow);
float field = lump * (1.0 + CwDetail(lump, worldPosition, flow));
float solidness = lerp(0.0, 7.0 * coverage,
saturate((worldPosition.z - 300.0) / 400.0));
float density = saturate((field - shape.z) /
max(shape.y - shape.z, 1.0e-4)) * solidness;
return float3(field, density * 1.45, shape.z);
}
// One inexpensive approximation of CloudWorks' eight-sample sun shadow march.
// It retains the original Chunk + DetailA profile used by ShadowMarching.
float CwShadowDensity(float3 worldPosition, float coverage, float flow) {
float3 shape = CwCloudShape(worldPosition.z, coverage);
float lump = CwChunk(worldPosition, shape.x, flow);
float3 distortion = CwDistortion(lump);
float detail = 0.3 * CwNoise3(
(worldPosition + CwOffsetC(flow) + distortion) * 0.02) * 1.75;
float field = lump * (1.0 + detail);
float solidness = lerp(0.0, 5.0 * coverage,
saturate((worldPosition.z - 300.0) / 400.0));
return saturate((field - (shape.z - 0.1)) /
max(shape.y - (shape.z - 0.1), 1.0e-4)) * solidness;
}
float CwSkyMask(float2 uv, float3 source, float rayHeight) {
uint width = 1u;
uint height = 1u;
SourceTexture.GetDimensions(width, height);
float2 pixel = rcp(float2(max(width, 1u), max(height, 1u)));
float3 left = SourceTexture.SampleLevel(SourceSampler,
saturate(uv - float2(pixel.x, 0.0)), 0.0).rgb;
float3 right = SourceTexture.SampleLevel(SourceSampler,
saturate(uv + float2(pixel.x, 0.0)), 0.0).rgb;
float3 above = SourceTexture.SampleLevel(SourceSampler,
saturate(uv - float2(0.0, pixel.y)), 0.0).rgb;
float3 below = SourceTexture.SampleLevel(SourceSampler,
saturate(uv + float2(0.0, pixel.y)), 0.0).rgb;
float localEdge = max(max(length(source - left), length(source - right)),
max(length(source - above), length(source - below)));
float flatSky = 1.0 - CwSmooth(0.025, 0.12, localEdge);
float blueOverRed = CwSmooth(0.025, 0.16, source.b - source.r);
float blueOverGreen = CwSmooth(-0.04, 0.10, source.b - source.g);
float luminance = dot(source, float3(0.2126, 0.7152, 0.0722));
float visibleSky = blueOverRed * blueOverGreen *
CwSmooth(0.12, 0.34, luminance);
float aboveHorizon = CwSmooth(0.035, 0.13, rayHeight);
return saturate(aboveHorizon * visibleSky * lerp(0.55, 1.0, flatSky));
}
// Returns premultiplied cloud radiance in rgb and remaining transmittance in a.
float4 CwMarchLowLayer(float3 rayOrigin, float3 rayDirection,
float coverage, uint marchSteps, float flow) {
float4 result = float4(0.0, 0.0, 0.0, 1.0);
const float cloudBottom = 300.0;
const float cloudTop = 700.0;
const float cloudFadeDistance = 6000.0;
float validDirection = rayDirection.z > 1.0e-4 ? 1.0 : 0.0;
float safeRayHeight = max(rayDirection.z, 1.0e-4);
float slabBegin = (cloudBottom - rayOrigin.z) / safeRayHeight;
float slabEnd = (cloudTop - rayOrigin.z) / safeRayHeight;
float rayBegin = max(min(slabBegin, slabEnd), 0.0);
float rayEnd = min(max(slabBegin, slabEnd), cloudFadeDistance);
rayEnd = validDirection > 0.5 ? rayEnd : rayBegin;
// World-anchored jitter breaks coherent contours without producing a
// screen-space stipple pattern that turns with the camera.
float3 entryPosition = rayOrigin + rayDirection * rayBegin;
float nominalStep = (rayEnd - rayBegin) / max(float(marchSteps), 1.0);
float jitter = CwNoise3(entryPosition * float3(0.031, 0.031, 0.013) + 19.19);
float distanceAlongRay = rayBegin + jitter * min(nominalStep, 40.0);
const float3 sunDirection = normalize(float3(0.38, -0.28, 0.88));
const float3 baseColor = float3(0.27, 0.32, 0.40);
const float3 sunColor = float3(1.02, 1.00, 0.93);
float3 radiance = 0.0;
float transmittance = 1.0;
float previousField = 0.0;
float previousDensity = 0.0;
[loop]
for (uint stepIndex = 0u; stepIndex < 64u; ++stepIndex) {
if (stepIndex >= marchSteps || distanceAlongRay >= rayEnd ||
transmittance <= 0.02) break;
float3 worldPosition = rayOrigin + rayDirection * distanceAlongRay;
float3 densitySample = CwDensity(worldPosition, coverage, flow);
// CloudWorks' dynamic empty-space skipping: dense regions approach a
// five-unit step; empty regions approach 80 units, expanding with
// distance. Unlike the old uniform slab division, the sampled Z
// planes therefore cannot form screen-aligned slices.
float occupancy = saturate((2.0 * densitySample.x - previousField) /
max(densitySample.z * 0.85, 1.0e-4));
float stepLength = lerp(80.0, 5.0, occupancy);
stepLength *= lerp(1.0, 8.0,
saturate(distanceAlongRay / cloudFadeDistance));
stepLength += CwNoise2(worldPosition + float3(0.0, 0.0, flow)) * 5.0;
stepLength = min(stepLength, 160.0);
stepLength = min(stepLength, rayEnd - distanceAlongRay);
if (densitySample.y > 1.0e-4 && stepLength > 0.0) {
// Trapezoidal Beer-Lambert integration makes opacity independent
// of the number of steps and avoids the saturated per-slice alpha
// produced by density * uniformStepLength.
float meanDensity = 0.5 * (previousDensity + densitySample.y);
float opticalDepth = meanDensity * min(stepLength, 80.0) * 0.018;
float segmentAlpha = 1.0 - exp(-opticalDepth);
float shadowDensity = CwShadowDensity(
worldPosition + sunDirection * 60.0, coverage, flow);
float sunVisibility = exp(-shadowDensity * 1.35);
float forwardScatter = pow(saturate(dot(rayDirection, sunDirection)), 24.0);
float lighting = saturate(0.28 + sunVisibility * 0.72);
float3 cloudColor = lerp(baseColor, sunColor, lighting);
cloudColor += sunColor * forwardScatter * 0.10;
radiance += transmittance * segmentAlpha * cloudColor;
transmittance *= 1.0 - segmentAlpha;
}
previousField = densitySample.x;
previousDensity = densitySample.y;
distanceAlongRay += max(stepLength, 1.0);
}
result = float4(radiance, saturate(transmittance));
return result;
}
float4 PresentPS(PresentVertexOutput input) : SV_TARGET {
float4 source = SourceTexture.SampleLevel(SourceSampler, input.uv, 0.0);
const uint enabledBit = 0x100u;
if ((CloudSettings & enabledBit) == 0u) return source;
uint marchSteps = min(64u, max(4u, CloudSettings & 0xFFu));
float2 ndc = float2(input.uv.x * 2.0 - 1.0,
1.0 - input.uv.y * 2.0);
float3 rayDirection = normalize(CloudRayForward +
CloudRayRight * ndc.x +
CloudRayUp * ndc.y);
float skyMask = CwSkyMask(input.uv, source.rgb, rayDirection.z);
if (skyMask <= 1.0e-3) return source;
// Preserve the existing INI Speed semantics while giving the low profile
// offsets a useful world-space velocity.
float flow = CloudTime * 25.0;
float4 clouds = CwMarchLowLayer(CloudCameraPosition, rayDirection,
saturate(CloudCoverage), marchSteps, flow);
float3 cloudComposite = clouds.rgb + source.rgb * clouds.a;
source.rgb = lerp(source.rgb, cloudComposite,
skyMask * saturate(CloudOpacity));
return source;
}
// World-target path. This is deliberately separate from PresentPS: its pixels
// are in the same coordinate system as the selected GE camera, and the D3D12
// pipeline depth-tests against the world's untouched clear depth. The result is
// blended behind geometry before VCS scales/composites that target to display.
float4 CloudTargetPS(PresentVertexOutput input) : SV_TARGET {
const uint enabledBit = 0x100u;
if ((CloudSettings & enabledBit) == 0u) discard;
uint marchSteps = min(64u, max(4u, CloudSettings & 0xFFu));
float2 ndc = float2(input.uv.x * 2.0 - 1.0,
1.0 - input.uv.y * 2.0);
float3 rayDirection = normalize(CloudRayForward +
CloudRayRight * ndc.x +
CloudRayUp * ndc.y);
if (rayDirection.z <= 1.0e-4) discard;
float flow = CloudTime * 25.0;
float4 clouds = CwMarchLowLayer(CloudCameraPosition, rayDirection,
saturate(CloudCoverage), marchSteps, flow);
float alpha = saturate((1.0 - clouds.a) * CloudOpacity);
if (alpha <= 1.0e-4) discard;
// CwMarchLowLayer returns premultiplied radiance, so the matching PSO uses
// ONE / INV_SRC_ALPHA blending.
return float4(clouds.rgb * CloudOpacity, alpha);
}
)CLOUD_HLSL";
} // namespace vcs
+9
View File
@@ -413,6 +413,15 @@ void shutdown_ge_gpu_backend() noexcept;
[[nodiscard]] bool ge_gpu_backend_graphics_ready() noexcept;
void ge_gpu_backend_record_draw(const GeGpuDrawDescriptor &draw) noexcept;
// Captures the dominant projected world camera for optional native post effects.
// The matrices are observed with the draw, before the GE state advances.
void ge_gpu_backend_observe_camera(const std::array<float, 12> &view,
const std::array<float, 16> &projection,
const std::array<float, 6> &viewport,
const std::array<float, 3> &camera_position,
const GeGpuDrawDescriptor &draw,
std::uint32_t vertex_weight) noexcept;
// Stages already-decoded vertices into a persistently mapped Vulkan buffer.
// This does not replace the software rasterizer yet; it proves that real VCS
// geometry can cross the host->Vulkan boundary with no per-draw allocation.
+387 -14
View File
@@ -1,9 +1,12 @@
#include "ge_gpu_backend.hpp"
#include "ge_cloud_camera_math.hpp"
#include "ge_cloudworks_present_shader.hpp"
#include "vcs_config.hpp"
#include "vcs_runtime_log.hpp"
#include <algorithm>
#include <array>
#include <bit>
#include <cmath>
#include <cstddef>
#include <cstdint>
@@ -117,6 +120,27 @@ struct Dx12PixelConstants {
};
static_assert(sizeof(Dx12PixelConstants) == 5u * sizeof(std::uint32_t));
struct CloudCameraCandidate {
std::array<float, 12> view{};
std::array<float, 16> projection{};
// scale X/Y, center X/Y and offset X/Y from the GE viewport. Keeping this
// with the exact draw camera lets the present shader reconstruct the same
// rays that the native geometry path rasterized.
std::array<float, 6> viewport{};
std::array<float, 3> camera_position{};
std::uint32_t target{};
std::uint64_t weight{};
std::uint64_t occluding_weight{};
};
struct CloudPresentConstants {
std::array<float, 4> ray_right_time{};
std::array<float, 4> ray_up_coverage{};
std::array<float, 4> ray_forward_opacity{};
std::array<float, 4> camera_settings{};
};
static_assert(sizeof(CloudPresentConstants) == 16u * sizeof(std::uint32_t));
struct Dx12FrameResources {
ComPtr<ID3D12CommandAllocator> allocator;
ComPtr<ID3D12Resource> upload_buffer;
@@ -188,6 +212,7 @@ struct Dx12GeState {
std::vector<std::byte> packed_0115_vertices;
std::vector<std::uint32_t> indices;
std::vector<Dx12Batch> batches;
std::vector<CloudCameraCandidate> cloud_cameras;
std::vector<std::byte> frame_rgba;
std::vector<std::byte> last_texture_rgba;
@@ -249,8 +274,10 @@ struct Dx12GeState {
std::uint32_t swap_width{};
std::uint32_t swap_height{};
ComPtr<ID3D12PipelineState> present_pipeline;
ComPtr<ID3D12PipelineState> cloud_target_pipeline;
ComPtr<ID3DBlob> present_vertex_shader;
ComPtr<ID3DBlob> present_pixel_shader;
ComPtr<ID3DBlob> cloud_target_pixel_shader;
bool direct_present_ok{};
std::uint32_t presented_framebuffer{};
std::uint32_t missed_display_intervals{};
@@ -813,6 +840,16 @@ cbuffer DrawPixelState : register(b1) {
uint TextureEnvPacked;
uint FogControlPacked;
uint FramebufferFormat;
float3 CloudRight;
float CloudInvProjectionX;
float3 CloudUp;
float CloudInvProjectionY;
float3 CloudCameraPosition;
float CloudTime;
float CloudCoverage;
float CloudOpacity;
float CloudMarchSteps;
float CloudEnabled;
};
struct VSIn {
float4 position : POSITION;
@@ -1022,19 +1059,7 @@ float4 PSMain(VSOut input) : SV_TARGET {
return false;
}
const char *present = R"HLSL(
Texture2D<float4> SourceTexture : register(t0);
SamplerState SourceSampler : register(s0);
struct VSOut { float4 position : SV_POSITION; float2 uv : TEXCOORD0; };
VSOut PresentVS(uint id : SV_VertexID) {
VSOut o;
if (id == 0u) { o.position=float4(-1.0,-1.0,0.0,1.0); o.uv=float2(0.0,1.0); }
else if (id == 1u) { o.position=float4(-1.0,3.0,0.0,1.0); o.uv=float2(0.0,-1.0); }
else { o.position=float4(3.0,-1.0,0.0,1.0); o.uv=float2(2.0,1.0); }
return o;
}
float4 PresentPS(VSOut input) : SV_TARGET { return SourceTexture.Sample(SourceSampler, input.uv); }
)HLSL";
const char *present = kCloudWorksPresentShaderHlsl;
errors.Reset();
hr = D3DCompile(present, std::strlen(present), "VCSNativeDX12GEPresent", nullptr, nullptr,
"PresentVS", "vs_5_1", flags, 0u, &s.present_vertex_shader, &errors);
@@ -1051,6 +1076,16 @@ float4 PresentPS(VSOut input) : SV_TARGET { return SourceTexture.Sample(SourceSa
: hr_text(hr, "D3DCompile(DX12 GE Present PS)");
return false;
}
errors.Reset();
hr = D3DCompile(present, std::strlen(present), "VCSNativeDX12GECloudTarget",
nullptr, nullptr, "CloudTargetPS", "ps_5_1", flags, 0u,
&s.cloud_target_pixel_shader, &errors);
if (FAILED(hr)) {
error = errors ? std::string(static_cast<const char *>(errors->GetBufferPointer()),
errors->GetBufferSize())
: hr_text(hr, "D3DCompile(DX12 GE cloud target PS)");
return false;
}
return true;
}
@@ -1087,7 +1122,10 @@ bool create_root_signature(Dx12GeState &s, std::string &error) noexcept {
parameters[3].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS;
parameters[3].Constants.ShaderRegister = 1u;
parameters[3].Constants.RegisterSpace = 0u;
parameters[3].Constants.Num32BitValues = 5u;
// 5 draw-state DWORDs plus 16 cloud-present DWORDs. Together with the
// 40-DWORD vertex transform and two descriptor tables this uses 63 of the
// D3D12 root signature's 64 DWORD budget.
parameters[3].Constants.Num32BitValues = 21u;
parameters[3].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
D3D12_ROOT_SIGNATURE_DESC desc{};
desc.NumParameters = static_cast<UINT>(parameters.size());
@@ -1664,6 +1702,254 @@ std::uint32_t present_sampler(Dx12GeState &s) noexcept {
return ensure_sampler(s, draw);
}
bool invert_cloud_matrix(const std::array<float, 16> &matrix,
std::array<double, 16> &inverse) noexcept {
// Gauss-Jordan in double precision. GE projection matrices are small, but
// the far plane can still make a float-only inverse needlessly fragile.
double rows[4][8]{};
double scale = 0.0;
for (std::size_t row = 0u; row < 4u; ++row) {
for (std::size_t column = 0u; column < 4u; ++column) {
const double value = matrix[column * 4u + row];
if (!std::isfinite(value)) return false;
rows[row][column] = value;
scale = std::max(scale, std::abs(value));
}
rows[row][4u + row] = 1.0;
}
if (!(scale > 0.0)) return false;
const double epsilon = scale * 1.0e-12;
for (std::size_t column = 0u; column < 4u; ++column) {
std::size_t pivot = column;
for (std::size_t row = column + 1u; row < 4u; ++row) {
if (std::abs(rows[row][column]) > std::abs(rows[pivot][column]))
pivot = row;
}
if (std::abs(rows[pivot][column]) <= epsilon) return false;
if (pivot != column) {
for (std::size_t entry = 0u; entry < 8u; ++entry)
std::swap(rows[pivot][entry], rows[column][entry]);
}
const double divisor = rows[column][column];
for (double &entry : rows[column]) entry /= divisor;
for (std::size_t row = 0u; row < 4u; ++row) {
if (row == column) continue;
const double factor = rows[row][column];
for (std::size_t entry = 0u; entry < 8u; ++entry)
rows[row][entry] -= factor * rows[column][entry];
}
}
std::array<double, 16> result{};
for (std::size_t row = 0u; row < 4u; ++row) {
for (std::size_t column = 0u; column < 4u; ++column) {
const double value = rows[row][4u + column];
if (!std::isfinite(value)) return false;
result[column * 4u + row] = value;
}
}
inverse = result;
return true;
}
bool create_cloud_target_pipeline(Dx12GeState &s, std::string &error) noexcept {
D3D12_GRAPHICS_PIPELINE_STATE_DESC pso{};
pso.pRootSignature = s.root_signature.Get();
pso.VS = {s.present_vertex_shader->GetBufferPointer(),
s.present_vertex_shader->GetBufferSize()};
pso.PS = {s.cloud_target_pixel_shader->GetBufferPointer(),
s.cloud_target_pixel_shader->GetBufferSize()};
pso.SampleMask = UINT_MAX;
pso.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
pso.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
pso.RasterizerState.DepthClipEnable = TRUE;
auto &blend = pso.BlendState.RenderTarget[0];
blend.BlendEnable = TRUE;
blend.SrcBlend = D3D12_BLEND_ONE;
blend.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
blend.BlendOp = D3D12_BLEND_OP_ADD;
blend.SrcBlendAlpha = D3D12_BLEND_ONE;
blend.DestBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA;
blend.BlendOpAlpha = D3D12_BLEND_OP_ADD;
blend.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
pso.DepthStencilState.DepthEnable = TRUE;
pso.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
// World targets are cleared to reverse-depth zero. Equality therefore
// restricts the fullscreen pass to pixels untouched by world geometry.
pso.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_EQUAL;
pso.DepthStencilState.StencilEnable = FALSE;
pso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
pso.NumRenderTargets = 1u;
pso.RTVFormats[0] = kColorFormat;
pso.DSVFormat = s.depth_format;
pso.SampleDesc.Count = s.sample_count;
pso.SampleDesc.Quality = s.sample_quality;
const HRESULT hr = s.device->CreateGraphicsPipelineState(
&pso, IID_PPV_ARGS(&s.cloud_target_pipeline));
if (FAILED(hr)) {
error = hr_text(hr, "CreateGraphicsPipelineState(DX12 GE cloud target)");
return false;
}
return true;
}
const CloudCameraCandidate *select_cloud_camera(const Dx12GeState &s) noexcept {
if (s.display_framebuffer == 0u) return nullptr;
std::vector<std::uint32_t> ancestors;
ancestors.reserve(std::min<std::size_t>(s.frame_targets.size() + 1u,
kFramebufferTargetCapacity));
ancestors.push_back(s.display_framebuffer & 0x001FFFF0u);
const auto contains = [&](std::uint32_t address) {
address &= 0x001FFFF0u;
return std::find(ancestors.begin(), ancestors.end(), address) != ancestors.end();
};
bool changed = true;
while (changed && ancestors.size() < kFramebufferTargetCapacity) {
changed = false;
for (const Dx12Batch &batch : s.batches) {
if (!batch.framebuffer_feedback) continue;
const std::uint32_t source = batch.feedback_address & 0x001FFFF0u;
const std::uint32_t destination = batch.draw.framebuffer_address & 0x001FFFF0u;
if (source == destination || !contains(destination) || contains(source)) continue;
ancestors.push_back(source);
changed = true;
if (ancestors.size() == kFramebufferTargetCapacity) break;
}
}
const CloudCameraCandidate *best = nullptr;
for (const CloudCameraCandidate &candidate : s.cloud_cameras) {
if (!contains(candidate.target) || candidate.occluding_weight == 0u) continue;
// Match the camera selector already proven by Project2DFX: the camera
// with the most genuinely occluding geometry wins, then total weight.
// Prioritising depth writes selected auxiliary/reflection passes in VCS.
if (best == nullptr ||
candidate.occluding_weight > best->occluding_weight ||
(candidate.occluding_weight == best->occluding_weight &&
candidate.weight > best->weight)) {
best = &candidate;
}
}
return best;
}
CloudPresentConstants cloud_present_constants(const Dx12GeState &s) noexcept {
CloudPresentConstants out{};
const auto &config = vcs_configuration().volumetric_clouds;
if (!config.enabled || s.cloud_cameras.empty()) return out;
const CloudCameraCandidate *camera = select_cloud_camera(s);
if (camera == nullptr) return out;
GeCloudCameraFrame frame{};
if (!ge_cloud_camera_frame_from_view(camera->view, frame)) return out;
const Dx12FramebufferTarget *target = find_framebuffer_target(s, camera->target);
const float logical_width = static_cast<float>(std::max<std::uint32_t>(
1u, target != nullptr ? target->logical_width : kReferenceWidth));
const float logical_height = static_cast<float>(std::max<std::uint32_t>(
1u, target != nullptr ? target->logical_height : kReferenceHeight));
const float x_a = camera->viewport[0] * (2.0f / logical_width);
const float y_a = camera->viewport[1] * (2.0f / logical_height);
const float x_b = (camera->viewport[2] - camera->viewport[4]) *
(2.0f / logical_width) - 1.0f;
const float y_b = (camera->viewport[3] - camera->viewport[5]) *
(2.0f / logical_height) - 1.0f;
if (!std::isfinite(x_a) || !std::isfinite(y_a) ||
std::abs(x_a) < 1.0e-6f || std::abs(y_a) < 1.0e-6f)
return out;
// Fold the GE viewport into projection exactly as make_transform_constants
// does for native geometry. This eliminates the camera-relative drift that
// came from treating raw P00/P11 as if every pass occupied the full target.
std::array<float, 16> effective_projection{};
for (std::size_t column = 0u; column < 4u; ++column) {
const std::size_t base = column * 4u;
effective_projection[base + 0u] =
x_a * camera->projection[base + 0u] + x_b * camera->projection[base + 3u];
effective_projection[base + 1u] =
-y_a * camera->projection[base + 1u] - y_b * camera->projection[base + 3u];
effective_projection[base + 2u] = camera->projection[base + 2u];
effective_projection[base + 3u] = camera->projection[base + 3u];
}
std::array<double, 16> inverse_projection{};
if (!invert_cloud_matrix(effective_projection, inverse_projection)) return out;
const auto view_direction = [&](double ndc_x, double ndc_y,
std::array<double, 3> &direction) {
constexpr double clip_z = 0.5;
const std::array<double, 4> clip{ndc_x, ndc_y, clip_z, 1.0};
std::array<double, 4> point{};
for (std::size_t row = 0u; row < 4u; ++row) {
for (std::size_t column = 0u; column < 4u; ++column)
point[row] += inverse_projection[column * 4u + row] * clip[column];
}
if (!std::isfinite(point[3]) || std::abs(point[3]) < 1.0e-12) return false;
for (std::size_t axis = 0u; axis < 3u; ++axis) {
direction[axis] = point[axis] / point[3];
if (!std::isfinite(direction[axis])) return false;
}
return true;
};
std::array<double, 3> center_view{}, right_view{}, up_view{};
if (!view_direction(0.0, 0.0, center_view) ||
!view_direction(1.0, 0.0, right_view) ||
!view_direction(0.0, 1.0, up_view)) return out;
const auto to_world = [&](const std::array<double, 3> &value) {
return std::array<double, 3>{
frame.view_to_world[0] * value[0] + frame.view_to_world[1] * value[1] +
frame.view_to_world[2] * value[2],
frame.view_to_world[3] * value[0] + frame.view_to_world[4] * value[1] +
frame.view_to_world[5] * value[2],
frame.view_to_world[6] * value[0] + frame.view_to_world[7] * value[1] +
frame.view_to_world[8] * value[2]};
};
const std::array<double, 3> center_world = to_world(center_view);
const std::array<double, 3> right_world = to_world(right_view);
const std::array<double, 3> up_world = to_world(up_view);
for (std::size_t axis = 0u; axis < 3u; ++axis) {
const double ray_right = right_world[axis] - center_world[axis];
const double ray_up = up_world[axis] - center_world[axis];
if (!std::isfinite(ray_right) || !std::isfinite(ray_up) ||
!std::isfinite(center_world[axis])) return {};
out.ray_right_time[axis] = static_cast<float>(ray_right);
out.ray_up_coverage[axis] = static_cast<float>(ray_up);
out.ray_forward_opacity[axis] = static_cast<float>(center_world[axis]);
out.camera_settings[axis] = camera->camera_position[axis];
}
out.ray_right_time[3] =
static_cast<float>(s.frame_epoch) * (1.0f / 60.0f) * config.speed;
out.ray_up_coverage[3] = config.coverage;
out.ray_forward_opacity[3] = config.opacity;
const std::uint32_t settings =
std::clamp<std::uint32_t>(config.march_steps, 4u, 64u) | 0x100u;
out.camera_settings[3] = std::bit_cast<float>(settings);
return out;
}
void record_clouds_into_world_target(Dx12GeState &s, Dx12FramebufferTarget &target,
const CloudPresentConstants &clouds) noexcept {
prepare_target_for_render(s, target);
const D3D12_CPU_DESCRIPTOR_HANDLE rtv = rtv_cpu(s, target.rtv_index);
const D3D12_CPU_DESCRIPTOR_HANDLE dsv = dsv_cpu(s, target.dsv_index);
s.list->OMSetRenderTargets(1u, &rtv, FALSE, &dsv);
D3D12_VIEWPORT viewport{0.0f, 0.0f, static_cast<float>(s.target_width),
static_cast<float>(s.target_height), 0.0f, 1.0f};
D3D12_RECT scissor{0, 0, static_cast<LONG>(s.target_width),
static_cast<LONG>(s.target_height)};
s.list->RSSetViewports(1u, &viewport);
s.list->RSSetScissorRects(1u, &scissor);
s.list->SetPipelineState(s.cloud_target_pipeline.Get());
s.list->SetGraphicsRootSignature(s.root_signature.Get());
ID3D12DescriptorHeap *heaps[]{s.srv_heap.Get(), s.sampler_heap.Get()};
s.list->SetDescriptorHeaps(2u, heaps);
s.list->SetGraphicsRootDescriptorTable(0u, srv_gpu(s, 0u));
s.list->SetGraphicsRootDescriptorTable(1u, sampler_gpu(s, 0u));
std::array<std::uint32_t, 21> constants{};
std::memcpy(constants.data() + 5u, &clouds, sizeof(clouds));
s.list->SetGraphicsRoot32BitConstants(
3u, static_cast<UINT>(constants.size()), constants.data(), 0u);
s.list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
s.list->DrawInstanced(3u, 1u, 0u, 0u);
}
bool record_direct_present(Dx12GeState &s, Dx12FramebufferTarget &source,
std::string &error) noexcept {
if (!ensure_swapchain(s, error)) return false;
@@ -1692,6 +1978,14 @@ bool record_direct_present(Dx12GeState &s, Dx12FramebufferTarget &source,
s.list->SetDescriptorHeaps(2u, heaps);
s.list->SetGraphicsRootDescriptorTable(0u, srv_gpu(s, source.srv_index));
s.list->SetGraphicsRootDescriptorTable(1u, sampler_gpu(s, present_sampler(s)));
// Clouds are rendered into the selected 3D world target before VCS samples
// it for composition. Applying them here would mix world-camera rays with
// final-display pixels and make the layer follow the screen.
const CloudPresentConstants clouds{};
std::array<std::uint32_t, 21> present_constants{};
std::memcpy(present_constants.data() + 5u, &clouds, sizeof(clouds));
s.list->SetGraphicsRoot32BitConstants(
3u, static_cast<UINT>(present_constants.size()), present_constants.data(), 0u);
s.list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
s.list->DrawInstanced(3u, 1u, 0u, 0u);
transition(s.list.Get(), backbuffer, D3D12_RESOURCE_STATE_RENDER_TARGET,
@@ -2082,6 +2376,7 @@ void clear_accumulation(Dx12GeState &s) noexcept {
s.packed_0115_vertices.clear();
s.indices.clear();
s.batches.clear();
s.cloud_cameras.clear();
}
bool create_backend(Dx12GeState &s, std::string &error) noexcept {
@@ -2168,6 +2463,7 @@ bool create_backend(Dx12GeState &s, std::string &error) noexcept {
if (!create_root_signature(s, error)) return false;
if (!create_targets(s, error)) return false;
if (!create_present_pipeline(s, error)) return false;
if (!create_cloud_target_pipeline(s, error)) return false;
s.vertices.reserve(262144u);
s.packed_0115_vertices.reserve(2621440u);
s.indices.reserve(524288u);
@@ -2208,6 +2504,8 @@ void destroy_backend(Dx12GeState &s) noexcept {
s.swapchain.Reset();
s.swap_rtv_heap.Reset();
s.present_pipeline.Reset();
s.cloud_target_pipeline.Reset();
s.cloud_target_pixel_shader.Reset();
s.present_pixel_shader.Reset();
s.present_vertex_shader.Reset();
s.pipelines.clear();
@@ -2417,6 +2715,43 @@ void ge_gpu_backend_record_draw(const GeGpuDrawDescriptor &draw) noexcept {
}
}
void ge_gpu_backend_observe_camera(const std::array<float, 12> &view,
const std::array<float, 16> &projection,
const std::array<float, 6> &viewport,
const std::array<float, 3> &camera_position,
const GeGpuDrawDescriptor &draw,
std::uint32_t vertex_weight) noexcept {
Dx12GeState &s = state();
if (!s.enabled || !vcs_configuration().volumetric_clouds.enabled ||
vertex_weight == 0u || !draw.depth_test_enabled) return;
if (!std::all_of(view.begin(), view.end(), [](float value) { return std::isfinite(value); }) ||
!std::all_of(projection.begin(), projection.end(), [](float value) { return std::isfinite(value); }) ||
!std::all_of(viewport.begin(), viewport.end(), [](float value) { return std::isfinite(value); }) ||
!std::all_of(camera_position.begin(), camera_position.end(),
[](float value) { return std::isfinite(value); }))
return;
const std::uint32_t target = draw.framebuffer_address & 0x001FFFF0u;
const auto found = std::find_if(
s.cloud_cameras.begin(), s.cloud_cameras.end(),
[&](const CloudCameraCandidate &candidate) {
return candidate.target == target && candidate.view == view &&
candidate.projection == projection && candidate.viewport == viewport;
});
if (found != s.cloud_cameras.end()) {
found->weight += vertex_weight;
found->camera_position = camera_position;
if ((draw.depth_function & 7u) >= 2u) found->occluding_weight += vertex_weight;
return;
}
// Normal gameplay has only a handful of camera variants per frame. A hard
// cap prevents malformed guest state from growing this host-only observer.
if (s.cloud_cameras.size() >= 16u) return;
const std::uint64_t occluding_weight = (draw.depth_function & 7u) >= 2u
? vertex_weight : 0u;
s.cloud_cameras.push_back({view, projection, viewport, camera_position,
target, vertex_weight, occluding_weight});
}
bool ge_gpu_backend_stage_vertices(const GeGpuDrawDescriptor &, std::span<const GeGpuVertex> vertices) noexcept {
Dx12GeState &s = state();
if (!s.enabled) return false;
@@ -2976,10 +3311,42 @@ bool ge_gpu_backend_finish_color_frame(std::uint64_t vblank) noexcept {
bool active_vertex_layout_valid = false;
D3D12_PRIMITIVE_TOPOLOGY active_topology = D3D_PRIMITIVE_TOPOLOGY_UNDEFINED;
bool touched_display = false;
const CloudCameraCandidate *cloud_camera = select_cloud_camera(s);
const CloudPresentConstants clouds = cloud_present_constants(s);
const std::uint32_t cloud_target_address = cloud_camera != nullptr
? cloud_camera->target : 0u;
bool clouds_injected = false;
constexpr float black[4]{0.0f, 0.0f, 0.0f, 1.0f};
for (const Dx12Batch &batch : s.batches) {
const std::uint32_t address = batch.draw.framebuffer_address & 0x001FFFF0u;
// Insert immediately before the first non-self pass samples the chosen
// world target. At this point its geometry/depth are complete, while
// the later VCS composition has not consumed its color yet.
if (!clouds_injected && cloud_camera != nullptr &&
batch.framebuffer_feedback &&
(batch.feedback_address & 0x001FFFF0u) == cloud_target_address &&
address != cloud_target_address) {
if (Dx12FramebufferTarget *cloud_target =
find_framebuffer_target(s, cloud_target_address);
cloud_target != nullptr && cloud_target->color && cloud_target->depth) {
if (current_target != nullptr && current_target != cloud_target)
resolve_target_for_sampling(s, *current_target, false);
record_clouds_into_world_target(s, *cloud_target, clouds);
current_target = cloud_target;
current_address = cloud_target_address;
clouds_injected = true;
active_pipeline = nullptr;
active_pipeline_key = std::numeric_limits<std::uint64_t>::max();
bound_srv = bound_sampler = std::numeric_limits<std::uint32_t>::max();
active_transform_valid = false;
active_pixel_valid = false;
active_scissor_valid = false;
active_blend_fix = std::numeric_limits<std::uint32_t>::max();
active_vertex_layout_valid = false;
active_topology = D3D_PRIMITIVE_TOPOLOGY_UNDEFINED;
}
}
Dx12FramebufferTarget *target = address == current_address
? current_target : find_framebuffer_target(s, address);
if (target == nullptr || !target->color || !target->depth) continue;
@@ -3381,6 +3748,12 @@ bool ge_gpu_backend_active() noexcept { return false; }
bool ge_gpu_backend_transfer_ready() noexcept { return false; }
bool ge_gpu_backend_graphics_ready() noexcept { return false; }
void ge_gpu_backend_record_draw(const GeGpuDrawDescriptor &) noexcept {}
void ge_gpu_backend_observe_camera(const std::array<float, 12> &,
const std::array<float, 16> &,
const std::array<float, 6> &,
const std::array<float, 3> &,
const GeGpuDrawDescriptor &,
std::uint32_t) noexcept {}
bool ge_gpu_backend_stage_vertices(const GeGpuDrawDescriptor &, std::span<const GeGpuVertex>) noexcept { return false; }
bool ge_gpu_backend_texture_needed(const GeGpuDrawDescriptor &) noexcept { return false; }
void ge_gpu_backend_prepare_texture_keys(GeGpuDrawDescriptor &) noexcept {}
+25
View File
@@ -4188,6 +4188,31 @@ bool render_ge_primitive(psprecomp::GuestMemory &memory,
gpu_draw.texture_content_signature = any_signature ? signature : 0u;
}
ge_gpu_backend_record_draw(gpu_draw);
if (!gpu_draw.through && !gpu_draw.clear_mode &&
primitive >= 3u && primitive <= 5u) {
const std::array<float, 6> cloud_viewport{
decode_float24(data24(commands[0x42u])),
decode_float24(data24(commands[0x43u])),
decode_float24(data24(commands[0x45u])),
decode_float24(data24(commands[0x46u])),
static_cast<float>(data24(commands[0x4Cu]) & 0xFFFFu) / 16.0f,
static_cast<float>(data24(commands[0x4Du]) & 0xFFFFu) / 16.0f};
// VCS' authoritative camera origin. The affine GE view used by
// individual passes is not guaranteed to encode this position as
// a rigid inverse (reflections and camera-relative passes do not),
// which made a world-space cloud slab orbit while only turning.
constexpr std::uint32_t kVcsCameraPosition = 0x08BC87E0u;
std::array<float, 3> cloud_camera_position{};
if (memory.contains(kVcsCameraPosition, 12u)) {
cloud_camera_position = {
std::bit_cast<float>(memory.load32(kVcsCameraPosition + 0u)),
std::bit_cast<float>(memory.load32(kVcsCameraPosition + 4u)),
std::bit_cast<float>(memory.load32(kVcsCameraPosition + 8u))};
}
ge_gpu_backend_observe_camera(transform.view, transform.projection,
cloud_viewport, cloud_camera_position,
gpu_draw, count);
}
if (!gpu_draw.clear_mode && primitive >= 3u && primitive <= 6u)
fps_overlay_observe_draw(gpu_draw, count);
if (!gpu_draw.through && !gpu_draw.clear_mode &&
+59
View File
@@ -106,12 +106,70 @@ bool parse_u64(std::string value, std::uint64_t minimum, std::uint64_t maximum,
return true;
}
bool parse_float(std::string value, float minimum, float maximum, float &out) {
value = trim_copy(std::move(value));
if (value.empty()) return false;
errno = 0;
char *end = nullptr;
const float parsed = std::strtof(value.c_str(), &end);
if (errno == ERANGE || end == value.c_str() || *end != '\0' ||
!std::isfinite(parsed) || parsed < minimum || parsed > maximum) {
return false;
}
out = parsed;
return true;
}
void warning(VcsConfiguration &config, std::size_t line, const std::string &message) {
std::ostringstream stream;
stream << "line " << line << ": " << message;
config.warnings.push_back(stream.str());
}
void load_proper_shaders_configuration(VcsConfiguration &config,
const std::filesystem::path &path) {
std::ifstream input(path);
if (!input) return;
std::string section;
std::string raw_line;
std::size_t line_number = 0u;
while (std::getline(input, raw_line)) {
++line_number;
std::string line = trim_copy(raw_line);
if (line.empty() || line[0] == ';' || line[0] == '#') continue;
if (line.front() == '[' && line.back() == ']') {
section = lowercase_copy(trim_copy(line.substr(1u, line.size() - 2u)));
continue;
}
if (section != "volumetricclouds" && section != "volumetric clouds") continue;
const std::size_t separator = line.find('=');
if (separator == std::string::npos) {
warning(config, line_number, "ProperShaders.ini: expected key=value");
continue;
}
const std::string key = lowercase_copy(trim_copy(line.substr(0u, separator)));
const std::string value = strip_inline_comment(line.substr(separator + 1u));
auto bad_float = [&](const char *name) {
warning(config, line_number, std::string("ProperShaders.ini: invalid ") + name);
};
if (key == "enabled") {
if (!parse_bool(value, config.volumetric_clouds.enabled))
warning(config, line_number, "ProperShaders.ini: Enabled expects true/false");
} else if (key == "marchsteps") {
if (!parse_u32(value, 4u, 64u, config.volumetric_clouds.march_steps))
warning(config, line_number, "ProperShaders.ini: MarchSteps must be between 4 and 64");
} else if (key == "coverage") {
if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.coverage)) bad_float("Coverage");
} else if (key == "opacity") {
if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.opacity)) bad_float("Opacity");
} else if (key == "speed") {
if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.speed)) bad_float("Speed");
} else {
warning(config, line_number, "ProperShaders.ini: unknown [VolumetricClouds] key '" + key + "'");
}
}
}
void apply_display_key(VcsConfiguration &config, const std::string &key,
const std::string &value, std::size_t line) {
if (key == "enabled") {
@@ -635,6 +693,7 @@ void initialize_vcs_configuration(const std::filesystem::path &executable_direct
}
VcsConfiguration loaded = load_vcs_configuration(path);
load_proper_shaders_configuration(loaded, executable_directory / "ProperShaders.ini");
loaded.initialized = true;
loaded.executable_directory = executable_directory;
{
+12
View File
@@ -173,6 +173,17 @@ struct WidescreenConfiguration {
std::uint32_t aspect_y{0u};
};
// Standalone ProperShaders.ini feature. These values deliberately do not read
// the guest timecycle/weather: the first port is a fixed, independently
// configurable CloudWorks sky layer.
struct VolumetricCloudsConfiguration {
bool enabled{false};
std::uint32_t march_steps{20u};
float coverage{0.52f};
float opacity{0.78f};
float speed{0.018f};
};
// The aspect the game itself builds its projection with. VCS loads the
// constant 0x3FE38E39 -- exactly 16/9 -- and everything is relative to it.
inline constexpr float kGameNativeAspectRatio = 16.0f / 9.0f;
@@ -211,6 +222,7 @@ struct VcsConfiguration {
TimingConfiguration timing{};
DiagnosticsConfiguration diagnostics{};
WidescreenConfiguration widescreen{};
VolumetricCloudsConfiguration volumetric_clouds{};
std::filesystem::path source_path{};
// Where the executable lives. Saves go beside it rather than into the game
// data, so a player who points the runtime at a read-only or shared copy of
+17
View File
@@ -153,6 +153,23 @@ int main() {
const vcs::VcsConfiguration missing =
vcs::load_vcs_configuration(root / "missing.ini");
{
std::ofstream proper(root / "ProperShaders.ini", std::ios::trunc);
proper << "[VolumetricClouds]\n"
<< "Enabled=true\n"
<< "MarchSteps=28\n"
<< "Coverage=0.61\n"
<< "Opacity=0.72\n"
<< "Speed=0.03\n";
}
vcs::initialize_vcs_configuration(root);
const auto &clouds = vcs::vcs_configuration().volumetric_clouds;
require(clouds.enabled, "ProperShaders.ini VolumetricClouds.Enabled was not parsed");
require(clouds.march_steps == 28u, "ProperShaders.ini MarchSteps was not parsed");
require(std::abs(clouds.coverage - 0.61f) < 0.0001f &&
std::abs(clouds.opacity - 0.72f) < 0.0001f &&
std::abs(clouds.speed - 0.03f) < 0.0001f,
"ProperShaders.ini cloud parameters were not parsed");
// Widescreen: explicit ratio, "auto", and off. The correction must be
// exactly neutral when disabled -- PSP parity stays the baseline.
require(config.widescreen.enabled, "Widescreen.Enabled was not parsed");
+14
View File
@@ -0,0 +1,14 @@
# CloudWorks attribution
The volumetric-cloud density and noise model in the VCS DirectX 12 presenter is
adapted from **CloudWorks Alpha 4.0** by Brian Tu (RTU), dated 2021-07-25.
Original project: <https://github.com/keroroxzz>
The original shader identifies its license as Creative Commons
Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0):
<https://creativecommons.org/licenses/by-nc-sa/3.0/>
This adaptation replaces RenderHook/timecycle inputs with fixed parameters from
`ProperShaders.ini`, uses the PSP GE camera captured by VCSNative, and composites
the result in the native DirectX 12 presentation pass.