SDL

android下的SDL实现(SDL2-2.0.12)

一、activity生命周期

网上已有不少文章叙述activity生命周期,像“Android生命周期详解[1] ”。

1.1 mIsResumedCalled、mSurface.mIsSurfaceReady和mHasFocus

  • mIsResumedCalled。是true时,指示当前执行了onStart/onResume。
  • mSurface.mIsSurfaceReady。是true时,指示当前执行了SDLSurface.surfaceChanged。
  • mHasFocus。是true时,指示当前执行了onWindowFocusChanged(hasFocus:true)。

这三个变量的一个作用是决定何时创建并运行SDLMain。

public static void handleNativeState() {
    ...
    // Try a transition to resumed state
    if (mNextNativeState == NativeState.RESUMED) {
        if (mSurface.mIsSurfaceReady && mHasFocus && mIsResumedCalled) {
            if (mSDLThread == null) {
                // This is the entry point to the C app.
                // Start up the C app thread and enable sensor input for the first time
                // FIXME: Why aren't we enabling sensor input at start?

                mSDLThread = new Thread(new SDLMain(), "SDLThread");
                mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true);
                mSDLThread.start();

                // No nativeResume(), don't signal Android_ResumeSem
                mSurface.handleResume();
            } else {
                nativeResume();
                mSurface.handleResume();
            }

            mCurrentNativeState = mNextNativeState;
        }
    }
}

以下概述SDL处理生命周期步及到函数,要结合上面的handleNativeState()去理解。假设操作的app是kdesktop。

用户操作:在后台app列表,点击kdesktop窗口,让恢复到前台运行
onStart()
onResume()
  mIsResumedCalled = true;
  mNextNativeState = NativeState.RESUMED;
  SDLActivity.handleNativeState(); // mCurrentNativeState是Paused,希望转到RESUMED。if (mIsSurfaceReady && mHasFocus && mIsResumedCalled),此时mIsSurfaceReady、mHasFocus都是false,这函数其实什么也没做。
SDLSurface.surfaceChanged(width:1920, height:1080)
  mIsSurfaceReady = true;
  SDLActivity.onNativeSurfaceChanged();
  SDLActivity.handleNativeState(); // mCurrentNativeState是Paused,希望转到RESUMED。if (mIsSurfaceReady && mHasFocus && mIsResumedCalled),此时mHasFocus是false,这函数其实什么也没做。
onWindowFocusChanged(hasFocus:true)
  mHasFocus = true;
  mNextNativeState = NativeState.RESUMED;
  SDLActivity.handleNativeState(); // mCurrentNativeState是Paused,希望转到RESUMED。if (mIsSurfaceReady && mHasFocus && mIsResumedCalled),此时三个布尔变量都已是true,执行创建并运行SDLMain线程。

用户操作:把kdesktop切到后台
onWindowFocusChanged(hasFocus:false)
  mHasFocus = false;
  mNextNativeState = NativeState.PAUSED;
  SDLActivevity.handleNativeState();
onPause()
  mIsResumedCalled = false;
  mNextNativeState = NativeState.PAUSED;
  SDLActivevity.handleNativeState(); // 由于onWindowFocusChanged已经让SDL处于PAUSED状态,此个handleNativeState其实是什么都不做。
onStop()
onSurfaceDestroy()
  mIsSurfaceReady = false;

用户操作:在后台app列表,移除kdesktop窗口
onDestroy()
  mNextNativeState = NativeState.PAUSED;
  SDLActivity.handleNativeState();

1.2 mSDLThread(SDLMain)的生命周期

初始值是null;

mIsResumedCalled、mSurface.mIsSurfaceReadyhandle和mHasFocus都是true后,创建并启动mSDLThread。

onDestroy时,mSDLThread=null,即销毁mSDLThread。

除了第一次resume,其它的resume/pause时,不会去触碰mSDLThread的生命周期。

1.3 android没有回调onDestroy()

