Skip to content

Conversation

@marcin-serwin
Copy link

According to the docs, SDL_PixelFormat is a read-only structure. Creating it manually leaves out some important fields like format and next pointer to be undefined.

@marcin-serwin marcin-serwin requested a review from a team as a code owner November 8, 2025 18:49
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 8, 2025

📝 Walkthrough

Walkthrough

Replaces stack-allocated SDL_PixelFormat usage with dynamically allocated SDL_PixelFormat* via SDL_AllocFormat(SDL_MasksToPixelFormatEnum(...)) across surf_convert, surf_convert_alpha, and pgSurface_Blit in src_c/surface.c; applies palettes with SDL_SetPixelFormatPalette(...) for indexed formats and ensures SDL_FreeFormat(...) is called on all paths.

Changes

Cohort / File(s) Summary
Pixel format allocation & conversion updates
src_c/surface.c
Replace stack SDL_PixelFormat instances with SDL_AllocFormat(format_enum) in surf_convert and surf_convert_alpha; compute format_enum via SDL_MasksToPixelFormatEnum(...), use allocated format for SDL_ConvertSurface/PG_ConvertSurface, apply palette via SDL_SetPixelFormatPalette(format, ...) for indexed formats, and free with SDL_FreeFormat(format) on all paths.
Blit path temporary format handling
src_c/surface.c
In pgSurface_Blit, replace local newfmt with SDL_AllocFormat(...); pass it to PG_ConvertSurface/SDL_BlitSurface, adjust palette handling to the allocated format, and ensure SDL_FreeFormat(newfmt) on success and error paths.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Blit/Convert caller
  participant Src as Source surface
  participant MaskEnum as SDL_MasksToPixelFormatEnum
  participant Alloc as SDL_AllocFormat
  participant Pal as SDL_SetPixelFormatPalette
  participant Conv as SDL_ConvertSurface / PG_ConvertSurface
  participant Free as SDL_FreeFormat
  participant Dest as Destination surface

  Caller->>MaskEnum: compute format_enum from masks/bpp
  MaskEnum-->>Caller: format_enum
  Caller->>Alloc: SDL_AllocFormat(format_enum)
  Alloc-->>Caller: format*
  alt indexed format
    Caller->>Pal: SDL_SetPixelFormatPalette(format*, palette)
  end
  Caller->>Conv: convert source using format* -> converted_src
  Conv-->>Caller: converted_src
  Caller->>Free: SDL_FreeFormat(format*)
  Caller->>Dest: SDL_BlitSurface(converted_src -> Dest)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Verify all early-return and error paths free allocated formats (SDL_FreeFormat).
  • Confirm palette application uses the allocated format and matches previous semantics.
  • Check for potential use-after-free when passing allocated format into conversion/blit functions.

Suggested reviewers

  • ankith26

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: replacing manual SDL_PixelFormat creation with SDL_AllocFormat calls throughout the codebase.
Description check ✅ Passed The description clearly explains the rationale for the changes by referencing SDL documentation and noting the issue with undefined fields when manually creating SDL_PixelFormat structures.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 035dd86 and a7ff64e.

