SDL_BlitScaled distortion for odd integer multiple ?

The problem

Given a square region (of some SDL_Surface) of side length L, I want to blit it but amplified to side length L * R where R is some positive integer magnification ratio, e.g. R=2 or R=3, for instance if the original square region is 10x10 I want to blit it magnified to 20x20 or 30x30 etc (amplified side length is always a multiple of original side length, so all proportions should be preserved exactly). I tried using SDL_BlitScaled setting w=h=L*R on the dstrect, but I seem to notice that for odd values of R, I get a distorted image. If instead I hand-code my own scaling routine using a naive algorithm which reads each pixel from the source area and blits it as an RxR rect, then I get the expected result with accurate scaling. see routines blit_scaled_1 and blit_scaled_2 in the source code below.

see image here : scaled blit distortion when using SDL_BlitScaled - Image on Pasteboard
(or compile the code below and try it yourself)

I’m using SDL 2.0.14 on ArchLinux.

What am I doing wrong? is this a bug in SDL_BlitScaled ?

Source code for a simple demo

Compile the following file and run it. Notice the difference between the upper and lower rows for odd scales (3 and 5), the upper row corresponds to the SDL_BlitScaled version, the lower row corresponds to the hand-coded (naive/inefficient but accurate) routine.
Two 50x50 amplified squares are getting alternatedly blitted on the exact same spot, using first one routine and then the other, if results where exactly the same then it would be impossible to notice any movement, but I can see the square shaking.

#include <assert.h>
#include <SDL2/SDL.h>

SDL_Window *window = NULL;
SDL_Surface *screen = NULL;

void init() {
  SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);

   window = SDL_CreateWindow("blit scaled",
                             SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
                             200, 200, 0);
  assert(window);

  screen = SDL_GetWindowSurface(window);
  assert(screen);
  assert(screen->format->BytesPerPixel == 4); /* for getpixel */

  SDL_FillRect(screen, NULL, 0);
}

void quit() {
  SDL_DestroyWindow(window);
  SDL_Quit();
}

Uint32 getpixel32(const SDL_Surface *surface, int x, int y) {
  /* PRE: surface format is 4 bytes per pixel */
  Uint32 *row = (Uint32 *)((Uint8*)surface->pixels + y * surface->pitch);
  return *(row + x);
}

/* blit with magnification factor 'scale'. uses SDL_BlitScaled */
void blit_scaled_1(SDL_Surface *srcsurf, const SDL_Rect *srcrect,
                 SDL_Surface *dstsurf, int x, int y, int scale) {
  SDL_Rect dstrect = {x, y, scale * srcrect->w, scale * srcrect->h};
  /* e.g. if scale = 2 and rect is 10x10, then dstrect is 20x20 */
  SDL_BlitScaled(srcsurf, srcrect, dstsurf, &dstrect);
}

/* blit with magnification factor 'scale'. uses hand-coded naive routine, might
   be much slower than SDL_BlitScaled but seems accurate */
void blit_scaled_2(const SDL_Surface *srcsurf, const SDL_Rect *srcrect,
                   SDL_Surface *dstsurf, int x, int y, int scale) {
  /* PRE: surface format is 4 bytes per pixel. scale > 0, ideally > 1 */
  assert(srcsurf);
  assert(srcrect);
  assert(dstsurf);

  int x0 = x;
  int sx0 = srcrect->x;
  int sy0 = srcrect->y;
  SDL_Rect r = {x, y, scale, scale}; /* shape of "scaled pixel" */
  for (int y = 0; y < srcrect->h; ++y) {
    for (int x = 0; x < srcrect->w; ++x) {
      Uint32 color = getpixel32(srcsurf, sx0 + x, sy0 + y);
      SDL_FillRect(dstsurf, &r, color);
      r.x += scale;
    }
    /* end of row, go to the next one */
    r.x = x0;
    r.y += scale;
  }
}

/* draws a sequence of scaled-up versions of the 10x10 rect at (0,0) using both
   scale routines for comparison: upper row uses SDL_BlitScaled, lower row uses
   hand-coded routine. of special interest are odd amplification factors, where
   SDL_BlitScaled seems to cause distortion */
void draw_scaled_rects_seq(int x, int y) {
  const int rectlen = 10;
  SDL_Rect src = {0, 0, rectlen, rectlen};
  const int gap = 5;
  for (int scale = 2; scale <= 5; ++scale) {
    blit_scaled_1(screen, &src, screen, x, y, scale);
    blit_scaled_2(screen, &src, screen, x, y + rectlen * scale + gap, scale);
    x += rectlen * scale + gap;
  }
}

void draw_10x10rect() {
  {
    /* outer color */
    SDL_Rect r = {0, 0, 10, 10};
    Uint32 color = SDL_MapRGB(screen->format, 0, 255, 0);
    SDL_FillRect(screen, &r, color);
  }
  {
    /* inner color */
    SDL_Rect r = {1, 1, 8, 8};
    Uint32 color = SDL_MapRGB(screen->format, 255, 0, 0);
    SDL_FillRect(screen, &r, color);
  }
}

int main(int argc, char* argv[]) {
  init();

  /* create a 10x10 bitmap at (0,0). we will use this as source for scaled
     blitting tests */

  SDL_FlushEvent(SDL_KEYDOWN);
  int running = 1;
  int tick = 0;
  while (running) {
    SDL_Event ev;
    while (SDL_PollEvent(&ev)) {
      if (ev.type == SDL_QUIT) {running = 0; break;}
      if (ev.type == SDL_KEYDOWN &&
           (ev.key.keysym.sym == SDLK_SPACE ||
            ev.key.keysym.sym == SDLK_RETURN ||
            ev.key.keysym.sym == SDLK_ESCAPE)) {running = 0; break;}
    }

    /* create a 10x10 bitmap at (0,0) */
    SDL_FillRect(screen, NULL, 0);
    draw_10x10rect();

    /* blit the 10x10 bitmap at (0,0) with incrementing sizes, using both
       scaling routines */
    draw_scaled_rects_seq(15, 0);

    /* alternate between both scaling routines blitting at the same spot with
       the same scale factor. image should be visually stable if both scaling
       blit routines do the same thing, otherwise you might see it shake! */
    SDL_Rect src = {0, 0, 10, 10};
    int y = 70;
    int scale = 5;              /* try it with values 2,3,4,5 */
    if (tick == 0) {
      blit_scaled_1(screen, &src, screen, 0, y, scale); tick = 1;
    } else {
      blit_scaled_2(screen, &src, screen, 0, y, scale); tick = 0;
    }

    SDL_UpdateWindowSurface(window);
    SDL_Delay(10);
  }

  quit();

  return 0;
}

I mean, the old software drawing routines like SDL_BlitScaled() are optimized for speed over pixel-perfect accuracy.

Oh, I see, I didn’t know that from reading the documentation on the wiki page (I think it would be better if it said so explicitly). I’ll be avoiding this function when pixel-perfect accuracy is required then. Thanks!