Homebrew Citro2D C2D_Image example

  • Thread starter Thread starter sheepy0125
  • Start date Start date
  • Views Views 1,944
  • Replies Replies 8
  • Likes Likes 3

sheepy0125

New Member
Newbie
Joined
Dec 9, 2024
Messages
4
Reaction score
3
Trophies
0
XP
48
Country
United States
hey! I've noticed there weren't any good examples for drawing an *image* with Citro2D. spritesheets and t3x files, sure!
anyways, I want to share what I learnt from digging around other homebrew projects for someone in a similar position to me ^w^.

here is a snippet from git DOT sr DOT ht/~sheepy/c2d-image-example/ @ source/main.c! check out the README.md in that repo for a rust3ds version, too.

C:
const u32 next_pow2(u32 n) {
  n--;
  n |= n >> 1;
  n |= n >> 2;
  n |= n >> 4;
  n |= n >> 8;
  n |= n >> 16;
  n++;
  return n;
}
const u32 clamp(u32 n, u32 lower, u32 upper) {
  if (n < lower)
    return lower;
  if (n > upper)
    return upper;
  return n;
}
const u32 rgba_to_abgr(u32 px) {
  u8 r = (px & 0xff000000) >> 24;
  u8 g = (px & 0x00ff0000) >> 16;
  u8 b = (px & 0x0000ff00) >> 8;
  u8 a = px & 0x000000ff;
  return (a << 24) | (b << 16) | (g << 8) | r;
}

/** Read an RGBA image from `path` with dimensions `image_width`x`image_height`
 * and return a `C2D_Image` object.
 * Assumes image data is stored left->right, top->bottom.
 * Dimensions must be within 64x64 and 1024x1024.
 * svcBreak's if the file can't be opened. */
C2D_Image get_image(const char *path, u32 image_width, u32 image_height) {
  // Open file
  FILE *file = fopen(path, "rb");
  if (file == NULL) {
    fprintf(stderr, "failed to open `%s'", path);
    svcBreak(USERBREAK_PANIC);
  }
  u32 px_count = image_width * image_height;
  u32 *rgba_raw = malloc(px_count * sizeof(u32));
  fread((char *)rgba_raw, sizeof(u32), px_count, file);

  // Image data
  C2D_Image image;

  // Base texture
  C3D_Tex *tex = malloc(sizeof(C3D_Tex));
  image.tex = tex;
  // Texture dimensions must be square powers of two between 64x64 and 1024x1024
  tex->width = clamp(next_pow2(image_width), 64, 1024);
  tex->height = clamp(next_pow2(image_height), 64, 1024);

  // Subtexture
  Tex3DS_SubTexture *subtex = malloc(sizeof(Tex3DS_SubTexture));
  image.subtex = subtex;
  subtex->width = image_width;
  subtex->height = image_height;
  // (U, V) coordinates
  subtex->left = 0.0f;
  subtex->top = 1.0f;
  subtex->right = (float)image_width / (float)tex->width;
  subtex->bottom = 1.0 - ((float)image_height / (float)tex->height);

  C3D_TexInit(tex, tex->width, tex->height, GPU_RGBA8);
  C3D_TexSetFilter(tex, GPU_LINEAR, GPU_NEAREST);

  memset(tex->data, 0, px_count * 4);
  for (u8 i = 0; i < image_height; i++) {
    for (u8 j = 0; j < image_width; j++) {
      u32 src_idx = (j * image_width) + i;
      u32 rgba_px = rgba_raw[src_idx];
      u32 abgr_px = rgba_to_abgr(rgba_px);

      
      // 8x8 Morton Z-order swizzling, c.f.
      // https://problemkaputt.de/gbatek-3ds-video-texture-swizzling.htm
      // https://github.com/astronautlevel2/Anemone3DS/blob/ba08ab9108cec81a4fcb31d12a2af09bab589b82/source/loading.c#L338
      // https://github.com/devkitPro/tex3ds/blob/master/source/swizzle.cpp
      u32 dst_ptr_offset = ((((j >> 3) * (tex->width >> 3) + (i >> 3)) << 6) +
                            ((i & 1) | ((j & 1) << 1) | ((i & 2) << 1) |
                             ((j & 2) << 2) | ((i & 4) << 2) | ((j & 4) << 3)));
      ((u32 *)tex->data)[dst_ptr_offset] = abgr_px;
    }
  }

  free(rgba_raw);

  return image;
}

