Skip to content

Commit 1176d79

Browse files
Spruill-1Copilot
andcommitted
ShaderLab Scale + Image Info effects, revert D2D Scale changes
Three related changes shipped together: 1) Revert the D2D CLSID_D2D1Scale registry expansion. Per user direction, our effect catalog should not "wrap" D2D effects with modified surfaces. The D2D Scale entry goes back to exposing only what it always did (Scale + CenterPoint). 2) New ShaderLab "Scale" effect (D3D11 compute, category "Color", subcategory "Resampling"). Implements filter algorithms beyond D2Ds native set: 0 = Bilinear (1-tap, fast) 1 = Catmull-Rom Bicubic (4-tap, sharp with mild ringing) 2 = Mitchell-Netravali (4-tap, B=1/3 C=1/3, balanced) 3 = Lanczos-3 (6-tap, sharpest) <-- default 4 = Box / Area (1-tap, no ringing; widens for downscale) 5 = Gaussian (4-tap, smooth) Output dimensions are explicit OutputWidth + OutputHeight cbuffer parameters; 0 means "pass through at source dims" (no scale). For downscale the kernel widens by the source/dest ratio so the filter integrates over the destination footprint instead of point-sampling (which would alias). Composes with Image Info + Numeric Expression to drive output dims from the source -- e.g. ImageInfo.Width through "A * 0.5" into Scale.OutputWidth. 3) New "Image Info" effect (D3D11 compute, analysis-only, category "Analysis", subcategory "Statistics"). Exposes the source images dimensions and a couple of derived quantities as typed analysis fields: Width (float, pixels) Height (float, pixels) AspectRatio (float, w/h) PixelCount (float, w*h) The runner already auto-injects Width / Height into the cbuffer from the input texture; this shader just copies them into the result buffer. General-purpose primitive: lets graphs wire any source- dimension-derived parameter (Scale output dims, fixed-aspect crops, per-frame normalization scale factors) without bespoke evaluator hooks. Verified live on HDR Test Pattern: Width=512, Height=512, AspectRatio=1, PixelCount=262144. 154/154 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a86409a commit 1176d79

2 files changed

Lines changed: 238 additions & 17 deletions

File tree

Effects/EffectRegistry.cpp

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -388,23 +388,12 @@ namespace ShaderLab::Effects
388388
.category = L"Transform",
389389
.inputPins = SINGLE_INPUT,
390390
.defaultProperties = {
391-
{ L"Scale", Numerics::float2{ 1.0f, 1.0f } },
392-
{ L"CenterPoint", Numerics::float2{ 0.0f, 0.0f } },
393-
// D2D1_SCALE_INTERPOLATION_MODE: 0=NearestNeighbor, 1=Linear,
394-
// 2=Cubic, 3=MultiSampleLinear, 4=Anisotropic, 5=HighQualityCubic.
395-
// Default to HighQualityCubic (Catmull-Rom): sharp at non-integer
396-
// ratios with controlled mild ringing, HDR-safe in scRGB FP16.
397-
{ L"InterpolationMode", static_cast<uint32_t>(5) },
398-
// D2D1_BORDER_MODE: 0=Soft (transparent edges), 1=Hard (clamp).
399-
{ L"BorderMode", static_cast<uint32_t>(0) },
400-
{ L"Sharpness", 0.0f },
401-
},
402-
.propertyMetadata = {
403-
{ L"Scale", { .uiHint = PropertyUIHint::VectorEditor, .minValue = 0.01f, .maxValue = 10.0f, .step = 0.01f, .componentLabels = { L"X", L"Y" } } },
404-
{ L"CenterPoint", { .uiHint = PropertyUIHint::VectorEditor, .step = 1.0f, .componentLabels = { L"X", L"Y" } } },
405-
{ L"InterpolationMode", { .uiHint = PropertyUIHint::ComboBox, .enumLabels = { L"Nearest Neighbor", L"Bilinear", L"Bicubic", L"Multi-Sample Linear", L"Anisotropic", L"High Quality Bicubic" } } },
406-
{ L"BorderMode", { .uiHint = PropertyUIHint::ComboBox, .enumLabels = { L"Soft (transparent)", L"Hard (clamp)" } } },
407-
{ L"Sharpness", { .uiHint = PropertyUIHint::Slider, .minValue = 0.0f, .maxValue = 1.0f, .step = 0.05f } },
391+
{ L"Scale", Numerics::float2{ 1.0f, 1.0f } },
392+
{ L"CenterPoint", Numerics::float2{ 0.0f, 0.0f } },
393+
},
394+
.propertyMetadata = {
395+
{ L"Scale", { .uiHint = PropertyUIHint::VectorEditor, .minValue = 0.01f, .maxValue = 10.0f, .step = 0.01f, .componentLabels = { L"X", L"Y" } } },
396+
{ L"CenterPoint", { .uiHint = PropertyUIHint::VectorEditor, .step = 1.0f, .componentLabels = { L"X", L"Y" } } },
408397
},
409398
});
410399

Effects/ShaderLabEffects.cpp

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3174,6 +3174,238 @@ void main(uint3 GTid : SV_GroupThreadID)
31743174
m_effects.push_back(std::move(desc));
31753175
}
31763176

3177+
// ---- ShaderLab Scale (Resampling) ----
3178+
// D3D11 compute scale effect with filter algorithms beyond what D2D's
3179+
// CLSID_D2D1Scale natively offers (Lanczos-3, Mitchell-Netravali,
3180+
// Catmull-Rom, Box / area, Gaussian). Useful as a hand-inserted
3181+
// resolution cap in heavy graphs: insert between Source and the
3182+
// visual branch and the entire downstream chain renders at the
3183+
// smaller resolution.
3184+
//
3185+
// Output dimensions are explicitly OutputWidth + OutputHeight. To
3186+
// drive them from source dimensions, add an Image Info node on the
3187+
// same input and route ImageInfo.Width / .Height through Numeric
3188+
// Expression nodes (e.g. expr "A * 0.5") into Scale.OutputWidth /
3189+
// .OutputHeight.
3190+
//
3191+
// OutputWidth = 0 or OutputHeight = 0 falls back to the source's
3192+
// bounds (no scaling).
3193+
{
3194+
static const std::string scaleHLSL = R"HLSL(
3195+
// ShaderLab Scale -- D3D11 compute resampler with multi-tap filters.
3196+
// Output pixel (x,y) reads a windowed neighborhood around the corresponding
3197+
// source location and combines the samples with the chosen kernel.
3198+
3199+
Texture2D<float4> Source : register(t0);
3200+
RWTexture2D<float4> ImageOutput : register(u1);
3201+
3202+
cbuffer constants : register(b0) {
3203+
uint Width; // source dims (auto-injected from input #0)
3204+
uint Height;
3205+
uint OutputWidth; // 0 = pass-through (= source Width)
3206+
uint OutputHeight; // 0 = pass-through (= source Height)
3207+
uint FilterMode; // 0=Bilinear 1=CatmullRom 2=Mitchell 3=Lanczos3 4=Box 5=Gaussian
3208+
};
3209+
3210+
// ---- Filter kernels (1D weights; separable for 2D) ---------------------
3211+
3212+
float W_bilinear(float t) { return max(0.0, 1.0 - abs(t)); }
3213+
3214+
float W_catmullrom(float t) {
3215+
t = abs(t);
3216+
if (t < 1.0) return 1.5*t*t*t - 2.5*t*t + 1.0;
3217+
if (t < 2.0) return -0.5*t*t*t + 2.5*t*t - 4.0*t + 2.0;
3218+
return 0.0;
3219+
}
3220+
3221+
// Mitchell-Netravali B=1/3, C=1/3 -- balanced sharpness/ringing default.
3222+
float W_mitchell(float t) {
3223+
t = abs(t);
3224+
const float B = 1.0/3.0, C = 1.0/3.0;
3225+
if (t < 1.0) {
3226+
return ((12.0 - 9.0*B - 6.0*C)*t*t*t
3227+
+ (-18.0 + 12.0*B + 6.0*C)*t*t
3228+
+ (6.0 - 2.0*B)) / 6.0;
3229+
}
3230+
if (t < 2.0) {
3231+
return ((-B - 6.0*C)*t*t*t
3232+
+ (6.0*B + 30.0*C)*t*t
3233+
+ (-12.0*B - 48.0*C)*t
3234+
+ (8.0*B + 24.0*C)) / 6.0;
3235+
}
3236+
return 0.0;
3237+
}
3238+
3239+
float W_lanczos3(float t) {
3240+
t = abs(t);
3241+
if (t < 1e-5) return 1.0;
3242+
if (t >= 3.0) return 0.0;
3243+
float pt = 3.14159265 * t;
3244+
return 3.0 * sin(pt) * sin(pt / 3.0) / (pt * pt);
3245+
}
3246+
3247+
float W_box(float t, float halfWidth) {
3248+
return abs(t) <= halfWidth ? 1.0 : 0.0;
3249+
}
3250+
3251+
float W_gauss(float t) {
3252+
// sigma = 0.5 -- tight kernel. Effective radius widens with downscale
3253+
// ratio via the support multiplier in main().
3254+
return exp(-(t*t) * 2.0); // = exp(-t^2 / (2*0.5^2))
3255+
}
3256+
3257+
[numthreads(8, 8, 1)]
3258+
void main(uint3 dtid : SV_DispatchThreadID)
3259+
{
3260+
// Pass-through when OutputWidth/Height not set: act as identity.
3261+
uint outW = (OutputWidth > 0) ? OutputWidth : Width;
3262+
uint outH = (OutputHeight > 0) ? OutputHeight : Height;
3263+
if (dtid.x >= outW || dtid.y >= outH) return;
3264+
3265+
// Output pixel center -> source-pixel-center coords.
3266+
float2 outCenter = float2(dtid.xy) + 0.5;
3267+
float2 outSize = float2(outW, outH);
3268+
float2 srcSize = float2(Width, Height);
3269+
float2 ratio = srcSize / outSize; // src per dst pixel
3270+
float2 srcCenter = outCenter * ratio - 0.5;
3271+
3272+
// For downscale (ratio > 1) we widen the kernel by the ratio so the
3273+
// filter integrates over the *destination footprint* in source pixels;
3274+
// otherwise samples alias. For upscale (ratio < 1) we keep support = 1.
3275+
float2 support = max(ratio, float2(1.0, 1.0));
3276+
3277+
// Per-mode base radius in kernel domain (half-width of the kernel).
3278+
uint mode = FilterMode;
3279+
float baseRadius = 1.0;
3280+
if (mode == 1) baseRadius = 2.0; // Catmull-Rom
3281+
else if (mode == 2) baseRadius = 2.0; // Mitchell
3282+
else if (mode == 3) baseRadius = 3.0; // Lanczos-3
3283+
else if (mode == 4) baseRadius = 0.5; // Box (half-width)
3284+
else if (mode == 5) baseRadius = 2.0; // Gaussian (truncated)
3285+
3286+
// Effective radius in source pixels after widening for downscale.
3287+
float2 effR = baseRadius * support;
3288+
int2 r = int2(ceil(effR));
3289+
r = clamp(r, int2(1, 1), int2(12, 12)); // safety cap
3290+
3291+
int2 ic = int2(floor(srcCenter));
3292+
3293+
float4 sum = float4(0, 0, 0, 0);
3294+
float wsum = 0.0;
3295+
3296+
[loop]
3297+
for (int dy = -r.y; dy <= r.y; ++dy)
3298+
{
3299+
[loop]
3300+
for (int dx = -r.x; dx <= r.x; ++dx)
3301+
{
3302+
int2 sxy = clamp(ic + int2(dx, dy),
3303+
int2(0, 0),
3304+
int2(int(Width) - 1, int(Height) - 1));
3305+
float2 t = (float2(sxy) + 0.5 - srcCenter) / support;
3306+
3307+
float wx, wy;
3308+
if (mode == 0) { wx = W_bilinear(t.x); wy = W_bilinear(t.y); }
3309+
else if (mode == 1) { wx = W_catmullrom(t.x); wy = W_catmullrom(t.y); }
3310+
else if (mode == 2) { wx = W_mitchell(t.x); wy = W_mitchell(t.y); }
3311+
else if (mode == 3) { wx = W_lanczos3(t.x); wy = W_lanczos3(t.y); }
3312+
else if (mode == 4) { wx = W_box(t.x, 0.5); wy = W_box(t.y, 0.5); }
3313+
else { wx = W_gauss(t.x); wy = W_gauss(t.y); }
3314+
3315+
float w = wx * wy;
3316+
sum += Source.Load(int3(sxy, 0)) * w;
3317+
wsum += w;
3318+
}
3319+
}
3320+
3321+
ImageOutput[dtid.xy] = (wsum > 1e-6) ? (sum / wsum) : float4(0, 0, 0, 0);
3322+
}
3323+
)HLSL";
3324+
ShaderLabEffectDescriptor desc;
3325+
desc.name = L"Scale";
3326+
desc.effectId = L"ShaderLab Scale"; desc.effectVersion = 1;
3327+
desc.category = L"Color";
3328+
desc.subcategory = L"Resampling";
3329+
desc.shaderType = Graph::CustomShaderType::D3D11ComputeShader;
3330+
desc.hasImageOutput = true;
3331+
desc.threadGroupX = 8;
3332+
desc.threadGroupY = 8;
3333+
desc.threadGroupZ = 1;
3334+
desc.hlslSource = scaleHLSL;
3335+
desc.inputNames = { L"Source" };
3336+
desc.parameters = {
3337+
// OutputWidth / OutputHeight are the primary controls.
3338+
// 0 means "pass through at source dims". To drive them
3339+
// from source dimensions, bind from an Image Info node
3340+
// (see catalog) through a Numeric Expression.
3341+
{ L"OutputWidth", L"uint", 0.0f, 0.0f, 16384.0f, 1.0f },
3342+
{ L"OutputHeight", L"uint", 0.0f, 0.0f, 16384.0f, 1.0f },
3343+
{ L"FilterMode", L"float", 3.0f, 0.0f, 5.0f, 1.0f,
3344+
{ L"Bilinear", L"Catmull-Rom Bicubic", L"Mitchell-Netravali",
3345+
L"Lanczos-3", L"Box (Area)", L"Gaussian" } },
3346+
};
3347+
m_effects.push_back(std::move(desc));
3348+
}
3349+
3350+
// ---- Image Info ----
3351+
// Analysis-only compute that exposes its source's dimensions and a
3352+
// few derived quantities as typed analysis fields. The runner
3353+
// auto-injects Width / Height into the cbuffer based on the input
3354+
// texture; this shader just copies them into the result buffer
3355+
// alongside two convenience derivations.
3356+
//
3357+
// Composes naturally with Numeric Expression to drive parameters
3358+
// that should track the source -- e.g. ImageInfo.Width through
3359+
// "A * 0.5" into Scale.OutputWidth for a half-resolution pass.
3360+
{
3361+
static const std::string imageInfoHLSL = R"HLSL(
3362+
// Image Info -- D3D11 compute, exposes source dims as analysis fields.
3363+
3364+
Texture2D<float4> Source : register(t0);
3365+
RWStructuredBuffer<float4> Result : register(u0);
3366+
3367+
cbuffer constants : register(b0) {
3368+
uint Width;
3369+
uint Height;
3370+
};
3371+
3372+
[numthreads(1, 1, 1)]
3373+
void main()
3374+
{
3375+
float w = float(Width);
3376+
float h = float(Height);
3377+
float aspect = (h > 0.5) ? (w / h) : 0.0;
3378+
float pixels = w * h;
3379+
// Each analysis field maps to its own Result[i].x slot.
3380+
Result[0] = float4(w, 0, 0, 0); // Width
3381+
Result[1] = float4(h, 0, 0, 0); // Height
3382+
Result[2] = float4(aspect, 0, 0, 0); // AspectRatio
3383+
Result[3] = float4(pixels, 0, 0, 0); // PixelCount
3384+
}
3385+
)HLSL";
3386+
ShaderLabEffectDescriptor desc;
3387+
desc.name = L"Image Info";
3388+
desc.effectId = L"Image Info"; desc.effectVersion = 1;
3389+
desc.category = L"Analysis";
3390+
desc.subcategory = L"Statistics";
3391+
desc.shaderType = Graph::CustomShaderType::D3D11ComputeShader;
3392+
desc.dataOnly = true;
3393+
desc.hasImageOutput = false;
3394+
desc.threadGroupX = 1;
3395+
desc.threadGroupY = 1;
3396+
desc.threadGroupZ = 1;
3397+
desc.hlslSource = imageInfoHLSL;
3398+
desc.inputNames = { L"Source" };
3399+
desc.analysisOutputType = Graph::AnalysisOutputType::Typed;
3400+
desc.analysisFields = {
3401+
{ L"Width", Graph::AnalysisFieldType::Float },
3402+
{ L"Height", Graph::AnalysisFieldType::Float },
3403+
{ L"AspectRatio", Graph::AnalysisFieldType::Float },
3404+
{ L"PixelCount", Graph::AnalysisFieldType::Float },
3405+
};
3406+
m_effects.push_back(std::move(desc));
3407+
}
3408+
31773409
// ---- Working Space Parameter Node ----
31783410
// A first-class, host-driven parameter node that mirrors the active
31793411
// display profile (live or simulated) as 14 typed analysis output

0 commit comments

Comments
 (0)