View Full Version : Internaly multi-threaded resampling functions
Pages :
1
2
3
4
5
6
[
7]
8
9
jpsdr
4th August 2025, 20:30
Thanks.
It's specific to vertical, a quick check, there is only AVX instruction on the AVX ASM...
I didn't put AVX ASM on 3.7.0, so it was still using the original code. Even more strange.
Just for information : If you're using threads=1, prefetch has no effect, as in that case the code path is specific to not MT, and there is even no threadpool created.
tormento
4th August 2025, 20:32
Even more strange.
What is stranger is that the error pattern inverted from 3.7.0 to 3.8.0.
tormento
5th August 2025, 10:46
If you're using threads=1, prefetch has no effect, as in that case the code path is specific to not MT, and there is even no threadpool created.
My CPU is so limited that I almost see no differences, when using BM3D + x265 encoding :o
I need AVSMeter and remove BM3D to see the real impact:
SetMemoryMax()
SetFilterMTMode("DEFAULT_MT_MODE", 2)
LoadPlugin("D:\Eseguibili\Media\DGDecNV\DGDecodeNV.dll")
Import("D:\Eseguibili\Media\StaxRip\Apps\Plugins\AVS\DehaloAlpha\Dehalo_alpha.avsi")
Import("D:\Eseguibili\Media\StaxRip\Apps\Plugins\AVS\Dither\mt_xxpand_multi.avsi")
Import("D:\Eseguibili\Media\StaxRip\Apps\Plugins\AVS\FineDehalo\FineDehalo.avsi")
DGSource("M:\In\My hero academia S3 ~720p bil Dynit\NCED-3.dgi")
z_ConvertFormat(resample_filter="Bicubic", pixel_type="yuv420p16")
DeBilinearResizeMT(1280, 720, threads=1, prefetch=2, accuracy=2)
fmtc_bitdepth (bits=10,dmode=8)
Prefetch(2,6)
FPS (min | max | average): 67.15 | 159.5 | 130.9
Process memory usage (max): 690 MiB
Thread count: 19
CPU usage (average): 26.0%
DeBilinearResizeMT(1280, 720, prefetch=2, accuracy=2)
FPS (min | max | average): 67.22 | 252.5 | 206.8
Process memory usage (max): 689 MiB
Thread count: 35
CPU usage (average): 68.3%
DeBilinearResizeMT(1280, 720, threads=2, prefetch=2, accuracy=2)
FPS (min | max | average): 81.23 | 215.2 | 188.7
Process memory usage (max): 689 MiB
Thread count: 23
CPU usage (average): 41.2%
Could you explain me the real impact of threads and prefetch parameters? In the wiki it's a bit obscure.
DTL
5th August 2025, 16:09
Ah...
My issue is that i have an SSE4.1, an AVX2 and AVX512 CPU, but not an AVX only.
You can use intel SDE to emulate any SIMD CPU (up to some AVX512) and it will show unsupported instruction found.
So you can run at SSE CPU and enable AVX emulation only and it will crash if unsupported instruction happen. It also can be integrated in VS2017 (up to VS2017 as I remember).
jpsdr
5th August 2025, 18:17
If threads=1 no threadpool is created, and a code path purely without threads is used, so any parameter related to MT has no effect, otherwise, the MT process is instancied.
By default, the plugin creates one threadpool the filter will request in GetFrame function to run. Prefetch calls several times GetFrame at the same time, so, if there is only one threadpool, one GetFrame will be granted the threadpool, and the others will be waiting for the threadpool to be free. The filter will create a number of threapools indentical to the prefetch parameter, in that case, each GetFrame will always have a threadpool available when requested (theoricaly...).
The threads parameter is the number of threads created in each threadpool. Each GetFrame will process the frame splitted in threads parts.
So the total number of thread created is : prefetch x threads.
This is why if you're using prefetch, i think you should keep prefecth x threads = number of cores.
So if using prefetch, don't let the threads parameter open (or at 0), otherwise it will be by default to the number of cores.
jpsdr
8th August 2025, 18:05
@DTL
I have to try/test this (i've made for H resampler):
void resize_prepare_coeffs_align_offset(ResamplingProgram* p, IScriptEnvironment* env, const int filter_size_alignment)
{
p->filter_size_alignment = filter_size_alignment;
p->overread_possible = false;
int ByteSize;
if (p->bits_per_pixel == 32) ByteSize=4;
else
{
if (p->bits_per_pixel == 8) ByteSize=1;
else ByteSize=2;
}
const int end_pos_safe = AlignNumber(p->target_size, ALIGN_FRAME / ByteSize);
// note: filter_size_real was the max(kernel_sizes[])
const int filter_size_aligned = AlignNumber(p->filter_size_real+filter_size_alignment-1,p->filter_size_alignment);
const int target_size_aligned = AlignNumber(p->target_size, ALIGN_RESIZER_TARGET_SIZE);
// Common variables for both float and integer paths
void* new_coeff = NULL;
void* src_coeff = NULL;
size_t element_size = 0;
// allocate for a larger target_size area and nullify the coeffs.
// Even between target_size and target_size_aligned.
if (p->bits_per_pixel == 32)
{
element_size = sizeof(float);
src_coeff = p->pixel_coefficient_float;
new_coeff = (void *)_aligned_malloc(element_size*target_size_aligned*filter_size_aligned, 64);
if (new_coeff==NULL)
{
myalignedfree(new_coeff);
env->ThrowError("Could not reserve memory in a resampler.");
}
std::fill_n((float*)new_coeff, target_size_aligned * filter_size_aligned, 0.0f);
}
else
{
element_size = sizeof(short);
src_coeff = p->pixel_coefficient;
new_coeff = (void *)_aligned_malloc(element_size*target_size_aligned*filter_size_aligned, 64);
if (new_coeff==NULL)
{
myalignedfree(new_coeff);
env->ThrowError("Could not reserve memory in a resampler.");
}
memset(new_coeff, 0, element_size * target_size_aligned * filter_size_aligned);
}
const int last_line = p->source_size - 1;
// Process coefficients - common code for both types
for (int i = 0; i < p->target_size; i++)
{
const int kernel_size = p->kernel_sizes[i];
const int offset = p->pixel_offset[i];
const int last_coeff_index = offset + p->filter_size_real - 1;
const int shift_needed = last_coeff_index > last_line ? p->filter_size_real - kernel_size : 0;
const int offsetD = (offset-shift_needed) % filter_size_alignment;
const int end_pos_aligned = offset - (shift_needed+offsetD) + filter_size_aligned - 1;
const int offset2 = end_pos_aligned >= end_pos_safe ? filter_size_alignment : 0;
// Copy coefficients with appropriate shift
if (p->bits_per_pixel == 32)
{
float* dst = (float*)new_coeff + i * filter_size_aligned;
float* src = (float*)src_coeff + i * p->filter_size;
for (int j = 0; j < kernel_size; j++)
dst[j + shift_needed+offsetD+offset2] = src[j];
}
else
{
short* dst = (short*)new_coeff + i * filter_size_aligned;
short* src = (short*)src_coeff + i * p->filter_size;
for (int j = 0; j < kernel_size; j++)
dst[j + shift_needed+offsetD+offset2] = src[j];
}
// Update offsets and kernel sizes
p->pixel_offset[i] -= (shift_needed+offsetD+offset2);
p->kernel_sizes[i] += (shift_needed+offsetD+offset2);
// left side, already right padded with zero coeffs, we can
// change to actual width to the common one
if(p->kernel_sizes[i] < filter_size_aligned)
p->kernel_sizes[i] = filter_size_aligned;
}
// Fill the extra offset after target_size with fake values.
// Our aim is to have a safe, up to 8 pixels/cycle simd loop for V resizers.
// Their coeffs will be 0, so they don't count if such coeffs
// are multiplied with invalid pixels.
if (p->target_size < target_size_aligned)
{
p->kernel_sizes.resize(target_size_aligned);
p->pixel_offset.resize(target_size_aligned);
for (int i = p->target_size; i < target_size_aligned; ++i)
{
p->kernel_sizes[i] =filter_size_aligned;
p->pixel_offset[i] = 0; // 0th pixel offset makes no harm
}
}
// Free old coefficients and assign new ones
if (p->bits_per_pixel == 32)
{
myalignedfree(p->pixel_coefficient_float);
p->pixel_coefficient_float = (float*)new_coeff;
}
else
{
myalignedfree(p->pixel_coefficient);
p->pixel_coefficient = (short*)new_coeff;
}
p->filter_size = filter_size_aligned;
p->filter_size_real = filter_size_aligned; // Worst case can produce shit needing all the size.
// by now coeffs[old_filter_size][target_size] was copied and padded into coeffs[new_filter_size][target_size] with offsetx aligned to filter_size_alignment
}
DTL
8th August 2025, 21:47
" // Update offsets and kernel sizes
p->pixel_offset[i] -= (shift_needed+offsetD+offset2);"
I not sure if it valid to change offsets generated in the resampling program.
Offset for each (i) - each output sample computing convolution of source samples with FIR filter kernel is computed as:
https://github.com/DTL2020/AviSynthPlus/blob/034a47e2c91ad9d84ad24492d37a18f99b58d996/avs_core/filters/resample_functions.cpp#L390
int start_pos = (int)(pos + filter_support) - fir_filter_size + 1;
program->pixel_offset[i] = clamp(start_pos, 0, last_line);
filter_support and fir_filter_size are constants inside resampling program generation loop and for each next output sample we have advance for double-precision variable pos as
pos += src_step;
where
double src_step = crop_size / double(target_size); // Distance between source pixels for adjacent dest pixels - it is real resampling/resizing ratio in float format
So program->pixel_offset[i] advances by some values proportional to resize ratio. And it points to the start sample in the source samples buffer to read for convolution computing for each output sample. I not understand how you can align this pointer to some SIMD-friendly value (or change it in any other way) and still have the same convolution result for output.
The second input for convolution - the kernel samples sets for each output sample convolution can be aligned (and as I understand they are already aligned to attempt to help performance increase with aligned SIMD loads). These samples sets are simply structure of arrays (1D vectors) and each array/vector can be placed anywhere in the address space.
But the first input for convolution - the source samples sequence is read from the input buffer and can not be divided into sets of source samples aligned to some address to be SIMD-loading friendly. They are pointed by (random) pixel_offset[] members-pointers to the single buffer in memory.
Addition: You can make pixel_offset[] aligned by padding kernel samples with zeroes to skip non-aligned loaded source samples from convolution computing. But it may create significant performance penalty for typical small size upsampling kernels if you still keep load full set of source samples for each output. Other way possible is load aligned sources and skip source samples by shift or other shuffling in register file.
In the AVX512 (and AVX2) with programmable fetch-permutation it is possible to load aligned part of row samples once for several output samples (for upsampling at least and small downsampling ratios) and convert global row pixel_offset[] source fetching program into local part of row (currently loaded into register file) for several output samples. And this saves from several aligned/unaligned loads for each output samples. But this limited to only small downsampling and possibly all upsampling ratios. Example is https://github.com/DTL2020/AviSynthPlus/blob/034a47e2c91ad9d84ad24492d37a18f99b58d996/avs_core/filters/intel/resample_avx512.cpp#L381
jpsdr
9th August 2025, 11:26
I not sure if it valid to change offsets generated in the resampling program.
Yes, i'm just padding with zero.
You said:
p->pixel_offset[i] -= (shift_needed+offsetD+offset2);
but forgot:
dst[j + shift_needed+offsetD+offset2] = src[j];
You can't have one without the other.
Example, aligned 8:
You have, original : offset=3, FIR size = 3 -> size_filter=8
[0][1][2][3][4][5][6][7] ->pixels
[X][X][X][0][0][0][0][0] -> Coeffs
Changed to : offset=0
[0][1][2][3][4][5][6][7] ->pixels
[0][0][0][X][X][X][0][0] -> Coeffs
Indeed, it increases the FIR size of "filter_alignment", meaning it will produce one more access in the filter loop compute.
For small filter size, it changes nothing, as you always load and compute at least one packet. For exemple, on AVX512, you'll load and process at least 32 pixels, so if FIR size=3 or 18 it doesn't matter.
I allready made a quick test on my new horizontal ASM, it seems to work (meaning no garbage and no crash, that just what a "quick test" is) for 8,10,16 bits, bur for now i have a crash with my float ASM, i didn't figure out yet why.
After, when everything will be "finished", i'll made benchmark, before pushing anything this time, and i'll see.
The issue will be with too small image size with my method, because it requires that the image width (aligned with the frame alignment) is at least twice the padding, but i'll see this later.
DTL
9th August 2025, 13:39
"For small filter size, it changes nothing, as you always load and compute at least one packet. For exemple, on AVX512, you'll load and process at least 32 pixels, so if FIR size=3 or 18 it doesn't matter."
It may be for the compute engine uses H-sums of the multiplication. If you use engine with H to V transposition you can compute more output samples per loop spin/pass. And use lower filter size minimum granularity. Small size kernels are widely used for upsampling - like most of support=2 filters. They require only 4 source samples for any upsample ratio scaling. If your engine only process with minimal size of convolution for each output sample of 32 it may lost performance for many upsampling use cases.
After some time of design resampling engines I think it is good to separate at least 2 scale ratios for different engines -
1. Scale ratio about 1.0 and higher
2. Downscaling with scale ratios about 0.5..0.9 and lower.
1 and 2 uses very different number of source samples to compute output result and resampling engines can uses different ways of load H-sources and feed to FMA convolution engine for better performance in each case.
For upsample number of source samples is about equal to support*2 and typically very small and for downsample it can be much more larger (about support*2 x 1/scale_ratio).
jpsdr
9th August 2025, 14:01
Figure out my crash... In my fast rewrite of the C calling ASM, i was using program->pixel_coefficient instead of program->pixel_coefficient_float in the float data case...
Otherwise, used your compare script (finaly very usefull, thanks for it), and for now, with my aligned offset, output are identicals with core resample.
Only done x86 ASM, i'll benchmark only the intersting : x64 ASM, more effecient. I hope this time i'll have a better result... :D
Otherwise, for now, i don't intend to change method (like swapping H/V for small size FIR).
Edit:
I've also made (and tested...) an ASM unligned using normal pixel position. This way, i'll be able to benchmark both versions.
jpsdr
15th August 2025, 18:39
You can use intel SDE to emulate any SIMD CPU (up to some AVX512) and it will show unsupported instruction found.
.
I've downloaded it, i just now have to figure out how it works...
If i'm lucky, it will be easy with just a command line with few parameters to emulate a specific CPU (AVX, AVX512,...).
Edit:
It seems to be a dead end if i understood the few i've read, as i'm with VS2019 under Windows 7.
wonkey_monkey
15th August 2025, 21:22
The following:
BicubicResizeMT(1288,720, src_Left=0.00001)
doesn't produce the same result as BicubicResize. It spreads the leftmost column across the whole output (even if src_width = 1288 is specified).
That's with W7_AVX2 anyway, haven't tried the rest.
jpsdr
16th August 2025, 11:50
What's the source size ?
jpsdr
16th August 2025, 13:42
I've been able to get the last Intel SDE working with Windows 7, and lucky, it can emulate AVX-512.
But...
Despite my efforts and my research, i don't know how to debug my DLL on a program started with SDE.
What i found said to attach the process to sde.exe, not Virtualdub, i tried both, and also run directly from a command line or a VS native tools command line, no change.
Breakpoints have no effect, and when crashing, program just quit and not the usual debug code show.
Of course, as for the DLL to appears in the "Debug -> Windows -> Modules" you have VDub to start avisynth, wich loads the DLL, which occurs only when you open the script... And crash !
So, if anyone know how to debug with VDub started with SDE, i'm interested...
tormento
16th August 2025, 14:32
So, if anyone know how to debug with VDub started with SDE, i'm interested...
Caveat: I don’t know anything about programming.
As a curiosity, why don’t you use AVSmeter or AVSPmod to launch scripts?
wonkey_monkey
16th August 2025, 14:41
What's the source size ?
1288x720 (result of ColorBarsHD)
The result seems to be the same with any input and any target size, as long as src_left is specified.
I think src_width is being overwritten with the value of src_left somewhere, or the two values are getting swapped. Same with src_top and src_height.
jpsdr
16th August 2025, 14:46
Virtualdub allow to see the result, also to reload directly script (when testing).
Also, i start VDub, i attach the process in Visual Studio, and then, after, i load/start the script.
It's impossible to attach to a process with AVSMeter, because it starts the script at startup.
I don't konw AVSPmod.
jpsdr
16th August 2025, 14:52
The result seems to be the same with any input and any target size, as long as src_left is specified.
The following doesn't show differences :
Function Diff(clip src1, clip src2)
{
return Subtract(src1,src2).Levels(120, 1, 255-120, 0, 255, coring=false)
}
BlankClip(100, 200, 100, color=$7F7F7F, pixel_type="YV24")
AddBorders(2, 2, 2, 2)
pad=50
Subtitle("Resample test", align=5).convertbits(8)
std=LanczosResize(width*2, ((height/4)*8)+4+2+1, taps=16, src_left=0.0001)
mt=LanczosResizeMT(width*2, ((height/4)*8)+4+2+1, taps=16, src_left=0.0001)
d1 = Diff(mt,std)
d2 = Diff(mt,std)
StackHorizontal(StackVertical(std, mt), Stackvertical(d1, d2))
Didn't test specificaly with Bicubic yet.
wonkey_monkey
16th August 2025, 16:48
It's specific to BicubicResizeMT.
Perhaps because of the b and c parameters shifting everything out of expected index. Create_BicubicResize seems okay though...
jpsdr
16th August 2025, 18:14
It's specific to BicubicResizeMT.
In that case, there is a big change i've made a mistake in the parameters list. I'll check Create_BicubicResize next week.
wonkey_monkey
16th August 2025, 19:53
Perhaps line 3304 of resample.cpp:
args[Offset_Arg+5].AsInt(0),args[Offset_Arg+6].AsInt(1),false,0,0,args[Offset_Arg+7].AsInt(6),&args[3],&f,
should read
args[Offset_Arg+5].AsInt(0),args[Offset_Arg+6].AsInt(1),false,0,0,args[Offset_Arg+7].AsInt(6),&args[5],&f,
?
Edit: Yup, was &args[5] prior to 1b9ef36.
jpsdr
17th August 2025, 10:14
Good catch, thanks.
DTL
17th August 2025, 12:36
I've downloaded it, i just now have to figure out how it works...
It works with Win7 . But latest VS integration may be limited to VS2017 only. I do not know why intel stop support of new VS versions.
You can install it as standalone software pack (decompress files to some folder) and use from command line only
like
sde -options (path_to)virtualdub.exe
When it is integrated in VS you can select SDE debugger and debug applications as if you have emulated CPU installed.
" it will be easy with just a command line with few parameters to emulate a specific CPU (AVX, AVX512,...)."
Yes - it can do emulation in stand alone mode.
"how to debug with VDub started with SDE, i'm interested..."
You need VS2017 and install SDE with VS integration. And simply select SDE Debugger to use. For new VS I do now know how to debug with SDE. Only emulation and crash check is working.
jpsdr
18th August 2025, 08:54
Well, i was finaly indeed in just emulation and crash test mode, with the last Windows 7 compatible version (8.63). Luckily, this version already emulate AVX-512.
I finaly fix all my crash issues, and begun some tests.
On a little long filter, unaligned is a little faster than aligned. Didn't check on small filter, but difference will probably be bigger. So, aligned was a nice try, but failed.
I made a quick check of my ASM AVX-512 vs Intrinsic AVX-2, on horizontal only, and my ASM was a little slower... :(
But i checked the core vs external. The core being "core", it can be a little faster, so i have to check my actual version vs the last version of mine with intrinsic... But, again, odds are unfortunately not good. I'll see...
jpsdr
18th August 2025, 13:25
Hmm... I may have understood why if the filter size is too small, AVX-512 is slower than AVX2...
I need to finish my AVX2 asm...
tormento
18th August 2025, 13:34
Hmm... I may have understood why if the filter size is too small, AVX-512 is slower than AVX2...
I need to finish my AVX2 asm...
Please, when time is on your side, check AVX emulation too [emoji3526]
jpsdr
18th August 2025, 13:47
It's on the roadmap.
Now that i've finished rewriting the AVX512, i'm doing AVX2. And when finished AVX2, i'll do AVX. Useless for me to check AVX asm as there will be a total rewrite of the actual horizontal.
Why this order ? Because i'm using "uper code" (AVX2 for exemple) to write "lower" code (AVX for exemple).
Why AVX512 can be slower than AVX2, or AVX2 slower than AVX for horizontal ?
Quick answer :
There is 2 steps. The first step (a loop) make the multiplication pixel/coeff, the second step the sum of these results.
With small filter, the first step will loop only once, so, should you be AVX512, AVX2 or AVX, the computation time will be the same.
After this first step, you have a register with 32 values to sum in AVX512, only 16 with AVX2 and only 8 with AVX.
Summing 32 takes more time than 16 taking more time than 8.
So, in case of small loop, for exemple a filter of size 6, AVX will be faster than AVX2, also faster than AVX512.
There will be a threshold of filter size where AVX512 will be faster than AVX2, and the same with AVX2 vs AVX.
But this will be after i've finished all the code re-write.
jpsdr
18th August 2025, 19:12
I'll stop and remove/roll-back all asm, benchmark are not good, no more time waisted on this.
And so no AVX issue anymore...
FranceBB
24th August 2025, 19:34
I'll stop and remove/roll-back all asm, benchmark are not good, no more time waisted on this
Really sorry to hear that.
Still a lot of kudos for the commits, we saw how much time you spent on this and we appreciated it regardless, even if it didn't pay off. :(
jpsdr
28th August 2025, 11:16
New version, see first post.
tormento
28th August 2025, 16:36
And so no AVX issue anymore...
I tried 3.9.0 plugins and they work fine on my ancient CPU.
If you want to gain some speed, can I suggest you to give a try to Intel Compiler? Its community version is absolutely free and it has great optimizations working very well even on AMD processors.
jpsdr
28th August 2025, 18:17
My PC with VS is under Windows 7, i think Intel compiler will not work with this.
Also, I'm allready making LLVM builds, probably not far from Intel compiler.
Anyway, there is allready Intrinsics, which for now are faster than my ASM...
For now, i'll keep things the way they are.
tormento
28th August 2025, 19:08
My PC with VS is under Windows 7, i think Intel compiler will not work with this.
Also, I'm allready making LLVM builds, probably not far from Intel compiler.
Anyway, there is allready Intrinsics, which for now are faster than my ASM...
For now, i'll keep things the way they are.
On my computer Clang is slower than MSVC builds.
jpsdr
29th August 2025, 10:42
Questions for DTL, our resampler expert... :D
What do you think of JincResize vs the resamplers of the core AVS ?
And is JincResize more suitable for upscale, downscale, or both ?
It's just a question, there will not be a JincResizeMT as the code is allready multithreaded, and i understand nothing, the ++ level of the C++ is too much for me... :(
DTL
29th August 2025, 20:44
"What do you think of JincResize vs the resamplers of the core AVS ?"
I hope someone (pinterf ?) will someday implement 2D single pass resampling engine in AVS core and JincResize will be available as internal resizer. It is simply one of infinite number possible kernels for 2D single pass resampling engine. All 1D kernels possible too - simply treat 1D f(x) kernel function as rotation around 0,0 point in 2D space (use f(r) where r is raidius to current 2D point).
"And is JincResize more suitable for upscale, downscale, or both ?"
JincResize is equal to LanczosResize (from 2 pass 1D + 1D resizers) in 2D single pass resizers domain. Its kernel is jinc weighted by sinc (or jinc - the weighting function is not very critical). So it is expected as reference upsampler (with enough taps number). For downsample also equal to UserDefined2Resize possible to setup low-pass de-Gibbs downsampling kernel. Simply replace base function from sinc(x) to jinc(x) and the number of kernel members may be > 2 for best case (and thus more complex to setup and control). But simple form of 2D_UserDefined2Resize may be started with 2 members b and c.
For quality - 2D (single pass) resizers expected to be more natural for processing 2D images and expected to provide some better quality (at special cases visibly better). But in real digital imaging we do not have standard for reference image upsampler. For example is it must be dual-1D like SincResize or 2D single pass like JincResize ? So in real use cases it is recommended to test possible workflows of:
1. 2D downsize (compression) and 2D upsize (displaying, decompression). Expected in the future. Much computing required.
2. 1D+1D downsize (compression) and 1D+1D upsize (displaying, decompression). Typically widely used today. Low computing required and better performance.
The mix of 1 and 2 may give some average results. For general natural imaging there is not great difference between 1 and 2 workflows but MPEG compression may be more friendly with one of 2.
"there will not be a JincResizeMT as the code is allready multithreaded"
JincResize in Asd-g repository uses OpenMP for intra-frame MT. So it is easy to set threads task separation in your threads pool implementation. Simply cut output frame to H-stripes (or V-stripes) and set each stripe process to different threads. OpenMP do this by y-variable of the loop.
It is simple C-program. The only ++ part is AVS+ interface and some vector ++ class for resampling program generation. But it can be replaced with simple 1D or 2D C-array.
jpsdr
30th August 2025, 11:02
For now, i just made very small update in original code.
- Now improper size send an error message instead of crashing.
- AVX512 also in automatic detect.
- Add "range" parameter, same as mine in ResampleMT.
I don't know if it's relevent putting on my github...
I easely see how it can be using my threadpool, vector c++ is not realy the issue, my issue is with a lot of parts i don't understand, for now, what it's doing.
Especialy the coeff creations.
Also, i don't understand why it needs AVS+...
There is several syntax i've never seen, and don't know what they mean.
Anyway, for now, i'll stay like this. Little by little (but very little) i understand a little more what is done.
Edit:
Finaly, i'll try to do an MT version... :D
First step, full rewrite without threadpool...
Edit 2:
LOL !!!!!!
I just dig out the fact that i've allready begun to create a JincResizeMT a long time ago, i totaly forgot...
DTL
30th August 2025, 16:40
AVS is C++ .dll interface for plugin to work with AVS environment. Though as I remember C AVS interface is also possible (still working in some Asd-g plugins ?).
The resize process structure is equal to 1D (dual-1D):
1. Get kernel function f(x) for 1D or f(r) or f(x,y) for 2D
2. Create a resampling program from the resample ratio and kernel samples (from 1.). It is typically kernel samples for each output sample convolution and some service stuff to handle edge cases The most complex and performance-limited with 2D resize is that resampling program in 2D case typically pre-computed for the total output frame size. And it can take GBytes in size with not very big kernel (filter) size and not very big output size. And reading from RAM is RAM-performance limited. Second possible way is run-time creation of a resampling program for each output sample. But it also required kernel function (1.) to be computed fast enough.
For jinc resize we need bessel _j1(x) and it may be not fast to compute at runtime and may require some faster (and lower quality) approximation if possible.
For 1D resize the resampling program only computed for 1 row or 1 column and much smaller.
3. Send a resampling program to the resampling engine to make resize.
"There are several syntax i've never seen, and don't know what they mean."
It is some very new C++ text like C++17 or later. You can take an older version of JincResize with more simple C++.
Really most important question is in resize naming or some other way to mark the resize engine used. Because many kernels (may be all) can be processed in both dual-pass 1D+1D and in single-pass 2D resize engines. Some plugins to show it is single pass 2D resize add ewa_ prefix. But it does not look universal because many weighting ways are not elliptical but simply round or squared.
One possible suggestion is add 2D_ prefix. So BilinearResize executed with a 2D single pass engine will be 2D_BilinearResize(). Or simply to keep old filters, add one new param to resize filter - use dual-pass 1D + 1D resize engine or single-pass 2D resize engine.
So you can run JincResize(engine="1pass") as typical JincResize() and can run JincResize(engine="2pass") with 1D+1D resize engine (as today in AVS+ core and in your ResampleMT) with simply using jinc weighted by jinc as kernel function. It will produce a bit different result in comparison with SincResize() (or any sinc-based kernel). It decays a bit faster and has not completely equal distance between zeroes.
And with this way users can run any other old resize kernel with a 2D single pass resize engine like LanczosResize(engine="1pass"). For single-sample excitation and upsampling it will produce significantly different output:
1D+1D is checker-board like pattern
2D is nice round circles from rotation of the sinc kernel around the input sample in 2D plane.
The UserDefined2Resize() will also run somehow not bad but for better result with 1pass resize mode it is better to replace sinc() base function to jinc() in https://github.com/jpsdr/ResampleMT/blob/3c604eba81d9c550933c7f5fc9d34bcd6aad6e77/ResampleMT/resample_functions.cpp#L370 . It may be done automatically if the user requests this resize to be dispatched by 2D 1pass resize engine. So no more control params are required.
Difference example between dual-1D and single-2D resampling engines result with close looking ringing kernels (sinc and jinc):
LoadPlugin("JincResize.dll")
BlankClip(200,1,1, pixel_type="YV24", color_yuv=$D07f7f)
AddBorders(21,21,21,21, color_yuv=$307f7f)
dual1D=SincLin2Resize(width*10, height*10).Subtitle("sinc dual-1D")
single2D=JincResize(width*10, height*10, tap=16).Subtitle("jinc single-2D")
StackHorizontal(dual1D, single2D)
https://i.postimg.cc/2jZk23tT/2025-08-31-110140.png
Dual-1D processing causes 'secondary-ringing' from primary ringed 1D upsampled result when processing in second dimension. This ends in sort of checker-board pattern. Single-2D processing outputs kernel shape in 2D form non-distorted as expected.
jpsdr
31st August 2025, 12:41
I will not create another thread, this one is still good for also talking of JincResize, we are still in the resampler world.
I've created my repo Git. What i've pushed is still a lot WIP, but it builds, it produces an output and works with AVS 2.6, it's not crashing, but... The output is blury as hell, so i have to figure out why... as i don't have this blur with the build i've made from the true original...
Otherwise, with your output now i see the difference between the two methods... If i would have to choose, for me, single-2D over dual-1D, for this specific case.
DTL
31st August 2025, 15:32
The performance difference is great:
At i5-9600K CPU with Prefetch(6) and latest build of JincResize from Asd-g and AVS+ 3.7.4:
JincResize(tap=16) - 9.8 fps 99% CPU load
LanczosResize(taps=16) - 1452 fps 16% CPU load. About 150 times faster.
"is blury as hell, so i have to figure out why..."
It may be issue with kernel scaling in resampling program generation or in the kernel source function. Like too wide kernel causing too much low-pass filtering.
I think you can integrate single pass 2D resampling engine into ResampleMT plugin and use (share) single kernel sources for 2 different resampling engines for user to select. So all old/classic resize kernels can be processed by different engines if required.
The only new addition is jinc kernel function and new 2D resampling program generator and (single) 2D resampling engine (C and SIMD versions). It even more simple in comparison with dual-1D resize where we need to make and run 2 different H and V resample engines.
jpsdr
31st August 2025, 16:04
Before running (integrate a 2D engine), begin to walk properly (having a not multi-threaded, simple code C of plugin working).
I've made a build (with just a very few tweaks) of Asd-g version.
Then i've made a build of my starting version.
Calling with the exact same input with the same parameters, mine is a lot blurry, so, obviously, i made a mistake somewhere when porting for my own version.
Also i had only tested an x86 version under AVS 2.6, i've tested an x64 build, and it crashes. Odd... There is no ASM and so no differences should occur between x86 and x64, another thing to check...
DTL
31st August 2025, 16:13
x64 typically more stable (and only can run) with medium ouput frame size and medium taps number (filter size) because it can allocate > 2..3 GBs of (contigous virtual) RAM for coeffs table. Though if RAM allocation fail it expected to throw allocation error instead of some exception (like unknown C++ or crash with zero pointer).
Better start testing with small output frame size like 400x400 and taps about 8.
x86 version sometime can fail even with 512 MBs allocation (because it is single buffer (vector ?) allocation and must have contigous RAM virtual addresses and if process virtual address space is already fragmeted the allocation can fail with very low size below 1 GB even).
StvG
31st August 2025, 16:55
The performance difference is great:
At i5-9600K CPU with Prefetch(6) and latest build of JincResize from Asd-g and AVS+ 3.7.4:
JincResize(tap=16) - 9.8 fps 99% CPU load
LanczosResize(taps=16) - 1452 fps 16% CPU load. About 150 times faster.
JincResize(tap=16) (default threads=0) and prefretch(6)? Try JincResize(tap=16, threads=1) when you're using prefetch.
jpsdr
31st August 2025, 17:12
Test is made with small pitcure, of course... :D
Both blur and crash issues come from the same source, the llround computation index. Short version, don't have llround, so replaced with floor(x+0.5), but doing this, as the original code has some kind of "i don't understand" magic_round, it was now producing bad index in x86 (-> Blur), and negative index in x64 (crash).
Removed the magic_round, and now i have the same output in both x86 and x64.
It's a big first step.
DTL
31st August 2025, 17:58
JincResize(tap=16) (default threads=0) and prefretch(6)? Try JincResize(tap=16, threads=1) when you're using prefetch.
Yes - I forgot about internal intra-frame stripe-based OpenMP mulththreading. But it not helps any:
JincResize(tap=16, threads=1)
5.5 fps (single threaded performance)
And both AVS+ MT and internal OpenMP MT gives only about 9.5fps. It looks the RAM performance for reading large 2D coeffs table limits performance greatly.
Some strange note for building: Building need at least Visual Studio 2019 for AVX512.
At least VS2017 support intrinsincs of AVX512 (and the SDE integration work with AVX512 too as expected). The only difference may be with compiler control - in VS2017 if user want to force C compiler to use AVX512 instructions for C-part of program the manual command line switch /arch:AVX512 can be added. Not included in drop-down menu in GUI. But for manually written intrinsics it expected to work with default settings.
JincResize resampling engines uses manually written intrinsincs for AVX512 so expected to work with VS2017 too. If user expect some more optimization - the manual command line switch /arch:AVX512 may be added but it will cause full executable to have any AVX512 instruction in any place and not controlled by CPU selection and can be executed only at AVX512 capable chip.
https://devblogs.microsoft.com/cppblog/microsoft-visual-studio-2017-supports-intel-avx-512/
Microsoft Visual Studio 2017 supports Intel AVX-512, and with Visual Studio 2017 version 15.3 we’re enhancing that support to include more Intel AVX-512 instructions than ever before. We implemented over 1500 Intel AVX-512 intrinsic functions in Microsoft Visual C++ for Visual Studio 2017 version 15.3, and we have more to do. The available functions are mostly for 512-bit vectors or floating-point scalar values. We plan to add more functions for 256-bit and 128-bit vectors and floating-point scalars in an upcoming release, which will more than double the number of AVX-512 functions available. There are also many additional optimizations for the new AVX-512 features that we are planning to roll out over several releases.
About quant_x,y and blur params - I do not know what are they for and changing from default values causes more or less severe distortion of the jinc kernel. For simplified version of resize I think you can skip these params and use pure jinc kernel (with its implemented weighting).
At some old versions blur param was non-1.0 default (and scary extra precision value like 0.9812505644269356) https://github.com/Kiyamou/VapourSynth-JincResize/blob/master/README.md . But for latest version looks like this modification was disabled (blur=1.0).
Unfortunately the sources are significantly messy in kernel and resampling program generation. Looks like initial design was at times with no required bessel C-library function _j1(x) available and have its own implementation of bessel j(x) and this makes sources more complex. In latest C compilers (language math libraries ?) we finally have bessel j(x) implementation and can call it simply without manual computing and this makes kernel as simple as sinc(x) - see example in second branch
https://github.com/Asd-g/AviSynth-JincResize/blob/b7fbf5d680a2950dff65b907134e6719efd11916/src/JincResize.cpp#L916
The expected idea of importance of the jinc kernel function for resampling - it is (2D) impulse responce of ideal round (2D) imaging device (ideal round lens). https://en.wikipedia.org/wiki/Airy_disk
For most of natural non-coherent light sources it is squared jinc and as I remember for coherent light it is not squared. No any more magic numbers and transforms required.
Also this hints - we can check squared jinc as version of resize kernel too (it may make more blurry but better simulate imaging from natural non-coherent light).
So at least for main production operation downsampling usage of jinc and 2D resampling engine expected to be good simulation of creation of natural image from infinity resolution source by emulated ideal (diffraction-limited) lens. By low-pass filtering (convolution) with its 2D impulse responce that is desicribed by jinc function (or squared jinc as option).
jpsdr
1st September 2025, 18:35
Out of curiosity, i've tested on my Broadwell CPU the build i've made from the original JincResize (so with OpenMP), with threads=0 and threads=1.
720x480 -> 775x583 : 196.9 fps / 40.01 fps
3480x2160 -> 775x583 : 13.90 fps / 2.646 fps
720x480 -> 3480x2160 : 25.25 fps / 2.645 fps
StvG
1st September 2025, 18:44
From 2.1.0 (https://github.com/Asd-g/AviSynth-JincResize/blob/master/CHANGELOG.md#210) the parallel execution isn't done with OpenMP. What version of the code you use for your fork?
jpsdr
1st September 2025, 20:33
The 2.1.4 (https://github.com/Asd-g/AviSynth-JincResize.git).
It's done with a class called "execution" (from the few i understood), i don't know if it's OpenMP or not, DTL said it was OpenMP, so i assumed it was.
DTL
1st September 2025, 22:20
Initial internal MT was designed with OpenMP. It is simple
#pragma omp parallel for num_threads(threads_)
before y-loop. See example https://github.com/Asd-g/AviSynth-JincResize/blob/b7fbf5d680a2950dff65b907134e6719efd11916/src/KernelRow_avx2.cpp#L20
But in latest versions Asd-g redesign it for some other auto-MT may be implemented in new C++.
With your better MT performance results - may be you use small kernel (low taps). I use JincResize(width*10, height*10, tap=16) (from the script above for kernel image footprint, post https://forum.doom9.org/showthread.php?p=2022230#post2022230).
jpsdr
2nd September 2025, 13:18
@DTL
Your width & height is very small, so the memory allocated for coeff is small.
PM me your email, i'll send you my build if you want.
DTL
3rd September 2025, 20:14
Allocated memory for 2D resampling program is about output size multiplied to kernel size for each output sample (2D filter size). For tap=16 filter size expected about 32x32 floats = 1024 x 4 bytes - about 4 kbyte (if coefs in 32bit floats).
Input size is about 44x44 and 10x output is 440x440. It is 193 600 samples and 4096 bytes filter for each = 800 MBs resampling program coefs size. It is much larger in comparison with CPU cache size. Before resampling program reusage for equal planes in older versions the memory usage was even worse - the separate resampling program for each plane of 4:4:4 format and RAM usage was about 3x800 MBs.
Script:
LoadPlugin("JincResize.dll")
BlankClip(20000,1,1, pixel_type="YV24", color_yuv=$D07f7f)
AddBorders(21,21,21,21, color_yuv=$307f7f)
dual1D=LanczosResize(width*10, height*10, taps=16).Subtitle("sinc dual-1D")
single2D=JincResize(width*10, height*10, tap=16).Subtitle("jinc single-2D")
return single2D
AVSmeter report close:
Number of frames: 20000
Length (hh:mm:ss.ms): 00:13:53.333
Frame width: 430
Frame height: 430
Framerate: 24.000 (24/1)
Colorspace: YV24
Frames processed: 105 (0 - 104)
FPS (min | max | average): 7.236 | 10.12 | 9.655
Process memory usage (max): 1062 MiB
Thread count: 16
CPU usage (average): 90.8%
At i5-9600K CPU of 6 cores.
With threads=1
FPS (cur | min | max | avg): 5.451 | 4.515 | 5.764 | 5.469
Process memory usage: 1062 MiB
Thread count: 10
CPU usage (current | average): 16.2% | 15.1%
To process 3 planes of YV24 in current implementation CPU must read coefs from RAM 3 times so 800x3=2.4 GBytes read per frame for resampling program only. And with 10fps it cause RAM read about 24 GBytes per second. May be close to the performance of this old host.
What I remember of data locality optimization:
Make process all equal sized planes with same resampling program read once. It is for RGB and RGBA formats read coefs once for 3 and 4 planes instead of re-read coefs for each plane 3 or 4 times. For 4:2:2 and 4:2:0 formats benefit is lower - only UV planes of smaller size can be combined.
So we can need to add several more resampling functions in addition to single plane resize_plane_(SIMDfamily)(). They are resize_2planes(), resize_3planes() and resize_4planes().
They are also simple but we send several planes in and out pointers and strides and single resampling program input pointer. Inside it is interleaved processing of several planes with single resampling program coefs load for each output sample for each plane.
This looks like working finally as expected - see pull request as example https://github.com/Asd-g/AviSynth-JincResize/pull/21
Test build https://github.com/DTL2020/AviSynth-JincResize/releases/tag/post_2.1.4_1
for script
LoadPlugin("JincResize.dll")
ColorBarsHD(100,100)
ConvertToYUV444()
JincResize(width*10, height*10, tap=16)
About the same performance boost for downsample:
LoadPlugin("JincResize.dll")
ColorBarsHD(1000,1000)
ConvertToYUV444()
JincResize(width/10, height/10, tap=16)
As I see the performance of 2D resampling with pre-computed resampling program is hardly limited by host RAM performance. And no computing optimizations are useful. The next optimization idea still attempt to compute resampling program at runtime at SIMD unit. But it will be greatly kernel-function dependant and will require to create separate 2D resampling function for each kernel function (though some like bicubic and bilinear and possibly gauss are simple enough).
vBulletin® v3.8.11, Copyright ©2000-2026, vBulletin Solutions Inc.