anyway, I'm hoping this can help someone out. It's a *lot* faster than writing to the fb manually, & useful for cases where you can't just use a spritesheet (e.g. album art).
 
Last edited by sheepy0125,
I've genuinely spent hours going through different projects, examples, all sorts, just trying to find a way to make an image in memory then draw it to screen, the closest attempt I got had the swizzle result and i just couldn't figure how to unswizzle or where the documentation for it was at all, your linked repo (bit of a puzzle to get to) totally just saved me so much thANK you lol

Idk why writing rgba in memory doesnt seem to need the abgr switch so I removed that
With some editing I finally reached the non swizzled fully coded to memory image magic ive been longing for :cry::yay3ds:
1745265978818.png

Post automatically merged:

just spent some time making that image scaleable (every time i tried a rectangular image it was cropped off into a square anyway) but have got it now.
Important to note, if you ever stop using the image or you try to replace it using the function again you need to call
Code:
if (image.tex) {
      C3D_TexDelete(image.tex);
      free(image.tex);
  }
  if (image.subtex) {
      free((Tex3DS_SubTexture*)image.subtex);
  }
or else the previous image will stay in memory, stack up and eventually freeze the program

If anyone wants a program that draws a rainbow as any rectangle then prints it at any scale here's the full file code:grog:
Code:
/** Example to draw in mem a RGBA rainbow and display it with C2D */

#include <3ds.h>
#include <3ds/romfs.h>
#include <3ds/svc.h>
#include <c2d/base.h>
#include <c3d/texture.h>
#include <citro2d.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define CLEAR_COLOR C2D_Color32(0xFF, 0xD8, 0xB0, 0x68)

const u32 next_pow2(u32 n) {
  n--;
  n |= n >> 1;
  n |= n >> 2;
  n |= n >> 4;
  n |= n >> 8;
  n |= n >> 16;
  n++;
  return n;
}
const u32 clamp(u32 n, u32 lower, u32 upper) {
  if (n < lower)
    return lower;
  if (n > upper)
    return upper;
  return n;
}

C2D_Image get_image(u32 image_widthy, u32 image_heighty) {

    u32 image_width = image_widthy;
    u32 image_height = image_heighty;
    if (image_widthy > image_heighty) {
        image_height = image_widthy;
    }
    else {
        image_width = image_heighty;
    }

    // Create the raw RGBA pixel data
    u32 px_count = image_width * image_height;
    u32* rgba_raw = malloc(px_count * sizeof(u32));

    for (int i = 0; i < image_heighty; i++) {
        for (int j = 0; j < image_widthy; j++) {
            float hue = fmodf((float)(image_widthy - 1 - j) / (image_widthy - 1) - 1.0f / 3.0f + 1.0f, 1.0f);
            float sat = 1.0f - ((float)i / (image_heighty - 1));
            float val = 1.0f;

            float c = val * sat;
            float x = c * (1 - fabsf(fmodf(hue * 6.0f, 2.0f) - 1.0f));
            float m = val - c;

            float r, g, b;
            if (hue < 1.0f / 6.0f) { r = c; g = x; b = 0; }
            else if (hue < 2.0f / 6.0f) { r = x; g = c; b = 0; }
            else if (hue < 3.0f / 6.0f) { r = 0; g = c; b = x; }
            else if (hue < 4.0f / 6.0f) { r = 0; g = x; b = c; }
            else if (hue < 5.0f / 6.0f) { r = x; g = 0; b = c; }
            else { r = c; g = 0; b = x; }

            int red = (int)((r + m) * 255.0f);
            int green = (int)((g + m) * 255.0f);
            int blue = (int)((b + m) * 255.0f);

            rgba_raw[i * image_width + j] = (blue << 24) | (green << 16) | (red << 8) | 0xFF;
        }
    }

    for (int i = 0; i < image_heighty; i++) {
        for (int j = image_widthy; j < image_width; j++) {
            rgba_raw[i * image_width + j] = 0x00000000; // Transparent pixel
        }
    }
    if (image_width > image_heighty) {
        for (int i = image_heighty; i < image_width; i++) {
            for (int j = 0; j < image_width; j++) {
                rgba_raw[i * image_width + j] = 0x00000000; // Transparent pixel
            }
        }
    }

    // Image data
    C2D_Image image;
    C3D_Tex* tex = malloc(sizeof(C3D_Tex));
    image.tex = tex;

    // Texture dimensions must be square powers of two between 64x64 and 1024x1024
    tex->width = clamp(next_pow2(image_width), 64, 1024);
    tex->height = clamp(next_pow2(image_height), 64, 1024);

    // Subtexture
    Tex3DS_SubTexture* subtex = malloc(sizeof(Tex3DS_SubTexture));
    image.subtex = subtex;
    subtex->width = image_width;
    subtex->height = image_height;

    // (U, V) coordinates
    subtex->left = 0.0f;
    subtex->top = 1.0f;
    subtex->right = (float)image_width / (float)tex->width;
    subtex->bottom = 1.0 - ((float)image_height / (float)tex->height);

    // Initialize the texture with a specific format
    C3D_TexInit(tex, tex->width, tex->height, GPU_RGBA8);
    C3D_TexSetFilter(tex, GPU_NEAREST, GPU_NEAREST);

    // Clear texture data
    memset(tex->data, 0, tex->width * tex->height * sizeof(u32));

    // Process the pixel data to convert it to the correct format and swizzle it
    for (int i = 0; i < image_height; i++) {
        for (int j = 0; j < image_width; j++) {

            // Swizzle magic to convert into a t3x format
            u32 dst_ptr_offset = ((((j >> 3) * (tex->width >> 3) + (i >> 3)) << 6) +
                ((i & 1) | ((j & 1) << 1) | ((i & 2) << 1) |
                    ((j & 2) << 2) | ((i & 4) << 2) | ((j & 4) << 3)));

            // Store the swizzled pixel in the texture
            ((u32*)tex->data)[dst_ptr_offset] = rgba_raw[j * image_width + i];
        }
    }

    free(rgba_raw);

    return image;
}


int main() {
  romfsInit();

  gfxInitDefault();
  C3D_Init(C3D_DEFAULT_CMDBUF_SIZE);
  C2D_Init(C2D_DEFAULT_MAX_OBJECTS);
  C3D_RenderTarget *top = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT);
  C2D_Prepare();
  consoleInit(GFX_BOTTOM, NULL);

  C2D_Image image = get_image(64, 64);
  // MUST free images when not used/overwritten due to use of malloc (useless here but included cus important)
  if (image.tex) {
      C3D_TexDelete(image.tex);
      free(image.tex);
  }
  if (image.subtex) {
      free((Tex3DS_SubTexture*)image.subtex);
  }
  image = get_image(400, 240);

  int frameCount = 0;
  //int value = 0, value2 = 0;
  //float imgW = 64;
  //float imgH = 64;

  // Main loop
  while (aptMainLoop()) {
    hidScanInput();

    u32 kDown = hidKeysDown();
    if (kDown & KEY_START)
      break;

    C3D_FrameBegin(C3D_FRAME_SYNCDRAW);
    C2D_SceneBegin(top);
    C2D_TargetClear(top, CLEAR_COLOR);

    C2D_DrawImageAt(image, 0.0f, 0.0f, 0.0f, NULL, 1.0f, 1.0f);

 
    printf("%d\n", frameCount);
    C3D_FrameEnd(0);
    frameCount++;
  }

  C3D_TexDelete(image.tex);
  free((void *)image.subtex);

  C2D_Fini();
  C3D_Fini();
  gfxExit();
  romfsExit();

  return 0;
}

One thing tho, the way the rectangle image thing works, I noticed drawing the image was always a square - cropping off whatever data existed to the right if the width > height, same cropping but off the bottom when height>width - So to work around it my function creates the image as a square using whichever width/height value is larger, then just fills the rest of the square with 0 alpha pixels, result is technically drawing a square but visually the full rectangle.

Now I don't know if thats a silly workaround, maybe theres a better way, I couldn't figure it out tho 👁️

edit: the malloc on that rgba definition i pasted above is wrong, it needs to use the clamping + nearest power! oopsie there
 
Last edited by Blurro,
ehe <3 I was in the same predicament so I totally get it!! I'm just so glad that people (who know way more than me!) already dealt with this before & I was able to just plagiarize from them.

about the memory leak in that snippet: in the full example, I called C3D_TexDelete() and free'd the subtex at the end of main():
C:
C3D_TexDelete(image.tex);
free((void *)image.subtex);
(which, that should have been in the snippet here, too :P).
and, FWIW, you don't need to free() image.tex after you call C3D_TexDelete() on it -- the only heap-alloc'd data on it is the `void *data` field, and C3D_TexDelete() already takes care of that:
C:
// (snippet from citro3d @ source/texture.c @ line 242)
void C3D_TexDelete(C3D_Tex* tex)
{
    if (C3Di_TexIs2D(tex))
        allocFree(tex->data);
    else
        C3Di_TexCubeDelete(tex->cube);
}

...but, needing to make the width and height the same doesn't make sense. you shouldn't need to do that! oh my god, I know why. I swapped width and height in the for loops 😭. bwuh.
Diff:
diff --git a/source/main.c b/source/main.c
index 9861d94..58b62e7 100644
--- a/source/main.c
+++ b/source/main.c
@@ -80,8 +80,8 @@ C2D_Image get_image(const char *path, u32 image_width, u32 image_height) {
   C3D_TexSetFilter(tex, GPU_LINEAR, GPU_NEAREST);

   memset(tex->data, 0, px_count * 4);
-  for (u8 i = 0; i < image_height; i++) {
-    for (u8 j = 0; j < image_width; j++) {
+  for (u8 i = 0; i < image_width; i++) {
+    for (u8 j = 0; j < image_height; j++) {
       u32 src_idx = (j * image_width) + i;
       u32 rgba_px = rgba_raw[src_idx];
       u32 abgr_px = rgba_to_abgr(rgba_px);
that's so silly. I don't know how I missed that -- I guess I just didn't notice my test image was cropped :P.

p.s. sorry about the "bit of a puzzle to get to" part!! I hosted that on sourcehut (which has a truly horrendous UI!) and I couldn't paste links because my account had too few posts.
 
oh my god, I know why. I swapped width and height in the for loops 😭. bwuh.
dang bruh lol i was trying all sorts to fix. wish i was better at tracking down the source of issues like this :wacko:
Thanks for finding that tho and the freeing tip, have ditched the big square+crop method now lol.
What're you working on? I'm currently on this pictochat remake idea ive got, figured the sent messages should be the canvas + user's drawing baked together as one pic (at least the on-screen drawings, backlog drawings stored as not canvas merged RLE or sum not sure yet) so I went on quite the journey and found myself to your handy post
1745435654142.png

Top msg box used the same sprite as the bottom msg box but duplicated it into raw data, unswizzled and rotated back (t3x sometimes saves 90degrees), edited pixels (that black line on it) to draw baked into one pic, reswizzled and printed to the screen.

I did NOT expect it to be so difficult to essentially just edit a pic and have it show onscreen it really bugged me (though I may just be stupid and someone else wouldve done the same without much issue and less code)

And omg having to perfectly have the right size of array, remember that size, make sure not to write past it and cause leaks everywhere, im fresh off from c# I didnt expect this memory handling pain
 
oh, that's super cool!!! I'd really love to see the code if you have it hosted anywhere >W<..
it sounds like you're making it harder than it needs to be.. have you considered blitting a user-drawn image on top of (i.e. after) a regular canvas sprite?

I definitely feel the memory safety pain!! I'm working in Rust so I can have everything nicely wrapped with RAII and wide pointers.. so that's a bonus

I'm using this to display album art for a silly music player! ! I also used this in another project (gamecart powered GPS) to display the map
 

Attachments

  • Captura de pantalla_20250423_221725.png
    Captura de pantalla_20250423_221725.png
    449.7 KB · Views: 70
it sounds like you're making it harder than it needs to be.. have you considered blitting a user-drawn image on top of (i.e. after) a regular canvas sprite?
well then itd be drawing two images instead of one

I'll share the project after i do some more but first sort out where this current leak is coming from, cus I checked every alloc and free and array assignment and it all looks fine yet it crashes around 30-70 min of running (i left it open for a while) gotta start tryna debug and see memory stuff

and thats very nice i like your colours n graphics

Update: Turns out the source of the memory leaking I had was removing that 'free' line after TexDelete (the one you suggested removing) lol, the reason is because usually you point tex to a sprite from a spritesheet, but my function is allocating memory to tex manually in order to use the code-generated image, not one already loaded. TexDelete deletes just tex->data and leaves tex stacking up if you're doing what I'm doing with image generating/modifying. SOOOoo its going back in and ill add a comment too
 
Last edited by Blurro,
oh, really? thank you for pointing that out, I'll be sure to fix that in my project ^w<. I totally thought it was unneeded. thank you so much for correcting me
 
i am going through the hell of 3ds homebrew dynamic images, what types of images does this support? i need to use jpg and png in my project
Post automatically merged:

this is what happens when i try to set the char of rgba_raw to an stbi_loaded image
1777783429226.png
 
Last edited by loglot,

Site & Scene News

Popular threads in this forum