按正常流程,退出app后总应该调用onDestroy(),但基于SDL2-2.0.12写app时,确实遇到onDestroy()没被android回调。至少在android 7.1(AIO-3399J)、android 5.1.1(AIO-3288J)这两种主板上发现了这现象。

到现在还没找到原因,但找到种方法可大概率让能调用ondestroy()。方法是新建一个Service,在app运行期间,这个Service一直运行。在Service主线程,执行操作就是不断调用sleep。把这Service叫ForOnDestroyService。

private void startForOnDestroyService() {
    Intent playbackAudioServiceIntent = new Intent(mSingleton.getApplicationContext(), ForOnDestroyService.class);
    mSingleton.startService(playbackAudioServiceIntent);
}

private void stopForOnDestroyService() {
    if (!ForOnDestroyService.mExitThread) {
        ForOnDestroyService.mExitThread = true;
        Intent playbackAudioServiceIntent = new Intent(mSingleton.getApplicationContext(), ForOnDestroyService.class);
        mSingleton.stopService(playbackAudioServiceIntent);
    }
}

在onCreate,调用startForOnDestroyService,启动ForOnDestroyService。

在onDestroy,stopForOnDestroyService,终止ForOnDestroyService。

1.4 close_audio_device被阻塞,导致onDestroy无法退出

app进入后台,同时会阻塞播放声音的SDL_RunAudio线程,此时移除app,即onDestroy被回调,它会调用停止声音设备的close_audio_device,后者要等待SDL_RunAudio退出,可这线程正被阻塞!于是SDLMain出现死等。导致onDestroy长时间没响应。

什么导致SDL_RunAudio线程被阻塞?——线程会调用current_audio.impl.WaitDevice(device),在该函数得到可播放声音缓冲,要是没有就会阻塞。对Android,用openslES技术,WaitDevice对应openslES_WaitDevice,阻塞方法靠的是等待audiodata->playsem这个信号量有信号。

解决办法。在等待SDL_RunAudio退出前调用openslES_ResumeDevices(),向audiodata->playsem发信号。

static void close_audio_device(SDL_AudioDevice * device)
{
    ...
    if (device->thread != NULL) {
#if defined(__ANDROID__)
        extern void openslES_ResumeDevices(void);
        openslES_ResumeDevices();
#endif
        SDL_WaitThread(device->thread, NULL);
    }
    ...
}

二、SDLSurface(android原生渲染窗口)

在SDL,android渲染图像用的技术是opengles,Java层的SDLSurface提供了原生渲染窗口。为叙述方便,以下假设显示设备的分辨率是1920x1080。

  • Android_SurfaceWidth/Android_SurfaceHeight。C层的两个全局变量,值和设备当前朝向有关。surface指的是app在使用opengles能渲染到的图面。在全屏时,值等于显示设备的分辨率,即1920x1080(横屏);非全屏时,要扣除顶部状态条、底部导航条,值可能像是1920x1007。
  • Android_DeviceWidth/Android_DeviceHeight。C层的两个全局变量,值和设备当前朝向有关。指示显示设备的分辨率。
  • Android_Window。一个C层的、类型是“SDL_Window*”全局变量。对于基于Rose写的单窗口app,Android_Window指向那个唯一的窗口。
  • opengl两步操作。专指opengl/opengles中和操作系统有关两个特定步骤,得到原生窗口和从原生窗口得到egl_surface。在android,原生窗口的具体类型是ANativeWindow,Android_JNI_GetNativeWindow(void)是SDL封装了获取原生窗口的函数。内部先调用Java层的SDLSurface.getNativeSurface得到surface,再通过NDK提供的ANativeWindow_fromSurface由surface得到ANativeWindow。第二个步骤封装在SDL_EGL_CreateSurface。

