--- a/src/MacSrc/OpenGL.cc 2026-07-19 13:11:51.000000000 +0200 +++ b/src/MacSrc/OpenGL.cc 2026-09-27 11:42:22.497868697 +0200 @@ -562,7 +562,14 @@ static bool opengl_cache_texture(CachedTexture toCache, grs_bitmap *bm) { SDL_GL_MakeCurrent(window, context); - glPixelStorei(GL_UNPACK_ROW_LENGTH, bm->row); + // GL_UNPACK_ROW_LENGTH must describe the buffer actually passed to + // glTexImage2D() below (toCache.converted->pixels, a separately + // allocated SDL_PIXELFORMAT_RGBA32 surface from + // SDL_ConvertSurfaceFormat()) - not bm->row, the original 8-bit source + // bitmap's own unrelated row stride. Using bm->row here describes the + // wrong stride to OpenGL whenever the two differ, corrupting the + // texture upload. + glPixelStorei(GL_UNPACK_ROW_LENGTH, toCache.converted->pitch / 4); if (texturesByBitsPtr.size() < MAX_CACHED_TEXTURES) { // We have enough room, generate the new texture @@ -696,7 +703,10 @@ t->lastDrawTime = *tmd_ticks; if (isDirty) { - glPixelStorei(GL_UNPACK_ROW_LENGTH, bm->row); + // Same fix as opengl_cache_texture() above - t->converted->pixels is + // what's actually uploaded, so its own pitch is what + // GL_UNPACK_ROW_LENGTH needs to describe, not bm->row. + glPixelStorei(GL_UNPACK_ROW_LENGTH, t->converted->pitch / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bm->w, bm->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, t->converted->pixels); } }