From b1dced7c36e827a4c5393b88ce73148aeb324104 Mon Sep 17 00:00:00 2001 From: sdcb Date: Fri, 16 Oct 2026 19:40:32 +0800 Subject: [PATCH 1/3] CPU path: pixels <-> tensors through Sdcb.SimdOnnx.Imaging.ImageTensor (FromPixels with replicate padding, ToPixels with crop and alpha copy; bit-identical to the removed PixelOps converters on 160 random cases x 4 formats x both layouts; the NCHW variants now also split rows across threads). CPU / Vulkan e2e at parity with main. Needs the next Sdcb.SimdOnnx preview Co-Authored-By: Claude Opus 5.5 --- src/Sdcb.SimdGcdrNet/GcdrEnhancer.cs | 16 ++- src/Sdcb.SimdGcdrNet/Kernels/PixelOps.cs | 142 +---------------------- 2 files changed, 12 insertions(+), 146 deletions(-) diff --git a/src/Sdcb.SimdGcdrNet/GcdrEnhancer.cs b/src/Sdcb.SimdGcdrNet/GcdrEnhancer.cs index 4028547..3d12d5a 100644 --- a/src/Sdcb.SimdGcdrNet/GcdrEnhancer.cs +++ b/src/Sdcb.SimdGcdrNet/GcdrEnhancer.cs @@ -3,6 +3,8 @@ using Sdcb.SimdGcdrNet.Extensions; using Sdcb.SimdGcdrNet.Kernels; using Sdcb.SimdOnnx.Engine; +using Sdcb.SimdOnnx.Imaging; +using Sdcb.SimdOnnx.Kernels; using Sdcb.SimdOnnx.Onnx; using VkBackend = Sdcb.SimdOnnx.Backends.Vulkan.VkBackend; using VkDeviceLostException = Sdcb.SimdOnnx.Backends.Vulkan.VkDeviceLostException; @@ -99,6 +101,8 @@ public static GcdrEnhancer FromStreams(Stream gcnetOnnx, Stream drnetOnnx, // pipeline sessions keep I/O in plan layout: the glue kernels below read/write // channels-last directly, so the plan has no boundary relayouts + // the network sees B,G,R in [0, 1] + private static readonly PixelNormalization s_unit = PixelNormalization.Unit(); private static readonly CompileOptions CpuPlan = new(CompileOptions.DefaultNhwc, PhysicalIo: true, Extensions: GcdrExtensions.Instance); private static bool CpuNhwc => CpuPlan.Nhwc; @@ -235,8 +239,9 @@ public void Enhance(ReadOnlySpan src, int width, int height, int srcStride // stage 0: fill GC input — BGR u8 → float /255, top/left replicate pad Span x = gc.Input; - if (CpuNhwc) PixelOps.BgrToNhwcPad(src, width, height, srcStride, x, h, w, ph, pw, bpp, swap); - else PixelOps.BgrToNchwPad(src, width, height, srcStride, x, h, w, ph, pw, bpp, swap); + TensorLayout layout = CpuNhwc ? TensorLayout.Nhwc : TensorLayout.Nchw; + ImageTensor.FromPixels(src, width, height, srcStride, (PixelFormat)format, x, layout, s_unit, + w, h, padLeft: pw, padTop: ph, threads: Avx2Kernels.Threads); // stage 1: GC-Net → shading map gc.Run(); @@ -259,10 +264,9 @@ public void Enhance(ReadOnlySpan src, int width, int height, int srcStride dr.Run(); // stage 4: crop pad → u8 in the caller's format (alpha from src) - if (CpuNhwc) - PixelOps.NhwcToBgrCrop(dr.Output, h, w, ph, pw, dst, width, height, dstStride, bpp, swap, src, srcStride); - else - PixelOps.NchwToBgrCrop(dr.Output, h, w, ph, pw, dst, width, height, dstStride, bpp, swap, src, srcStride); + ImageTensor.ToPixels(dr.Output, layout, w, h, dst, width, height, dstStride, (PixelFormat)format, + cropLeft: pw, cropTop: ph, alphaSource: bpp == 4 ? src : default, alphaStride: srcStride, + threads: Avx2Kernels.Threads); } finally { cpuPool.Return(worker); } } diff --git a/src/Sdcb.SimdGcdrNet/Kernels/PixelOps.cs b/src/Sdcb.SimdGcdrNet/Kernels/PixelOps.cs index 8ff88dd..55fed60 100644 --- a/src/Sdcb.SimdGcdrNet/Kernels/PixelOps.cs +++ b/src/Sdcb.SimdGcdrNet/Kernels/PixelOps.cs @@ -5,38 +5,11 @@ namespace Sdcb.SimdGcdrNet.Kernels; /// -/// Pipeline glue kernels: BGR↔NCHW float conversion, replicate padding, -/// division correction, output crop. All vectorized over the innermost dim. +/// Pipeline glue kernels between the two nets (division correction, concat) and the Metal +/// pipeline's BGR24 rows; pixels ↔ tensors go through Sdcb.SimdOnnx.Imaging.ImageTensor. /// internal static class PixelOps { - /// - /// Interleaved u8 HWC ( 3 or 4 bytes per pixel, B,G,R order unless - /// ) → float NCHW /255 with top/left replicate padding. - /// dst layout: [3][h][w] float, channels B,G,R. - /// - public static void BgrToNchwPad(ReadOnlySpan src, int width, int height, int srcStride, - Span dst, int h, int w, int padTop, int padLeft, int bpp = 3, bool swapRB = false) - { - int hw = h * w; - int ob = swapRB ? 2 : 0, or = 2 - ob; - for (int y = 0; y < h; y++) - { - int sy = Math.Max(0, y - padTop); - ReadOnlySpan row = src.Slice(sy * srcStride, width * bpp); - Span d0 = dst.Slice(y * w, w); - Span d1 = dst.Slice(hw + y * w, w); - Span d2 = dst.Slice(2 * hw + y * w, w); - for (int x = 0; x < w; x++) - { - int sx = Math.Clamp(x - padLeft, 0, width - 1) * bpp; - d0[x] = row[sx + ob] / 255f; - d1[x] = row[sx + 1] / 255f; - d2[x] = row[sx + or] / 255f; - } - } - } - /// /// corrected = clamp(x / max(shadow, 1e-6), 0, 1) for all 3·hw elements. /// @@ -58,37 +31,6 @@ public static void DivClampInto(ReadOnlySpan x, ReadOnlySpan shado dst[i] = Math.Clamp(x[i] / Math.Max(shadow[i], 1e-6f), 0f, 1f); } - /// - /// float NCHW [3,h,w] (B,G,R) → interleaved u8 HWC, cropping (padTop,padLeft) margins. - /// 4: the alpha byte of each pixel is copied from - /// (same layout, row stride ). - /// - public static void NchwToBgrCrop(ReadOnlySpan src, int h, int w, - int padTop, int padLeft, Span dst, int width, int height, int dstStride, - int bpp = 3, bool swapRB = false, ReadOnlySpan alphaSrc = default, int alphaStride = 0) - { - int hw = h * w; - int ob = swapRB ? 2 : 0, or = 2 - ob; - for (int y = 0; y < height; y++) - { - int sy = y + padTop; - ReadOnlySpan r0 = src.Slice(sy * w, w); - ReadOnlySpan r1 = src.Slice(hw + sy * w, w); - ReadOnlySpan r2 = src.Slice(2 * hw + sy * w, w); - Span drow = dst.Slice(y * dstStride, width * bpp); - ReadOnlySpan arow = bpp == 4 ? alphaSrc.Slice(y * alphaStride, width * 4) : default; - for (int x = 0; x < width; x++) - { - int sx = x + padLeft, d = x * bpp; - drow[d + ob] = ToU8(r0[sx]); - drow[d + 1] = ToU8(r1[sx]); - drow[d + or] = ToU8(r2[sx]); - if (bpp == 4) drow[d + 3] = arow[d + 3]; - } - } - static byte ToU8(float v) => (byte)Math.Clamp((int)(v * 255f + 0.5f), 0, 255); - } - // The NHWC glue below runs once per image on the calling thread between the two nets; // single-threaded it was ~90 ms of a ~630 ms 3.7 MP frame on 32 threads. Every element is // independent, so row bands go wide (same per-element ops, same bits). @@ -102,39 +44,6 @@ private static void Rows(int rows, long elemsPerRow, Action body) Parallel.For(0, bands, PO, b => body((int)((long)rows * b / bands), (int)((long)rows * (b + 1) / bands))); } - /// - /// Interleaved u8 HWC ( 3 or 4 bytes per pixel, B,G,R order unless - /// ) → float NHWC [h][w][3] (B,G,R) /255 with top/left replicate padding. - /// - public static unsafe void BgrToNhwcPad(ReadOnlySpan src, int width, int height, int srcStride, - Span dst, int h, int w, int padTop, int padLeft, int bpp = 3, bool swapRB = false) - { - if (src.Length < (long)(height - 1) * srcStride + width * bpp || dst.Length < (long)h * w * 3) - throw new ArgumentException("pad buffer sizes"); - int ob = swapRB ? 2 : 0, or = 2 - ob; - fixed (byte* sp0 = src) - fixed (float* dp0 = dst) - { - byte* sp = sp0; - float* dp = dp0; - Rows(h, (long)w * 3, (y0, y1) => - { - for (int y = y0; y < y1; y++) - { - byte* row = sp + (long)Math.Max(0, y - padTop) * srcStride; - float* d = dp + (long)y * w * 3; - for (int x = 0; x < w; x++) - { - int sx = Math.Clamp(x - padLeft, 0, width - 1) * bpp; - d[x * 3] = row[sx + ob] / 255f; - d[x * 3 + 1] = row[sx + 1] / 255f; - d[x * 3 + 2] = row[sx + or] / 255f; - } - } - }); - } - } - /// /// NHWC div6: dst[px,0..2] = x[px], dst[px,3..5] = clamp(x / max(shadow,1e-6), 0, 1). /// x / shadow are [hw][3]; dst is [hw][6]. @@ -162,53 +71,6 @@ public static unsafe void DivConcatNhwc(ReadOnlySpan x, ReadOnlySpan - /// float NHWC [h][w][3] (B,G,R) → interleaved u8 HWC, cropping (padTop,padLeft) margins. - /// 4: the alpha byte of each pixel is copied from - /// (same layout, row stride ; - /// may alias ). - /// - public static unsafe void NhwcToBgrCrop(ReadOnlySpan src, int h, int w, - int padTop, int padLeft, Span dst, int width, int height, int dstStride, - int bpp = 3, bool swapRB = false, ReadOnlySpan alphaSrc = default, int alphaStride = 0) - { - if (src.Length < (long)h * w * 3 || dst.Length < (long)(height - 1) * dstStride + width * bpp) - throw new ArgumentException("crop buffer sizes"); - if (bpp == 4 && alphaSrc.Length < (long)(height - 1) * alphaStride + width * 4) - throw new ArgumentException("alpha source size"); - int ob = swapRB ? 2 : 0, or = 2 - ob; - fixed (float* sp0 = src) - fixed (byte* dp0 = dst, ap0 = alphaSrc) - { - float* sp = sp0; - byte* dp = dp0, ap = ap0; - Rows(height, (long)width * 3, (y0, y1) => - { - for (int y = y0; y < y1; y++) - { - float* r = sp + ((long)(y + padTop) * w + padLeft) * 3; - byte* drow = dp + (long)y * dstStride; - if (bpp == 3 && !swapRB) - { - for (int i = 0; i < width * 3; i++) - drow[i] = ToU8(r[i]); - continue; - } - byte* arow = ap + (long)y * alphaStride; - for (int x = 0; x < width; x++) - { - byte* d = drow + x * bpp; - d[ob] = ToU8(r[x * 3]); - d[1] = ToU8(r[x * 3 + 1]); - d[or] = ToU8(r[x * 3 + 2]); - if (bpp == 4) d[3] = arow[x * 4 + 3]; - } - } - }); - } - static byte ToU8(float v) => (byte)Math.Clamp((int)(v * 255f + 0.5f), 0, 255); - } - /// One row of interleaved u8 pixels in → tightly packed BGR24. public static void RowToBgr24(ReadOnlySpan src, Span bgr, int width, GcdrPixelFormat format) { From 3269f1d77cf2b5db8ce92134ebcbc75270186735 Mon Sep 17 00:00:00 2001 From: sdcb Date: Fri, 16 Oct 2026 20:22:23 +0800 Subject: [PATCH 2/3] Pin Sdcb.SimdOnnx 0.1.0-preview.5 Co-Authored-By: Claude Opus 5.5 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 914a541..cc0eaca 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,7 @@ 1.0.0 - 0.1.0-preview.4 + 0.1.0-preview.5 $(MSBuildThisFileDirectory)..\Sdcb.SimdOnnx\src\Sdcb.SimdOnnx\Sdcb.SimdOnnx.csproj From 1c1d181ac278d5fee0d6cc4f670d67e5519073c6 Mon Sep 17 00:00:00 2001 From: sdcb Date: Sat, 17 Oct 2026 13:57:26 +0800 Subject: [PATCH 3/3] Pin Sdcb.SimdOnnx 0.1.0-preview.6; README: PixelOps no longer converts pixels; drop an unused local (review) Co-Authored-By: Claude Opus 5.5 --- Directory.Build.props | 2 +- README.md | 3 ++- src/Sdcb.SimdGcdrNet/GcdrEnhancer.cs | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index cc0eaca..8b31436 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,7 @@ 1.0.0 - 0.1.0-preview.5 + 0.1.0-preview.6 $(MSBuildThisFileDirectory)..\Sdcb.SimdOnnx\src\Sdcb.SimdOnnx\Sdcb.SimdOnnx.csproj diff --git a/README.md b/README.md index 883ba42..87c2f9e 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,8 @@ The ONNX engine (protobuf reader, graph compiler, CPU SIMD kernels, Vulkan / Met - `src/Sdcb.SimdGcdrNet/Extensions` — engine extensions (`GcdrExtensions`): fused operators (`UpHeadOp`, `ChannelShiftOp`) with their CPU kernels (Vector / AVX-512 / NEON / WASM SIMD) and graph passes -- `src/Sdcb.SimdGcdrNet/Kernels` — `PixelOps` (interleaved pixels ↔ tensors, padding) +- `src/Sdcb.SimdGcdrNet/Kernels` — `PixelOps` (glue between the two nets — division correction / concat — and the + Metal pipeline's BGR24 rows; pixels ↔ tensors go through `Sdcb.SimdOnnx.Imaging.ImageTensor`) - `src/Sdcb.SimdGcdrNet/Backends/Vulkan` — `VkPipeline2` (fused gc + dr pipeline on SimdOnnx's Vulkan backend); `Shaders` holds the GCDRNet GLSL kernels, `tools/build-shaders.bat` recompiles `.comp` → `.spv` (committed; dev-time only, not part of `dotnet build`) diff --git a/src/Sdcb.SimdGcdrNet/GcdrEnhancer.cs b/src/Sdcb.SimdGcdrNet/GcdrEnhancer.cs index 3d12d5a..d5e4191 100644 --- a/src/Sdcb.SimdGcdrNet/GcdrEnhancer.cs +++ b/src/Sdcb.SimdGcdrNet/GcdrEnhancer.cs @@ -182,7 +182,6 @@ public void Enhance(ReadOnlySpan src, int width, int height, int srcStride Span dst, int dstStride, GcdrPixelFormat format = GcdrPixelFormat.Bgr24) { int bpp = format.BytesPerPixel(); - bool swap = format.SwapRB(); ArgumentOutOfRangeException.ThrowIfLessThan(width, 1); ArgumentOutOfRangeException.ThrowIfLessThan(height, 1); ArgumentOutOfRangeException.ThrowIfLessThan(srcStride, width * bpp);