SDLSurface得到一个有效图面后,android会回调它的surfaceChanged,它是SDLSurface中相对复杂成员函数。以下是会导致它被回调的几种场景。

  1. 第一次新建surface。调用它时,C层没有有效的SDL_Window。
  2. app由后台切换回前台。调用它时,C层存在有效的SDL_Window。
  3. 修改了设备朝向。调用它时,C层没有有效的SDL_Window。
  4. 全屏、非全屏间切换,即生了导致可显示区域发生变化的操作。调用它时,C层没有有效的SDL_Window。

surfaceChanged会依次执行四个操作。

2.1:SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, ...)

width、height是剔除了系统栏的surface尺寸,它们将被赋值给Android_SurfaceWidth、Android_SurfaceHeight。nDeviceWidth、nDeviceHeight是显示设备的分辨率,java是通过DisplayMetrics对象得到这两个值。

void Android_SetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, Uint32 format, float rate)
{
    Android_SurfaceWidth  = surfaceWidth;
    Android_SurfaceHeight = surfaceHeight;
    Android_DeviceWidth   = deviceWidth;
    Android_DeviceHeight  = deviceHeight;
    Android_ScreenFormat  = format;
    Android_ScreenRate    = (int)rate;
}

在C层的nativeSetScreenResolution,SDL只是执行变量赋值,赋值和Android_Window这个全局变量是否是nullptr无关。注:此时Android_Windows有可能不是nullptr,像导致它的第二种场景:app从后台切到前台。

2.2:SDLActivity.onNativeResize()

在C层的onNativeResize,如果Android_Window无效,什么也不做,否则执行Android_SendResize,参数window就是Android_Window。

void Android_SendResize(SDL_Window *window)
{
    /*
      Update the resolution of the desktop mode, so that the window
      can be properly resized. The screen resolution change can for
      example happen when the Activity enters or exits immersive mode,
      which can happen after VideoInit().
    */
    SDL_VideoDevice *device = SDL_GetVideoDevice();
    if (device && device->num_displays > 0)
    {
        SDL_VideoDisplay *display          = &device->displays[0];
        display->desktop_mode.format       = Android_ScreenFormat;
        display->desktop_mode.w            = Android_DeviceWidth;
        display->desktop_mode.h            = Android_DeviceHeight;
        display->desktop_mode.refresh_rate = Android_ScreenRate;
    }

    if (window) {
        /* Force the current mode to match the resize otherwise the SDL_WINDOWEVENT_RESTORED event
         * will fall back to the old mode */
        SDL_VideoDisplay *display              = SDL_GetDisplayForWindow(window);
        display->display_modes[0].format       = Android_ScreenFormat;
        display->display_modes[0].w            = Android_DeviceWidth;
        display->display_modes[0].h            = Android_DeviceHeight;
        display->display_modes[0].refresh_rate = Android_ScreenRate;
        display->current_mode                  = display->display_modes[0];

        SDL_SendWindowEvent(window, SDL_WINDOWEVENT_RESIZED, Android_SurfaceWidth, Android_SurfaceHeight);
    }
}

主要做两件事。一是设置存储在device->displays[0]的显示设备能力参数。二是如果Android_Window有效,发送SDL_WINDOWEVENT_RESIZED事件。

2.3:SDLActivity.onNativeSurfaceChanged()

JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceChanged)(JNIEnv *env, jclass jcls)
{
    if (Android_Window)
    {
        SDL_VideoDevice *_this = SDL_GetVideoDevice();
        SDL_WindowData  *data  = (SDL_WindowData *) Android_Window->driverdata;

        /* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
        if (data->egl_surface == EGL_NO_SURFACE) {
            data->egl_surface = SDL_EGL_CreateSurface(_this, (NativeWindowType) data->native_window);
        }

        /* GL Context handling is done in the event loop because this function is run from the Java thread */
    }
}

在C层的onNativeSurfaceChanged,如果Android_Window无效,什么也不做,否则执行opengl二步操作中的第二步,从native_window创建出egl_surface。对第一次调用,由于Android_Window无效,onNativeResize什么也不做。

