Tutorial step 7: adding MMX optimizations

The first step to optimizing our filter would be to use assembly code, but we'll spend some time on MMX optimizations first, because MMX can really speed this code up. The trick to using MMX is knowing when we can and can't use it; detection code isn't good enough because the user may have intentionally forced or disabled MMX support in VirtualDub's preferences dialog.

Checking to see if CPU optimizations are available

Two functions in the FilterFunctions struct are useful here.

  • ff^.isFPUEnabled returns true when FPU optimizations should be used.
  • ff^.isMMXEnabled returns true when MMX optimizations should be used.

FPU optimizations are rare in pixel processing; the only VirtualDub filter that uses them is the bilinear resize filter, because it requires 6 64-bit multiplies per pixel, which are more quickly done with the FPU than the integer units. MMX optimizations are the biggie; use them if at all possible.

The FPU and MMX enable flags are guaranteed not to change between filters or during processing. It is highly recommended that you globally cache the flags during startProc processing, and read the global flag in runProc.

Adding MMX optimizations to our filter

First, modify startProc to cache the MMX flag.

var
 (...)
 g_MMXenabled: boolean;

Function tutorialStartProc(fa: PFilterActivation; const ff: PFilterFunctions): Integer; cdecl;
var
 mfd: PFilterData;
 i: Integer;
begin {tutorialStartProc}
mfd := PFilterData(fa^.filter_data);
g_MMXenabled := ff^.isMMXEnabled;
(...)
end; {tutorialStartProc}

Now, write the MMX acceleration routine. For simplicity, it will only handle one scanline. I'm not usually very clear when writing MMX code, so don't feel bad if it takes a couple of passes to understand. ^^;;

procedure doscan_MMX(dst: PPixel32; src: PPixel32; w: integer; frac: longint; bias: longint; fDouble: integer); cdecl;
const Rmask: int64 = $0000FFFF00000000;
label xloop1, xloop2, xit;
asm
  push        ebp
  push        edi
  push        esi
  push        ebx

  mov         eax,[esp+4+16]
  mov         edx,[esp+8+16]

  mov         ecx,[esp+12+16]
  neg         ecx
  shl         ecx,2
  sub         eax,ecx
  sub         edx,ecx

  movq        mm6,Rmask
  movd        mm4,[esp+20+16]
  psllq       mm4,16
  movd        mm5,[esp+16+16]
  punpcklwd   mm5,mm5
  pxor        mm7,mm7

  mov         ebx,dword ptr [esp+24+16]
  or          ebx,ebx
  jz          xloop1

  sub         eax,ecx

xloop2:
  movd        mm0,[edx+ecx]       //mm0 = pixel
  movq        mm1,mm6             //mm1 = R mask
  punpcklbw   mm0,mm7             //unpack pixel to words
  pand        mm1,mm0             //mm1 = red component
  pmulhw      mm0,mm5             //scale green and blue
  paddw       mm1,mm4             //add green bias
  paddw       mm0,mm1             //add scaled green/blue
  packuswb    mm0,mm0             //repack pixel to bytes
  movq        [eax+ecx*2],mm0     //write 2 pixels
  add         ecx,4
  jne         xloop2
  jmp         xit

xloop1:
  movd        mm0,[edx+ecx]       //mm0 = pixel
  movq        mm1,mm6             //mm1 = R mask
  punpcklbw   mm0,mm7             //unpack pixel to words
  pand        mm1,mm0             //mm1 = red component
  paddw       mm0,mm0             //double g/b channels beforehand
  pmulhw      mm0,mm5             //scale green and blue
  paddw       mm1,mm4             //add green bias
  paddw       mm0,mm1             //add scaled green/blue
  packuswb    mm0,mm0             //repack pixel to bytes
  movd        [eax+ecx],mm0       //write pixel
  add         ecx,4
  jne         xloop1

xit:
  pop         ebx
  pop         esi
  pop         edi
  pop         ebp
end;

Finally, add the MMX optimizations to runProc.

Function tutorialRunProc(const fa: PFilterActivation; const ff: PFilterFunctions): Integer; cdecl;
var
  w, h: TPixDim;
  src, dst: PPixel32;
  old_pixel, new_pixel: TPixel32;
  mfd: PFilterData;

begin {tutorialRunProc}
src := PPixel32(fa^.src^.data);
dst := PPixel32(fa^.dst^.data);
mfd := PFilterData(fa^.filter_data);
h := fa^.src^.h;
repeat
  w := fa^.src^.w;
  if g_MMXenabled = true then begin
    if mfd^.fThird = true then
      doscan_MMX(dst, src, w, $2AAA, $55, integer(mfd^.fExpand))
    else
      doscan_MMX(dst, src, w, $4000, $80, integer(mfd^.fExpand));
    //If X is a pointer type, Inc increments X by N times the size of the type pointed to.
    Inc(src, fa^.src^.pitch div SizeOf(PPixel32));
    Inc(dst, fa^.dst^.pitch div SizeOf(PPixel32));
    // double the routine for speed; an if would kill us in the
    // inner loop, but in the outer loop it's ok
    end
  else begin
    if (mfd^.fExpand = True) then
      repeat
        old_pixel := src^;
        new_pixel := (old_pixel AND $FF0000)
                     + mfd^.grn_tab[(old_pixel shr 8) AND $FF]
                     + mfd^.blu_tab[old_pixel AND $FF];
        dst^ := new_pixel;
        Inc(dst);
        dst^ := new_pixel;
        Inc(src);
        Inc(dst);
        Dec(w);
      until w = 0
    else
      repeat
        old_pixel := src^;
        new_pixel := (old_pixel AND $FF0000)
                     + mfd^.grn_tab[(old_pixel shr 8) AND $FF]
                     + mfd^.blu_tab[old_pixel AND $FF];
        dst^ := new_pixel;
        Inc(src);
        Inc(dst);
      Dec(w);
      until w = 0;
    Inc(src, fa^.src^._modulo);
    Inc(dst, fa^.dst^._modulo);
    end;
  Dec(h);
until h = 0;
if g_MMXenabled = true then asm emms end;
Result := 0;
end; {tutorialRunProc}

Look here how your main project file should look like.

MMX optimizations basically just require the MMX code; you spend 99.99% of your time wringing out more speed. Two more important notes:

  • Make sure your non-MMX code still works! It's easy to break your non-MMX code if you're developing on an MMX machine, since VirtualDub will default to MMX enabled.
  • Do not forget the emms instruction at the end of your filter, but make sure not to call it if MMX is not enabled.

Onto the next, and the final, chapter of this tutorial!