📒 Files selected for processing (1)
  • src_c/surface.c (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src_c/surface.c

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Member

@ankith26 ankith26 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for contributing to pygame-ce! 🎉

@ankith26 ankith26 added this to the 2.5.7 milestone Nov 8, 2025
@marcin-serwin
Copy link
Author

I've run into the same issue in surf_convert. @ankith26 could you re-review?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src_c/surface.c (1)

4533-4545: Handle NULL from SDL_AllocFormat in blit path

The blit fallback has the same risk: if SDL_MasksToPixelFormatEnum yields something unsupported or we hit OOM, SDL_AllocFormat returns NULL. Passing that straight into PG_ConvertSurface will dereference it and crash. Guard the allocation, only blit once a converted surface exists, and propagate the error otherwise.

         else {
             SDL_PixelFormat *fmt = src->format;
-            SDL_PixelFormat *newfmt =
-                SDL_AllocFormat(SDL_MasksToPixelFormatEnum(
-                    fmt->BitsPerPixel, fmt->Rmask, fmt->Gmask, fmt->Bmask, 0));
-
-            src = PG_ConvertSurface(src, newfmt);
-
-            SDL_FreeFormat(newfmt);
-            if (src) {
+            Uint32 tmp_format_enum =
+                SDL_MasksToPixelFormatEnum(fmt->BitsPerPixel, fmt->Rmask,
+                                           fmt->Gmask, fmt->Bmask, 0);
+            SDL_PixelFormat *newfmt = SDL_AllocFormat(tmp_format_enum);
+            SDL_Surface *converted = NULL;
+
+            if (!newfmt) {
+                result = -1;
+            }
+            else {
+                converted = PG_ConvertSurface(src, newfmt);
+                SDL_FreeFormat(newfmt);
+            }
+
+            if (converted) {
+                src = converted;
 #if SDL_VERSION_ATLEAST(3, 0, 0)
                 result = SDL_BlitSurface(src, srcrect, dst, dstrect) ? 0 : -1;
 #else
                 result = SDL_BlitSurface(src, srcrect, dst, dstrect);
 #endif
                 SDL_FreeSurface(src);
             }
             else {
                 result = -1;
             }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e3fbdc and 8b0eda1.

📒 Files selected for processing (1)
  • src_c/surface.c (3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:115-126
Timestamp: 2025-09-01T20:22:31.010Z
Learning: In pygame-ce PR #3573, the PG_SURF_BitsPerPixel function implementation uses hardcoded return values of 32 for YUY2/UYVY/YVYU FOURCC formats because it's copying the exact logic from SDL's internal SDL_GetMasksForPixelFormat function. This ensures perfect compatibility between SDL2 and SDL3 behaviors rather than using the technically more accurate values that might be returned by SDL_GetPixelFormatDetails.
📚 Learning: 2025-09-01T20:18:57.500Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.

Applied to files:

  • src_c/surface.c
📚 Learning: 2025-09-01T20:22:31.010Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:115-126
Timestamp: 2025-09-01T20:22:31.010Z
Learning: In pygame-ce PR #3573, the PG_SURF_BitsPerPixel function implementation uses hardcoded return values of 32 for YUY2/UYVY/YVYU FOURCC formats because it's copying the exact logic from SDL's internal SDL_GetMasksForPixelFormat function. This ensures perfect compatibility between SDL2 and SDL3 behaviors rather than using the technically more accurate values that might be returned by SDL_GetPixelFormatDetails.

Applied to files:

  • src_c/surface.c
🧬 Code graph analysis (1)
src_c/surface.c (2)
src_c/base.c (1)
  • pg_UintFromObjIndex (591-604)
src_c/surflock.c (1)
  • pgSurface_Unprep (51-59)

@marcin-serwin marcin-serwin force-pushed the push-wmkrxtzwskyk branch 2 times, most recently from 3186ee9 to ffd60e2 Compare November 9, 2025 21:32
@aatle aatle added the Surface pygame.Surface label Nov 9, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src_c/surface.c (1)

1846-1846: Consider conditional palette allocation.

The palette is allocated unconditionally at line 1846 but only used for indexed formats (lines 1944-1956). This means non-indexed conversions allocate and free an unused palette. The SDL3 path (lines 1779-1790) demonstrates a more efficient approach by only allocating the palette when SDL_ISPIXELFORMAT_INDEXED(format_enum) is true.

Consider adopting the SDL3 pattern:

-            SDL_Palette *palette = SDL_AllocPalette(default_palette_size);
+            SDL_Palette *palette = NULL;
             Uint32 format_enum = 0;
 
             if (pg_IntFromObj(argobject, &bpp)) {
                 // ... compute format_enum ...
             }
             // ... other branches ...
+
             SDL_PixelFormat *format = SDL_AllocFormat(format_enum);
             if (!format) {
-                SDL_FreePalette(palette);
                 pgSurface_Unprep(self);
                 return RAISE(pgExc_SDLError, SDL_GetError());
             }
 
             if (SDL_ISPIXELFORMAT_INDEXED(format_enum)) {
                 if (SDL_ISPIXELFORMAT_INDEXED(PG_SURF_FORMATENUM(surf))) {
                     SDL_SetPixelFormatPalette(format, surf->format->palette);
                 }
                 else {
+                    palette = SDL_AllocPalette(default_palette_size);
+                    if (!palette) {
+                        SDL_FreeFormat(format);
+                        pgSurface_Unprep(self);
+                        return RAISE(pgExc_SDLError, SDL_GetError());
+                    }
                     SDL_SetPaletteColors(palette, default_palette_colors, 0,
                                          default_palette_size);
                     SDL_SetPixelFormatPalette(format, palette);
                 }
             }
             newsurf = PG_ConvertSurface(surf, format);
             SDL_SetSurfaceBlendMode(newsurf, SDL_BLENDMODE_NONE);
             SDL_FreeFormat(format);
-            SDL_FreePalette(palette);
+            if (SDL_ISPIXELFORMAT_INDEXED(format_enum) &&
+                !SDL_ISPIXELFORMAT_INDEXED(PG_SURF_FORMATENUM(surf))) {
+                SDL_FreePalette(palette);
+            }

Also applies to: 1960-1960

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffd60e2 and 035dd86.

📒 Files selected for processing (1)
  • src_c/surface.c (3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:115-126
Timestamp: 2025-09-01T20:22:31.010Z
Learning: In pygame-ce PR #3573, the PG_SURF_BitsPerPixel function implementation uses hardcoded return values of 32 for YUY2/UYVY/YVYU FOURCC formats because it's copying the exact logic from SDL's internal SDL_GetMasksForPixelFormat function. This ensures perfect compatibility between SDL2 and SDL3 behaviors rather than using the technically more accurate values that might be returned by SDL_GetPixelFormatDetails.
📚 Learning: 2025-09-01T20:18:57.500Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.

Applied to files:

  • src_c/surface.c
📚 Learning: 2025-09-01T20:22:31.010Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:115-126
Timestamp: 2025-09-01T20:22:31.010Z
Learning: In pygame-ce PR #3573, the PG_SURF_BitsPerPixel function implementation uses hardcoded return values of 32 for YUY2/UYVY/YVYU FOURCC formats because it's copying the exact logic from SDL's internal SDL_GetMasksForPixelFormat function. This ensures perfect compatibility between SDL2 and SDL3 behaviors rather than using the technically more accurate values that might be returned by SDL_GetPixelFormatDetails.

Applied to files:

  • src_c/surface.c
🧬 Code graph analysis (1)
src_c/surface.c (2)
src_c/base.c (1)
  • pg_UintFromObjIndex (591-604)
src_c/surflock.c (1)
  • pgSurface_Unprep (51-59)

Copy link
Member

@ankith26 ankith26 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTm, thanks again! 🎉

According to the docs, `SDL_PixelFormat` is a read-only structure.
Creating it manually leaves out some important fields like `format` and
`next` pointer to be undefined.

Signed-off-by: Marcin Serwin <marcin@serwin.dev>
Copy link
Member

@Starbuck5 Starbuck5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like I wanted to do stuff like this when porting SDL3 but I (probably with Ankith's encouragement), decided not to, in order to just leave the old code functioning how it is.

What is the justification for changing this code?

@marcin-serwin
Copy link
Author

What is the justification for changing this code?

Since libsdl-org/sdl2-compat#521 sdl2-compat relies on the format and next fields to be set correctly. The old code doesn't set them which causes some of the surface tests (e.g., GeneralSurfaceTests.test_convert_palettize, SurfaceSelfBlitTest.test_colorkey) to fail with the new 2.32.58 version.

@ankith26
Copy link
Member

It is interesting to note that the PR has been (partially) reverted as of libsdl-org/sdl2-compat#545 which was merged just a few days ago, which may change things.

However I think this PR is still worth merging because it is making the code more robust.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Surface pygame.Surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants