本文内容仅供存档说明,代码基本已经migrate进nop90的SDL移植,开发用途建议使用nop90的移植

前言

接上文,就是因为3DS上根本没有能好好用的SDL 1.2,所以自己移植咯……先给GitHub Repo:https://github.com/nbzwt/SDL-1.2-N3DS

其实我发现啊,这些个SDL 3DS移植的关系真是复杂,首先是有一个叫xerpi的人,在3年前fork了一份SDL2的repo,然后加入了对3DS的支持,虽然看commit记录似乎有模有样的样子,但是仔细看代码的话,应该是完全没有完成,而且因为库的原因也没有办法继续编译了。之后是nop90这个人,他在做SDL1.2的移植,很多代码直接用了xerpi的,当然他也做了不少修改,比如说按键部分,图形部分,至少应该是大概可以用了(当然用起来我感觉不对,哪有Flip一次不够得多来几次才能确保刷新的道理,还一言不合就卡死)。当然我可能就是第三个跳进坑里的人,在nop90的基础上继续移植(其实除了按键部分没有动,其它的都重写了)。

图形

首先要解决的问题是图形驱动,不需要那些fancy的硬件加速,更何况nop90的移植其实也就是用GPU做了下从SDL buffer复制到framebuffer的工作而已。实际用起来麻烦多多,在搞清楚之前不如就用普通的cpu循坏复制好了。基本没什么坑,配置好gfx,关闭双缓冲,参考参考nSDL的代码也就出来了,顺便支持了8bit色(然后嫌烦扔掉了15bit色的支持)。就是一点,3DS的Framebuffer不是从上到下从左到右的方式,而是从左到右从上到下的顺序,也就是扫描线是竖着的,复制起来就略蛋疼,但是考虑到主频和内存速度,也不是那么惨,跑个小游戏应该是够的。(代码:https://github.com/nbzwt/SDL-1.2-N3DS/blob/master/SDL-1.2.15/src/video/n3ds/SDL_n3dsvideo.c

所以现在图形部分的功能是(同README):

  • 除非特殊说明,没有影响SDL本身提供的功能,也没有破坏和标准的兼容性。
  • 支持任意尺寸的screen buffer,图像会显示在屏幕中央。默认屏幕是上屏幕,绘图都是没有硬件加速的,也没有双缓冲(会出现撕裂)。
  • 支持32位(RGBA8888)、24位(RGB888)、16位(RGB565)和8位(24位调色板)模式。
  • 基本上默认的初始化FLAG都会被忽略,不支持通过SDL_FULLSCREEN缩放。
  • 不能同时把两块屏幕都用作渲染设备,同时只能使用一块屏幕,但是可以把另外一块屏幕用作控制台输出。

测试代码:

#include <3ds.h>
#include <stdio.h>
#include "SDL/SDL.h"

int main(int argc, char **argv)
{
    SDL_Init(SDL_INIT_VIDEO);
    SDL_Surface *screen;
    SDL_Surface *bitmap;
  
    screen = SDL_SetVideoMode(320, 240, 16, SDL_SWSURFACE | SDL_TOPSCR | 
            SDL_CONSOLEBOTTOM);
  
    Result rs = romfsInit();
    if (rs)
        printf("romfsInit: %08lx\n", rs);
    else {
        printf("romfs Initialization succeed.\n");
        bitmap = SDL_LoadBMP("romfs:/test.bmp");
        if (bitmap == NULL)
            printf("Open Bitmap failed!\n");
        else
            SDL_BlitSurface(bitmap, NULL, screen, NULL);
    }
    printf("Should be bilted\n");
    SDL_Flip(screen);
    printf("Should be flipped\n");
    SDL_Delay(2000);
    SDL_Quit();
  
    return 0;
}

运行效果:

定时器

尚未完全完成。

多线程

xerpi的代码应该是有点历史了,反正里面有个未完成的多线程支持,总之基本上写得过程也是把原来的删了然后写自己的进去……不过这里感谢libctru,对这些东西有良好的支持,基本上对着libctru/include/3ds/svc.h和libctru/include/3ds/thread.h就能把SDL_systhread.c写出来了,毕竟只是一个封装。互斥锁可以用信号量实现,所以说顺便再实现一下信号量基本上就齐活了。这里我不是很确定我的实现是不是正确,但是因为一个坑爹的原因,基本上不用考虑太多这个问题。(代码:https://github.com/nbzwt/SDL-1.2-N3DS/tree/master/SDL-1.2.15/src/thread/n3ds

问题就是,3DS的线程调度策略是合作式。意思是说比如同时有线程A和线程B,然后线程A一直在执行,比如放了一个for(;;),那么线程B就永远不会被执行,因为线程A没有主动让出CPU时间给B。所以叫做合作式,只要有一个线程不配合,整个机子基本就是死机了……一般的让出的方法就是用Delay,毕竟Delay就相当于主动放弃CPU时间。所以修改一下之前定时器的代码,如果不是主线程就用非阻塞延时(休眠线程)。(代码:https://github.com/nbzwt/SDL-1.2-N3DS/blob/master/SDL-1.2.15/src/timer/n3ds/SDL_systimer.c

测试代码:

#include <3ds.h>
#include <stdio.h>
#include "SDL/SDL.h"
#include "SDL/SDL_thread.h"

static int testThread(void *ptr)
{
    int cnt;

    for (cnt = 0; cnt < 10; ++cnt) {
        printf("Thread %d counter: %d\n", SDL_ThreadID(), cnt);
        SDL_Delay(50);
    }

    return cnt;
}

int main(int argc, char **argv)
{
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
    SDL_Surface *screen;
    
    SDL_Thread *thread1;
    int threadReturnValue;
  
    screen = SDL_SetVideoMode(400, 240, 32, SDL_SWSURFACE | SDL_TOPSCR | 
            SDL_CONSOLEBOTTOM); //Init console
          
    thread1 = SDL_CreateThread(testThread, (void *)NULL);
  
    for (int i = 0; i < 5; i++) {
        printf("Main thread\n");
        SDL_Delay(50);
    }
  
    if (thread1 == NULL) {
        printf("\nSDL_CreateThread failed: %s\n", SDL_GetError());
    } else {
        SDL_WaitThread(thread1, &threadReturnValue);
        printf("\nThread returned value: %d", threadReturnValue);
    }
  
    SDL_Delay(10000);
    SDL_Quit();
  
    return 0;
}

声音

其实我本来是不想做多线程支持的,太蛋疼了,然而多线程是声音的前置条件,没办法了。当然我看SDL_audio.c里面虽然写着 如果没有多线程 也可以使用中断驱动,然后后面的代码就是,多线程环境初始化失败直接return -1……我猜大概是没有谁的移植真得用了中断驱动这一招。

一般3DS自制程序要调用声音有两种选择,一种是用csnd库,另外一种是用ndsp库,当然都是libctru的一部分。按照libctru注释说明的话,csnd算是已经过时的库,推荐使用ndsp,那我也就用ndsp好了。反正总之也是对着libctru的例程把SDL移植撸出来就行了。

测试代码:

#include <3ds.h>
#include <stdio.h>
#include "SDL/SDL.h"
#include "SDL/SDL_thread.h"

static Uint8 *audio_chunk;
static Uint32 audio_len;
static Uint8 *audio_pos;

void fill_audio(void *udata, Uint8 *stream, int len)
{
        /* Only play if we have data left */
        if ( audio_len == 0 )
            return;

        /* Mix as much data as possible */
        len = ( len > audio_len ? audio_len : len );
        SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME);
        audio_pos += len;
        audio_len -= len;
}

long fileLoad(const char * filename, unsigned char * * buffer) {
    FILE * pFile;
    long lSize;
  
    pFile = fopen(filename, "rb");
    if (pFile != NULL) {
        fseek(pFile, 0, SEEK_END);
        lSize = ftell(pFile);
        rewind(pFile);
        *buffer = (unsigned char *)malloc(lSize);
        if (buffer != NULL) {
            return fread(*buffer, 1, lSize, pFile);
        }
        fclose(pFile);
    }
    return 0;
}

int main(int argc, char **argv)
{
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
    SDL_Surface *screen;
    SDL_Surface *bitmap;
  
    SDL_Thread *thread1, *thread2;
    int threadReturnValue;
  
    screen = SDL_SetVideoMode(400, 240, 32, SDL_SWSURFACE | SDL_TOPSCR | 
            SDL_CONSOLEBOTTOM);
  
    romfsInit();

    audio_len = fileLoad("romfs:/test.wav", &audio_chunk);
    audio_pos = audio_chunk;
    printf("Ready to play\n");
  
    SDL_AudioSpec audioSpec;
  
    audioSpec.freq = 22050;
    audioSpec.format = AUDIO_S16;
    audioSpec.channels = 2;
    audioSpec.samples = 2048;
    audioSpec.callback = fill_audio;
    audioSpec.userdata = NULL;
  
    if (SDL_OpenAudio(&audioSpec, NULL) < 0) {
        printf("Sound initialization failed.\n");
    }
    SDL_PauseAudio(0);
    while (audio_len > 0) {
        printf("Playing: %lu left \n", audio_len);
        SDL_Delay(1000);
    }
    free(audio_chunk);
    
    SDL_Delay(10000);
    SDL_Quit();
  
    return 0;
}

【差不多就这样了】