要注意,Android_Widnows有效时,必须执行后面的SDL_EGL_CreateSurface。举个例子,kdesktop由后台切换到前台,之前切换到后台时,onNativeSurfaceDestroyed会销毁egl_surface,在此处必须重新让egl_surface有效。在app由前台切到后台、再由后台切回前台这一系列过程,会销毁、新建Java层SDLSurface中的图面,但不会销毁C中的SDL_Window。

2.4:SDLActivity.handleNativeState()

“一、activity生命周期”有介绍这函数,这里补说下,对这个函数,重要的第一次时行为,它会创建并运行SDLMain线程。

综上所述。1)在第一次时。surfaceChanged会修改C层的Android_SurfaceWidth、Android_SurfaceHeight、Android_DeviceWidth、Android_DeviceHeight,创建SDLThread线程,线程中运行app的main函数。在main函数,app调用SDL_CreateWindow,内中会执行Android_CreateWindow。2)在第二次及之后。surfaceChanged会修改C层的Android_SurfaceWidth、Android_SurfaceHeight、Android_DeviceWidth、Android_DeviceHeight,device->displays[0]的设备能力参数,如果Android_Windows有效的话,会设置egl_surface。

三、Android_CreateWindow(_THIS, SDL_Window * window)

app调用SDL_CreateWindow,SDL_CreateWindow调用Android_CreateWindow执行android这个操作系统的特定操作。调用这函数前,SDL_CreateWindow已经从内存中分配出SDL_Window(window指向这内存),但没有分配SDL_Window.driverdata。driverdata是操作系统私有数据,对android,要占多少字节只有Android_CreateWindow才知道。

Android_CreateWindow逻辑可分为四个步骤。

3.1:设置朝向

Android_JNI_SetOrientation(window->w, window->h, window->flags & SDL_WINDOW_RESIZABLE, SDL_GetHint(SDL_HINT_ORIENTATIONS));

如果app想自设置朝向,那就设置SDL_HINT_ORIENTATIONS。

基于个人看法,不适合在这里设置朝向。为什么?一旦设置朝向时发生了横、竖变化,会导致android回调SDLSurface.surfaceDestroyed、SDLSurface.surfaceChanged。意味此次创建的SDL_Window必将被销毁,后面还得重建。所以Rose专门增加了SDL_SetOrientation处理朝向,这里虽然还保留着Android_JNI_SetOrientation,已不会执行修改动作。

3.2:用Android_SurfaceWidth、Android_SurfaceHeight赋值给window->w、window->h

window->x = 0;
window->y = 0;
window->w = Android_SurfaceWidth;
window->h = Android_SurfaceHeight;

Android_CreateWindow会修正windows矩形四参数,那是不是说之前SDL_CreateWindow设置的4参数无用?不是。上面的Android_JNI_SetOrientation要用w、h。

3.3 从内存分配SDL_WindowData,并填充

SDL_WindowData* data = (SDL_WindowData *) SDL_calloc(1, sizeof(*data));
data->native_window = Android_JNI_GetNativeWindow();
data->egl_surface = SDL_EGL_CreateSurface(_this, (NativeWindowType) data->native_window);
window->driverdata = data;

这个data是全新的,包括从内存中分配,然后执行opengl两步操作,即Android_JNI_GetNativeWindow和SDL_EGL_CreateSurface。

3.4:赋值Android_Window

Android_Window = window;

整系统只在这一处给Android_Window赋有效值。

四、app在后台非阻塞运行

<SDL2>/src/video/android/SDL_androidvideo.c
static SDL_VideoDevice* Android_CreateDevice(int devindex)
{
    block_on_pause = SDL_GetHintBoolean(SDL_HINT_ANDROID_BLOCK_ON_PAUSE, SDL_TRUE);
    if (block_on_pause) {
        device->PumpEvents = Android_PumpEvents_Blocking;
    } else {
        device->PumpEvents = Android_PumpEvents_NonBlocking;
    }
}

