View Full Version : [Idea] Zero-overhead 2D plane view abstractions in C++17 for AviSynth+ and filters


MeteorRain
30th August 2026, 03:00
In current AviSynth+ plugin development, video frame processing predominantly relies on raw pointer arithmetic and manual pitch tracking (GetReadPtr, GetWritePtr, GetPitch, and ptr += pitch). While this low-level model grants direct hardware-level control, developers must continuously manage manual byte-to-element conversions (pitch / sizeof(T)), repetitive offset calculations for sub-regions (ROI/cropping), and potential boundary errors across different bit depths.

To modernize the developer experience, AviSynth+ could introduce a compact, header-only view template that abstracts 2D strided frame buffers. Downstream filters could query ready-to-use plane view objects directly from video frames instead of querying detached pointers and byte strides. By wrapping pointer arithmetic and element scaling inside an inline value-type wrapper, filter logic can remain clean, expressive, and less susceptible to pointer misuse.

Proposed Conceptual Structure

The core architecture separates horizontal scanlines and vertical stepping into orthogonal components:

Row<T>: Represents a single horizontal scanline. By enforcing unit stride (S_x = 1) in the type system, it guarantees that compilers can auto-vectorize horizontal pixel loops without dynamic stride checks.
RowCursor<T>: Replaces manual pointer incrementation (ptr += pitch) with a type-safe line iterator that compiles to a single add instruction.
Plane<T>: A 24-byte non-owning 2D view (Pointer, Width, Height, Element Pitch) designed to be passed directly via CPU registers.
RestrictPlane<T> / as_restrict: Safe by default for in-place buffers (MakeWritable), with an explicit opt-in conversion path to inject __restrict semantics when source and destination are known to be disjoint.


namespace your_namespace_here {

// ============================================================================
// 1. Row<T> - 1D Contiguous Scanline View (Guarantees unit stride for SIMD)
// ============================================================================
template <typename T>
class Row {
public:
T* data(); // Pointer to first element in current line
size_t size(); // Number of elements in current line
bool empty(); // Check if line width is 0
T& operator[](size_t x); // Fast indexed element access
T* begin(); // Contiguous forward iterator (for range-for)
T* end(); // End iterator
Row subspan(size_t off, size_t n); // Sub-line slice (e.g. for horizontal margins)
};

// ============================================================================
// 2. RowCursor<T> - Vertical Line Stepper (Compiles directly to: ptr += pitch)
// ============================================================================
template <typename T>
class RowCursor {
public:
T* ptr(); // Raw pointer to the start of current row
Row<T> row(); // Obtain contiguous Row<T> view for current line
Row<T> operator*(); // Dereference shorthand to obtain Row<T>
Row<T> operator[](int dy); // Relative vertical peek (e.g. cur[-1] for previous line)
RowCursor& operator++(); // Advance down by 1 stride line
RowCursor& operator--(); // Step up by 1 stride line
RowCursor& operator+=(int dy); // Step by arbitrary vertical offset
};

// ============================================================================
// 3. Plane<T> - 2D Strided Memory View (Register-friendly 24-byte layout)
// ============================================================================
template <typename T>
class Plane {
public:
int32_t width(); // Plane width in elements
int32_t height(); // Plane height in elements
int32_t stride(); // Horizontal pitch in elements (pitch / sizeof(T))
ptrdiff_t stride_bytes(); // Raw byte stride
T* data(); // Base plane buffer pointer

T& operator()(int y, int x); // 2D coordinate random access
T* row_ptr(int y); // Direct pointer to start of row y
Row<T> row(int y); // Contiguous 1D view of row y
RowCursor<T> cursor(int y = 0); // Create row cursor starting at row y
RowCursor<T> begin(); // Cursor at row 0 (enables range-for over rows)
RowCursor<T> end(); // Cursor at row height

Plane subplane(int x, int y, int w, int h); // Zero-copy ROI cropping / sub-window
};

// ============================================================================
// 4. Opt-in Restrict (No-Alias) Converters
// ============================================================================
// Explicit wrappers for independent buffers (bypasses compiler alias analysis)
template <typename T> using RestrictPlane = /* Plane with __restrict data pointer */;
template <typename T> using RestrictRow = /* Row with __restrict data pointer */;

// Converters (analogous to std::as_const)
template <typename T> RestrictPlane<T> as_restrict(Plane<T> plane);
template <typename T> Plane<T> as_unrestricted(RestrictPlane<T> plane);

} // namespace your_namespace_here