<librose>/video.cpp
SDL_SetHint(SDL_HINT_ANDROID_BLOCK_ON_PAUSE, "false");

一旦app切到后台,SDL默认是阻塞SDLMain线程,但也提供了非阻塞方案,即让PumpEvents运行在Android_PumpEvents_NonBlocking。对Rose app,会把SDL_HINT_ANDROID_BLOCK_ON_PAUSE设为false,强制使用非阻塞。

一旦使用非阻塞,自然希望app在后台时能尽可能少占cpu,为此一个重要手段是不去渲染窗口。实际使用时,切到后台会销毁SDLSurface中图面,在已销毁情况下操作opengles,也会导致app崩溃(疑问:即使已销毁图面,SDL2-2.0.8好像操作opengles不会造成问题)。如何保证app在后台时不操作opengles,一种在app控制,一种在SDL控制。个人认为光靠app是不现实的,主要还是靠SDL,为此需要对SDL进行修改。

<SDL2>/src/render/SDL_render.c
static SDL_bool IsEmergencyPause(SDL_Renderer* renderer)
{
    return renderer->window == NULL || renderer->window->is_emergency_pause || renderer->app_pause;
}

int SDL_RenderCopyF(SDL_Renderer * renderer, SDL_Texture * texture, const SDL_Rect * srcrect, const SDL_FRect * dstrect)
{
    if (IsEmergencyPause(renderer) || texture == NULL) return 0;
    ...
}

static int FlushRenderCommands(SDL_Renderer *renderer)
{
    DebugLogRenderCommands(renderer->render_commands);
    if (IsEmergencyPause(renderer)) {
        retval = 0;
    } else {
        retval = renderer->RunCommandQueue(renderer, renderer->render_commands, renderer->vertex_data, renderer->vertex_data_used);
    }
    ...
}

在SDL方面的修改可分为四类。

  1. 增加IsEmergencyPause。和opengle相关的api先执行这个函数,一旦它返回true,认为当前不适合执行,直接返回。另外,即使已经把renderer->batching设为false,仍会有opengl操作在IsEmergencyPause=false时放入队列,IsEmergencyPause是true执行。所以FlushRenderCommands在执行RunCommandQueue前仍需进行IsEmergencyPause检查。
  2. SDL_DestroyTexture。要是在后台时执行SDL_DestroyTexture,按第一条规则立即返回,将不会释放此SDL_Texture关联的gpu内存。为此做法是在SDL_DestroyTexture,遇到IsEmergencyPause是true时,把要释放的SDL_Texture存放在一个叫emergency_destroyed_textures的链表,等app恢复到前台,执行第一个SDL_DestroyTexture时,把这锭表中的节点一次性释放。
  3. 设置is_emergency_pause。is_emergency_pause是个新增字段,为什么不用已有的表示正处于后台的变量?——要尽快阻住opengl操作。按SDL逻辑,发生onPause时,是先向app发送SDL_APP_WILLENTERBACKGROUND、SDL_APP_DIDENTERBACKGROUND,等到app处理这两个消息,中间是有时差,要是在这段时间刚好来了opengl操作,那会导致app崩溃。于是使用新变量让尽可能快阻止,以及尽可能晚地放开。SDL在nativePause时就把is_emergency_pause置为true,在Android_PumpEvents_NonBlocking、确认已切到前台后(SDL准备好了新opengl surface)才把is_emergency_pause置为false。
  4. is_emergency_pause改回false,表示SDL准备好了新opengl surface。考虑到app在处理SDL_APP_DIDENTERFOREGROUND时,往往会执行gui操作,为此要等到is_emergency_pause改回false后再发SDL_APP_DIDENTERFOREGROUND。

一直到现在都没说IsEmergencyPause要用到的app_pause,顾名思义,这个变量是app控制。初始值是false,当app希望后绪阻住opengl操作,把它设为true,想放开了就回到false。要控制这变量一个场景是SDL_SetRenderTarget。

SDL_SetRenderTarget大多数场景是成对使用。举个例子,通常情况下,target是frameTex,时刻A要换成tex1,相关操作结束后就要恢复到frameTex。一旦中间出现app切到后台,或切到前台,下面具体列出不完整时场景。

  1. 第一次set到tex1,正常执行。中间发生切到后台,没法执行第二次set回frameTex。
  2. 第一次set时处在后台,即没有执行切换到tex1。中间发生切到前台,导致第二次set回frameTex可执行。
  3. 不论以上的第一点还是第二点,两次set之间会执行opengl其它操作,像SDL_RenderCopy,这些操作应该和两次set一样,或全执行,或全不执行。

针对场景一。在恢复到前台时,检查target是不是常态下应该的frameTex,不是的话,强制设置到frameTex。

int SDL_RenderOnResume(SDL_Window* window)
{
    SDL_Renderer* renderer = SDL_GetRenderer(window);
    SDL_Texture* target = SDL_GetRenderTarget(renderer);
    // 常态下,renderer->frame是app的target。
    if (renderer->frame != NULL && target != renderer->frame) {
        SDL_SetRenderTarget(renderer, renderer->frame);
    }
    return 0;
}

针对场景二。把两次set和中间的opengl操作视做一个事务,或全执行,或全不执行。

class trender_target_lock
{
public:
  trender_target_lock(SDL_Renderer* renderer, const texture& target)
    : renderer_(renderer)
    , original_(SDL_GetRenderTarget(renderer))
    , render_pause_(false)
  {
    // if desire to back-fb, tex is NULL.
    SDL_Texture* tex = target.get();
    int result = SDL_SetRenderTarget(renderer, tex);
    if (result != 0) { // 如果app处在后台,SDL_SetRenderTarget返回值不能是0。
      render_pause_ = true;
      // 此次SDL_SetRenderTarget无法执行,设置app_pause是true,阻住后绪的opengl操作以及第二次set
      SDL_SetRenderAppPause(renderer, SDL_TRUE);
    }
  }
  ~trender_target_lock()
  {
    if (!render_pause_) {
      SDL_SetRenderTarget(renderer_, original_);
    } else {
      // 第一次set失败,设置了app_pause是false,此处恢复app_pause。
      SDL_SetRenderAppPause(renderer_, SDL_FALSE);
    }
  }
private:
  SDL_Renderer* renderer_;
  SDL_Texture* original_;
  bool render_pause_;
};

app处在后台运行时,创建SDL_Texture会失败,为此app编程要注意。

  • SDL_CreateTexture可能会失败,调用它的SDL_CreateTextureFromSurface、create_neutral_texture[Rose]、clone_texture[Rose]也可能失败。调用它们时,如果能保证app不会在后台,只要参数正确,可认为一定成功。
  • 对要一定需要成功创建出的SDL_Texture,可放在tdialog::pre_show。目前,对pre_show基本会要求在非后台时执行。
  • SDL_LockTexture属于render系列函数,但它没有涉及到opengles上下文,因而即使在后台执行也会成功。
  • SDL_QueryTexture、SDL_SetTextureBlendMode。理论上应该修改,但为了编程诊断,没改。
  • SDL_RenderCopyF、SDL_RenderCopyF。不仅IsEmergencyPause直接退出,texture==nullptr时也认为没问题。放宽texture==nullptr,原因是从后台转到前台的瞬间,有可能会存在后台时创建的SDL_Texture,它们肯定是nullptr。要都是判断这些SDL_Texture的有效性,app工作太累了。

五、SDL_SetOrientation、SDL_SetFullscreen

它们都是新增api,一旦执行,都有可能触发android回调SDLSurface.surfaceDestroyed、SDLSurface.surfaceChanged,进而强制app调用SDL_CreateWindow。

5.1 SDL_SetOrientation

功能是修改朝向。