Usage

Obtaining Plane Views from Video Frames


Downstream filters can obtain view objects either via a free helper function or via a frame method:

// Option A: Via free factory helper
auto src_plane = ns::make_plane<const uint8_t>(src, PLANAR_Y);
auto dst_plane = ns::make_plane<uint8_t>(dst, PLANAR_Y);

// Option B: Directly from PVideoFrame (if integrated into core interface)
auto src_plane = src->GetPlane<const uint8_t>(PLANAR_Y);
auto dst_plane = dst->GetPlane<uint8_t>(PLANAR_Y);


Pure C++ non-restrict pointers


template <typename T>
void InvertFilter(ns::Plane<const T> src, ns::Plane<T> dst) {
for (int y = 0; y < src.height(); ++y) {
auto in_row = src.row(y); // Contiguous 1D view
auto out_row = dst.row(y); // Contiguous 1D view

for (size_t x = 0; x < in_row.size(); ++x) {
out_row[x] = 255 - in_row[x];
}
}
}


SIMD non-restrict pointers


void ProcessAVX2(ns::Plane<const uint8_t> src, ns::Plane<uint8_t> dst) {
const __m256i v_max = _mm256_set1_epi8(255);
const int width = src.width();

for (int y = 0; y < src.height(); ++y) {
const uint8_t* s = src.row_ptr(y);
uint8_t* d = dst.row_ptr(y);

int x = 0;
for (; x + 31 < width; x += 32) {
__m256i v = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(s + x));
_mm256_storeu_si256(reinterpret_cast<__m256i*>(d + x), _mm256_sub_epi8(v_max, v));
}
for (; x < width; ++x) {
d[x] = 255 - s[x];
}
}
}


Highway non-restrict pointers


#include <hwy/highway.h>
namespace hn = hwy::HWY_NAMESPACE;

template <class D>
void ProcessHighway(ns::Plane<const uint8_t> src, ns::Plane<uint8_t> dst, D d) {
const auto v_max = hn::Set(d, 255);
const size_t N = hn::Lanes(d);

for (int y = 0; y < src.height(); ++y) {
auto in_row = src.row(y);
auto out_row = dst.row(y);
size_t x = 0;

for (; x + N <= in_row.size(); x += N) {
const auto v = hn::LoadU(d, in_row.data() + x);
hn::StoreU(hn::Sub(v_max, v), d, out_row.data() + x);
}
for (; x < in_row.size(); ++x) {
out_row[x] = 255 - in_row[x];
}
}
}


Restrict pointers


template <typename T>
void DiffKernel(ns::RestrictPlane<const T> src1,
ns::RestrictPlane<const T> src2,
ns::RestrictPlane<T> dst)
{
for (int y = 0; y < dst.height(); ++y) {
auto in1 = src1.row(y); // Type is RestrictRow<const T>
auto in2 = src2.row(y); // Type is RestrictRow<const T>
auto out = dst.row(y); // Type is RestrictRow<T>

for (size_t x = 0; x < out.size(); ++x) {
out[x] = std::abs(static_cast<int>(in1[x]) - static_cast<int>(in2[x]));
}
}
}

// Filter entry point: direct retrieval of restrict objects
PVideoFrame MyDiffFilter::GetFrame(int n, IScriptEnvironment* env) {
PVideoFrame src1 = child1->GetFrame(n, env);
PVideoFrame src2 = child2->GetFrame(n, env);
PVideoFrame dst = env->NewVideoFrame(vi);

// Option A: Via make_restrict_plane helper
auto plane1 = ns::make_restrict_plane<const uint8_t>(src1, PLANAR_Y);
auto plane2 = ns::make_restrict_plane<const uint8_t>(src2, PLANAR_Y);
auto planeD = ns::make_restrict_plane<uint8_t>(dst, PLANAR_Y);

// Option B: Directly via frame method
// auto plane1 = src1->GetRestrictPlane<const uint8_t>(PLANAR_Y);
// auto plane2 = src2->GetRestrictPlane<const uint8_t>(PLANAR_Y);
// auto planeD = dst->GetRestrictPlane<uint8_t>(PLANAR_Y);

DiffKernel(plane1, plane2, planeD);
return dst;
}