void Android_SetOrientation(_THIS, SDL_bool landscape)
{
    if (Android_SurfaceWidth == 0 || Android_SurfaceHeight == 0) {
        return;
    }
    if (Android_SurfaceWidth == Android_SurfaceHeight) {
        return;
    }
    if (landscape) {
        SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeRight,LandscapeLeft");
        if (Android_SurfaceWidth > Android_SurfaceHeight) {
            return;
        }

    } else {
        SDL_SetHint(SDL_HINT_ORIENTATIONS, "Portrait");
        if (Android_SurfaceWidth < Android_SurfaceHeight) {
            return;
        }
    }

    // require rotate, flip width and height.
    int width = Android_SurfaceHeight;
    int height = Android_SurfaceWidth;

    waiting_SDL_WINDOWEVENT_RESIZED = SDL_TRUE;
    Android_JNI_SetOrientation(width, height, SDL_WINDOW_RESIZABLE, SDL_GetHint(SDL_HINT_ORIENTATIONS));

    while (waiting_SDL_WINDOWEVENT_RESIZED) {
        SDL_Delay(10);
    }
}

无论要不要修改朝向,都会设置环境变量SDL_HINT_ORIENTATIONS。

Android_JNI_SetOrientation修改朝向后,android会依次回调SDLSurface.surfaceDestroyed、SDLSurface.surfaceChanged,此函数退出前要确保同步执行完这些操作。方法是用waiting_SDL_WINDOWEVENT_RESIZED,它在执行修改朝向前设置为SDL_TRUE,修改朝向后,主线程就死等这变量,直到变为SDL_FALSE。由这逻辑可猜到,在SDLSurface线程,执行完SDLSurface.surfaceChanged的恰当时刻应该把waiting_SDL_WINDOWEVENT_RESIZED置为SDL_FALSE。

5.2 SDL_SetFullscreen

功能是全屏、非全屏间切换。

在C层,SDL已提供了全屏、非全屏间切换函数Android_JNI_SetWindowStyle。封装它的是Android_SetWindowFullscreen。那何时调用Android_SetWindowFullscreen?——SDL_UpdateFullscreenMode。调用后者的一个主要场景是在SDL_CreateWindow。一旦在SDL_CreateWindow调用,就会遇到和设置朝向一样问题,“会导致android回调SDLSurface.surfaceDestroyed、SDLSurface.surfaceChanged。意味此次创建的SDL_Window必将被销毁,后面还得重建”。

为确保SDL_UpdateFullscreenMode不会发生全屏、非全屏间切换,Rose在调用SDL_CreateWindow时,不会让flags拥有SDL_WINDOW_FULLSCREEN标记

和SDL_SetOrientation一样,此函数退出前要确保同步执行完SDLSurface.surfaceDestroyed、SDLSurface.surfaceChanged,于是也用了一样的waiting_SDL_WINDOWEVENT_RESIZED逻辑。

不论在哪种操作系统,SDL_VideoDisplay.display_modes中索引0总是最大分辩率,它将做为全屏时尺寸。不同于android、ios,ms windows操作系统切换全屏和非全屏要涉及到修改显示模式。

 

六、SDL_TEXTINPUT

<SDL2>/src/events/SDL_keyboard.c
------
int SDL_SendKeyboardText(const char *text)
{
    ...
    /* Post the event, if desired */
    posted = 0;
    // if (SDL_GetEventState(SDL_TEXTINPUT) == SDL_ENABLE) {
        SDL_Event event;
        event.text.type = SDL_TEXTINPUT;
        event.text.windowID = keyboard->focus ? keyboard->focus->id : 0;
        SDL_utf8strlcpy(event.text.text, text, SDL_arraysize(event.text.text));
        posted = (SDL_PushEvent(&event) > 0);
    // }
    return (posted);
}

修改方法;注释掉“if (SDL_GetEventState(SDL_TEXTINPUT) == SDL_ENABLE)”,让总是允许发送SDL_TEXTINPUT。