Performance verification benchmark

To verify whether these view abstractions introduce any compiler optimization penalties, a benchmark was conducted using a composite video processing kernel (3-tap blur + difference enhancement + clamping) on a 1080p sequence (1920x1080, stride 1984, Release /O2 /arch:AVX2, single-threaded):


[Benchmark Results: 1080p, Single-threaded, Release /O2 /arch:AVX2]

Clang-CL 19:
- C++ Scalar (Raw Pointer + Restrict) : 2,005 FPS ( 3.87 GB/s) -> 100.0% (base)
- C++ Scalar (Plane + RestrictRow) : 2,290 FPS ( 4.42 GB/s) -> 114.2% (+14.2%)
- AVX2 Hand-written (Raw Pointer) : 4,636 FPS ( 8.95 GB/s) -> 100.0% (base)
- AVX2 Hand-written (Plane/RowPtr) : 4,637 FPS ( 8.95 GB/s) -> 100.0% ( 0.00% delta)

MSVC (VS 2026):
- C++ Scalar (Raw Pointer + Restrict) : 1,449 FPS ( 2.80 GB/s) -> 100.0% (base)
- C++ Scalar (Plane/RowPtr + Restrict) : 1,460 FPS ( 2.82 GB/s) -> 100.7% (+ 0.7%)
- AVX2 Hand-written (Raw Pointer) : 4,482 FPS ( 8.65 GB/s) -> 100.0% (base)
- AVX2 Hand-written (Plane/RowPtr) : 4,458 FPS ( 8.61 GB/s) -> 99.5% (- 0.5% noise)



I would be very interested to hear your thoughts on this modernization concept.

While this is not meant for the immediate 3.7.6 milestone, it seems like a promising direction to simplify filter writing in the near future while preserving 100% of raw pointer performance. Looking forward to any advice, critique, or design feedback.

Disclaimer: This post was crafted by me and proofread by AI.

GeoffreyA
30th August 2026, 06:16
Don't know AviSynth, but the C++ interfaces look elegant, simple, and STL-like.

DTL
30th August 2026, 12:37
What is really useful for filtergraph performance is partial plane processing in a chain of compatible filters. In the best case - in-place processing where applicable. The memory-friendly parts with a typical h-scanned frame are rows stripes. So if new C++ language tools can somehow help with this type of frame additional subdivision this may be helpful. The new structure of movie objects is:
1. Clip of frames
2. Frame of planes
3. Plane of h-stripes.

Where total memory size of stripe is controlled by new object property number of rows in a stripe and can be optimized for cache sizes independant from plane size.
Current AVS filters interconnection API support only frame-granularity processing (filter data request function GetFrame() only) but in the future designs expect plane-based and partial-plane granularity.

In the current design idea it is close to a subplane. But it is expected to be good enough or best for memory subsystem performance to have such parts always width equal to plane stride (full row width). So the subplane or new object like h-stripe (or h-tile) properties may be only y-start (y) and height (h).

MeteorRain
31st August 2026, 02:59
The idea is more about improving readability and reducing potential mistakes. Incorrectly using pointers under different data types was widely seen in filters.

For example, even in current AviSynth+ code base there is this code:

if (packedRGB3264)
for (size_t x = mod16_width / 4; x < rowsize / 4; x += 4) {
result += std::abs(cur_ptr[x*4+0] - other_ptr[x*4+0]) +
std::abs(cur_ptr[x*4+1] - other_ptr[x*4+1]) +
std::abs(cur_ptr[x*4+2] - other_ptr[x*4+2]);
// no alpha
}
else

It should do x++ instead of x+=4.

Let alone in other filters when you need to work with 1/2/4 byte data and have to deal with all kinds of pointers, rows, offsets, etc.

Emulgator
31st August 2026, 14:03
Sounds good to me.