这么改是希望上层统一用SDL_TEXTINPUT接收字符。app可能会遇到这么种情况,外接一个做为键盘输入的刷二维码设备,这时也要允许接收SDL_TEXTINPUT。为此就须要修改SDL的一条字符处理原则。

  • 对会弹出软键盘系统,像android、iOS,在弹出软键盘时使能接收SDL_TEXTINPUT,关闭软键盘后就禁止按收SDL_TEXTINPUT。

 

七、ios下的SDL实现(SDL2-2.0.12)

7.1、SDL_SetOrientation、SDL_SetFullscreen

它们都是新增api,但在ios下,它们都是啥也不做。

SDL_SetOrientation。ios会根据SDL_CreateWindow传下的w、h,自动切换模屏、竖屏。

SDL_SetFullscreen。ios切换全屏、非全屏使用SDL内置的SDL_WINDOW_FULLSCREEN标记。换句话说,要全屏了,设上SDL_WINDOW_FULLSCREEN,非全屏不让出现SDL_WINDOW_FULLSCREEN。注:在ios,SDL_WINDOW_BORDERLESS和SDL_WINDOW_FULLSCREEN有着一样作用,rose只使用SDL_WINDOW_FULLSCREEN。

在ios,全屏和非全屏时,surfaceSize是一样的。

7.2 ios16时,横、竖屏旋转失败

ios16应该改了旋转要求,对解决办法,不少文章使用函数requestGeometryUpdateWithPreferences,像“iOS16适配-屏幕旋转”。但这里没用它,在SDL_SendWindowEvent,修改处理SDL_WINDOWEVENT_RESIZED时逻辑。

<SDL>/SDL2/src/video/uikit/SDL_uikitviewcontroller.m
------
int
SDL_SendWindowEvent(SDL_Window * window, Uint8 windowevent, int data1,
                    int data2)
{
    ...
    switch (windowevent) {
    ...
    case SDL_WINDOWEVENT_RESIZED:
        if (!(window->flags & SDL_WINDOW_FULLSCREEN)) {
            window->windowed.w = data1;
            window->windowed.h = data2;
        }
        if (data1 == window->w && data2 == window->h) {
            return 0;
        }

#if defined(__APPLE__) && TARGET_OS_IPHONE
        // ios,用以下代码
        if (window->w < window->h){
            window->w = data1;
            window->h = data2;
        } else {
            window->w = data2;
            window->h = data1;
        }
#else
        // 官方代码
        window->w = data1;
        window->h = data2;
#endif
        SDL_OnWindowResized(window);
        break;
    ...
}

这是临时方法,最终解决要靠升级到最新版SDL。

 

八、(android)让app进入后台,可播放声音

在android,用的AudioBootStrap是openslES_bootstrap(name: openslES),不是直观认为的ANDROIDAUDIO_bootstrap(name: android)。当然SDL_audio.c中的bootstrap会写着两个都支持,但openslES_bootstrap放在前面,实际只用它。

在android,一旦app被切到后台,那该app就不能播放声音。

<SDL>/SDL2-2.0.12/src/audio/openslES/SDL_openslES.c
------
void openslES_ResumeDevices(void)
{
    if (bg_audio_output) {
        SDL_Log("openslES_ResumeDevices, bg_audio_output is true, do nothing");
        return;
    }
    ...
}

void openslES_PauseDevices(void)
{
    if (bg_audio_output) {
        SDL_Log("openslES_PauseDevices, bg_audio_output is true, do nothing");
        return;
    }
    ...
}

bg_audio_output是个增加的变量,指示app进入后台,是否可以播放声音。openslES_ResumeDevices、openslES_PauseDevices只影响播放,不影响录音。

对后台录音,sdl的openslES没做限制。但在andorid,要是不改源码,android会让app在1分钟后暂停录音,切到前台则自动恢复。

全部评论: 0

    写评论: