PSP Emulator Inside Part-1

We will see the program flow of PPSSPP a PSP Emulator. PPSSPP using C++ programming language which starting or entry point from int main(argc, argv). On OSX port the PPSSPP using SDL+OPENGL and starting from SDL/SDLMain.cpp:

MainThread:

  1. glslang::InitializeProcess() [ext/glsllang/*/MachineIndependent/ShaderLang.cpp]
  2. argc, argv parsing
  3. NativeGetAppInfo: set app name, version variable [UI/NativeApp.cpp]
  4. SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_AUDIO)
  5. SDL_GetCurrentDisplayMode
  6. SDL_GL_SetAttribute
  7. calculate screen size
  8. NativeInit [UI/NativeApp.cpp]
    1. ResetUIState(): globalUIState = UISTATE_MENU [Core/System.cpp]
    2. VFSRegister: set assets dir [ext/native/file/zip_read.cpp]
    3. set g_Config data directories and search paths.
    4. LogManager::Init [Common/LogManager.cpp]
    5. Config::Load [Core/Config.cpp]
    6. argc, argv parsing for game ROM.
    7. PostLoadConfig:
      1. i18nrepo.LoadIni: load language [ext/native/i18n/i18n.cpp]
    8. i18nrepo.GetCategory [ext/native/i18n/i18n.cpp]
    9. (global)screenManager = new ScreenManager() [ext/native/ui/screen.cpp]
    10. new LogoScreen [UI/MiscScreen.h]
    11. ScreenManager::switchScreen to LogoScreen Screen.
    12. StartWebServer for remote debug [Core/WebServer.cpp]
    13. CheckFailedGPUBackends:
      1. g_Config.iGPUBackend = GPUBackend::OPENGL
    14. SetGPUBackend(g_Config.iGPUBackend) [Core/System.cpp]:
      1. gpuBackend = GPUBackend::OPENGL
  9. new SDLGLGraphicsContext
  10. SDLGLGraphicsContext::Init [SDL/SDLGLGraphicsContext.cpp]:
    1. SetGLCoreContext [ext/native/gfx_es2/gpu_features.cpp]
    2. SDL_CreateWindow
    3. SDL_GL_CreateContext
    4. SDL_ShowWindow
    5. CheckGLExtensions [ext/native/gfx_es2/gpu_features.cpp]
    6. Draw::T3DCreateGLContext [ext/native/thin3d/thin3d_gl.cpp]:
      1. new Draw::OpenGLContext
        1. GLRenderManager::CreatePushBuffer [ext/native/thin3d/GLRenderManager.h]
    7. get GLRenderManager from Draw::OpenGLContext::GetNativeObject(NativeObject::RENDER_MANAGER)
    8. SetGPUBackend(GPUBackend::OPENGL) again
    9. Draw::DrawContext::CreatePresets [ext/native/thin3d/thin3d.cpp]: create fs & vs shader:
      1. Draw::DrawContext::CreateShader:
        1. Draw::DrawContext::GetSupportedShaderLanguages
        2. Draw::DrawContext::CreateShaderModule:
          1. new Draw::OpenGLShaderModule
          2. Draw::OpenGLShaderModule::Compile
    10. GLRenderManager::SetSwapFunction = [&]() { SDL_GL_SwapWindow }
  11. useEmuThread for OPENGL
  12. SDL_SetWindowTitle
  13. SDLGLGraphicsContext::InitFromRenderThread
  14. SDL_GL_SetSwapInterval(1)
  15. InitSDLAudioDevice():
    1. SDL_OpenAudioDevice
    2. SDL_PauseAudioDevice
  16. new SDLJoystick [SDL/SDLJoystick.cpp]:
    1. SDL_RWFromConstMem
    2. SDL_GameControllerAddMappingsFromRW
    3. setUpControllers:
      1. SDL_NumJoysticks
      2. setUpController:
        1. SDL_IsGameController
        2. SDL_JoystickOpen
        3. SDL_JoystickGetGUID
        4. SDL_JoystickGetGUIDString
        5. SDL_JoystickName
        6. SDL_GameControllerAddMapping
        7. SDL_JoystickClose
        8. SDL_GameControllerOpen
        9. SDL_GameControllerGetAttached
        10. SDL_GameControllerGetJoystick
        11. SDL_JoystickInstanceID
        12. SDL_GameControllerName
        13. SDL_GameControllerMapping
  17. EmuThreadStart:
    1. emuThreadState = EmuThreadState::START_REQUESTED;
    2. run [thread] EmuThreadFunc:
      1. setCurrentThreadName(“Emu”) [ext/native/thread/threadutil.cpp]
      2. emuThreadState = EmuThreadState::RUNNING
      3. NativeInitGraphics [UI/NativeApp.cpp]:
        1. Core_SetGraphicsContext [Core/Core.cpp]:
          1. PSP_CoreParameter use SDLGLGraphicsContext
        2. DrawBuffer::SetAtlas
        3. UIThemeInit
        4. new UIContext
        5. DrawBuffer::CreateInputLayout
          1. Draw::DrawContext::CreateInputLayout(Draw::InputLayoutDesc)
        6. Draw::DrawContext::CreateBlendState:
          1. new Draw::OpenGLBlendState
        7. Draw::DrawContext::CreateDepthStencilState
          1. new Draw::OpenGLDepthStencilState
        8. Draw::DrawContext::CreateRasterState:
          1. new Draw::OpenGLRasterState
        9. Draw::DrawContext::CreateGraphicsPipeline(Draw::PipelineDesc) with Draw::DrawContext::GetVshaderPreset and Draw::DrawContext::GetFshaderPreset
        10. DrawBuffer::Init:
          1. Draw::Pipeline::RequiresBuffer
          2. Draw::DrawContext::CreateBuffer
        11. UIContext::Init
          1. Draw::DrawContext::CreateSamplerState
          2. TextDrawer::Create
        12. ScreenManager::setPostRenderCallback(RenderOverlays)
        13. ScreenManager::deviceRestored
          1. all Screen::deviceRestored
        14. new GameInfoCache [UI/GameInfoCache.cpp]:
          1. GameInfoCache::Init:
            1. new PrioritizedWorkQueue [ext/native/thread/prioritizedworkqueue.h]
            2. ProcessWorkQueueOnThreadWhile:
              1. run [thread] threadfunc:
                1. setCurrentThreadName(“PrioQueue”)
                2. continue in parallel… [2]
        15. GPUInterface::DeviceRestore
      4. continue in parallel … [1]
  18. SDLGLGraphicsContext::ThreadStart: [SDL/SDLGLGraphicsContext.h]:
    1. GLRenderManager::ThreadStart [ext/native/thin3d/GLRenderManager.h]
      1. GLQueueRunner::CreateDeviceObjects [ext/native/thin3d/GLQueueRunner.cpp]:
        1. glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT)
        2. glGenVertexArrays
      2. set GLBufferStrategy
  19. reset framecount
  20. Entering MainLoop, main thread as Render thread:
    1. case SDL_PollEvent when:
      1. SDL_QUIT
        1. set QuitRequested
      2. SDL_WINDOWEVENT
        1. Core_NotifyWindowHidden [Core/Core.cpp]
        2. UpdateScreenScale [Core/Core.cpp]
        3. NativeMessageReceived(“gpu_resized”, “”)
        4. SDL_ShowCursor
      3. SDL_KEYDOWN | SDL_KEYUP | SDL_TEXTINPUT
        1. NativeKey [UI/NativeApp.cpp]
          1. ScreenManager::key
            1. (top)Screen::key
      4. SDL_MOUSEBUTTONDOWN | SDL_MOUSEWHEEL | SDL_MOUSEMOTION | SDL_MOUSEBUTTONUP
        1. NativeTouch [UI/NativeApp.cpp]
          1. ScreenManager::touch
            1. (top)Screen::touch
        2. NativeKey
      5. SDLJoystick::ProcessInput [SDL/SDLJoystick.cpp]
        1. SDL_CONTROLLERBUTTONDOWN | SDL_CONTROLLERBUTTONUP
          1. NativeKey
        2. SDL_CONTROLLERAXISMOTION
          1. NativeAxis [UI/NativeApp.cpp]
            1. ScreenManager::axis
              1. (top)Screen::axis
        3. SDL_CONTROLLERDEVICEREMOVED
        4. SDL_CONTROLLERDEVICEADDED
      6. SDL_AUDIODEVICEADDED | SDL_AUDIODEVICEREMOVED
        1. SDL_GetAudioDeviceName
        2. StopSDLAudioDevice
        3. InitSDLAudioDevice
    2. SDL_GetKeyboardState
    3. break if QuitRequested
    4. if EmuThreadState::DISABLED:
      1. UpdateRunLoop [Core/Core.cpp]
    5. [EmuThread] loop while not EmuThreadState::QUIT_REQUESTED [1]:
      1. UpdateRunLoop [Core/Core.cpp]
    6. if not EmuThreadState::DISABLED or not paused:
      1. SDGLGraphicsContext::ThreadFrame [SDL/SDLGLGraphicsContext.h]:
        1. GLRenderManager::ThreadFrame [ext/native/thin3d/GLRenderManager.h]:
          1. increment threadFrame index
          2. get FrameData by threadFrame index
          3. wait FrameData to readyForRun
          4. GLDeleter::Perform prev [ext/native/thin3d/GLRenderManager.cpp]
          5. GLDeleter::Take prev [ext/native/thin3d/GLRenderManager.h]
          6. GLRenderManager::Run(threadFrame index) [ext/native/thin3d/GLRenderManager.cpp]:
            1. BeginSubmitFrame
            2. GLQueueRunner::RunInitSteps (FrameData.stes) [ext/native/thin3d/GLQueueRunner.cpp]
            3. GLQueueRunner::RunSteps (FrameData.initSteps) [ext/native/thin3d/GLQueueRunner.cpp]
            4. EndSubmitFrame or EndSyncFrame
          7. do next FrameData within MAX_INFLIGHT_FRAMES if GLRRunType::END
    7. SDGLGraphicsContext::SwapBuffers
    8. ToggleFullScreenIfFlagSet:
      1. SDL_GetWindowFlags
      2. SDL_SetWindowFullscreen
    9. if not UISTATE_INGAME or not PSP_IsInited or paused:
      1. sleep_ms: simple throttling
    10. increase framecount
  21. EmuThreadStop
    1. emuThreadState = EmuThreadState::QUIT_REQUESTED
  22. eat all SDLGLGraphicsContext::ThreadFrame
  23. EmuThreadJoin
  24. [EmuThread] emuThreadState = EmuThreadState::STOPPED
  25. [EmuThread] NativeShutdownGraphics [UI/NativeApp.cpp]
  26. [EmuThread] SDLGLGraphicsContext::StopThread
    1. GLRenderManager::WaitUntilQueueIdle
      1. wait all FrameData to ready
    2. GLRenderManager::StopThread
      1. GLRenderManager::Wipe
  27. ~SDLJoystick
  28. SDLGLGraphicsContext::ThreadEnd
    1. GLRenderManager::ThreadEnd
      1. GLQueueRunner::DestroyDeviceObjects
      2. GLDeleter::Perform
  29. NativeShutdown
  30. SDLGLGraphicsContext::ShutdownFromRenderThread
    1. ~Draw::DrawContext
    2. SDL_GL_DeleteContext
  31. ~SDLGLGraphicsContext
  32. SDL_PauseAudioDevice
  33. SDL_CloseAudioDevice
  34. SDL_Quit
  35. glslang::FinalizeProcess

PrioritizedWorkerQueue [2]:

  1. PrioritizedWorkQueue::Pop()
  2. PrioritizedWorkQueueItem::run
  3. loop unless PrioritizedWorkQueueItem::Done

UpdateRunLoop

Running in: MainThread otherwise EmuThread depends on emuThreadState, on UI/NativeApp.cpp:

  1. NativeUpdate
    1. toProcess = pendingMessages
    2. for each toProcess:
      1. HandleGlobalMessage
        1. inputDeviceConnected
        2. inputbox_completed
        3. bgImage_updated
        4. savestate_displayslot
        5. gpu_resized | gpu_clearCache
        6. core_powerSaving
        7. permission_granted:storage
      2. ScreenManager::sendMessage
        1. recreateviews
          1. RecreateAllViews
        2. lost_focus
        3. (top)Screen::sendMessage
    3. http::Downloader::Update
    4. ScreenManager::update
      1. ScreenManager::switchToNext() if needed
      2. (top)Screen::update
  2. NativeRender [UI/NativeApp.cpp]
    1. GameManager::update [Core/Util/GameManager.cpp]
    2. UpdateBackgroundAudio if not UISTATE_INGAME
    3. DrawBuffer::PushDrawMatrix(Lin::Matrix4x4::setOrtho)
    4. ScreenManager::render [ext/native/ui/screen.cpp]:
      1. (top)Screen::preRender
      2. (top)Screen::render
      3. (overlay)Screen::render
      4. PostRenderCallback
      5. (top)Screen::postRender
      6. ScreenManager::processFinishDialog
    5. SDLGLGRaphicsContex::Resize
    6. ScreenManager::resized
      1. each Screen::resized
    7. DrawBuffer::PopDrawMatrix

SCREENMANAGER

ScreenManager hold current (top)Screen to be shown(render) and which screen receive input. Here are the Screens which may appear in order:

  1. LogoScreen
    1. after few seconds it will do ScreenManager::switchScreen to MainScreen Screen
  2. MainScreen
    1. when user click one of the game it will trigger MainScreen::OnGameSelectedInstant to LaunchFile to tell ScreenManager::switchScreen to EmuScreen Screen
  3. EmuScreen
    1. on each Screen::update call, several stuff being process based on state from loading the game, booting up, and on each Screen::render to running/emulate the game.

Compiling Dynamorio VS2019

Here is just a quick tips too start create compatibility to use VS2019.

References:

I try to install VS2017 on my new installed VM, but currently there is only VS2019 installer so I trying a combination a bit:

VS2019 with VS2017 (v141 | v14.16):

  1. Start command line to use cl.exe from v14.16 not VS 2019 (v142 | v14.24)
%comspec% /k "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars32.bat" -vcvars_ver=14.16

See xx.16:

cl.exe
Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27034 for x86
ml.exe
Microsoft (R) Macro Assembler Version 14.16.27034.0
  1. Run cmake to use v141 toolset

I cannot change the Generator so it always use “Visual Studio 16 2019”.

cmake -T v141,host=x86,version=14.16 -A Win32 ..
  1. Build as wiki said:
cmake --build . --config RelWithDebInfo

VS2019

  1. Start command line as “Developer Command Prompt for VS 2019”, or:
%comspec% /k "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\VsDevCmd.bat"
  1. Run cmake with Win32 (within build dir):
cmake -A Win32 ..
  1. Buils as wiki said:
cmake --build . --config RelWithDebInfo

Install

This instruction will install a copy to export dir.

cmake --build . --config RelWithDebInfo --target install

Running

Goto export dir and try:

bin32\drrun.exe -c samples\bin32\bbcount.dll -- notepad.exe

TODO

The error above if we enforce -A x64 (by default), so need to update cpp2asm_support.cmake

Crash Course Chef Infra

My experience is using Ansible, then my current company using Chef, by default I should learn first to use. They both do same thing to provisioning the VM, install software what I our apps need on the VM.

Confusion at first because too many terminology:

  • Chef,
  • Chef Infra,
  • Chef Solo,
  • Cookbook,
  • Recipe,
  • Chef Zero,
  • Chef Server,
  • Chef Kitchen,
  • Knife,
  • Chef Client,
  • Chef DK,
  • Chef Workstation,
  • Berkshelf,
  • Kitchen,
  • Supermarket, etc.

My goals is to be able to compare current JRuby deployment with Ruby MRI deployment on my team. If the benchmark result good then swap the engine. My problem with JRuby is:

  • Not energy saving,
  • TDD become slow.

The existing cookbook is tightly coupled with JRuby and don’t know why prefer using JRuby instead MRI. Is that because:

  • Java thread pool,
  • Java monitoring tool?

Tbh, I still don’t get it, no data or metric, and how meaningful that feature for our us.

The question, How is possibilty? Where to start? Actually my team is focus on Product, not on Infra or Devops. And Devops was handled by special team. No one has experience here, just reach out Devops team, they may too buzy. May be I could digging on company documentation, NOT FOUND! but wait found one two docs and it is actually here on my team wiki :‑P

  • How creating new services and setup the VM, and I know there is a command like this:
    $ sudo EDITOR=vi knife node edit $(hostname) -c /etc/chef/client.rb then run
    sudo chef-client.
    These commands let me knows my goals is Chef Infra (client and server).
  • Chef tutorial with chef zero, playing with chef on your laptop. But what I need is Chef Infra not Chef Zero, but it’s okay to do this effort.

From that docs there is lot of things I learn:

  • There is knife command to edit/see node config with knife node edit .  First I think this command run on client but then I got it wrong.
  • I know a recipe is a things that VM do to install software, and it resides in a cookbook. You could see run_list consists of list with format [COOKBOOK::RECIPE].
  • Argument -c /etc/chef/client.rb makes me curious and gotcha! the server is here.
  • And chef-client will execute the run_list. It run on client, should be. But it doesn’t need to explicitly client.rb. Why?

The cookbook name gives me idea to search on my company source code repo about the recipe contents. So each cookbook will have its repo. You could inspect the repo metadata.rb and see the name. But you may not go that far since every cookbooks that client need alread cached on /var/chef/cache/cookbooks/ folder. Reading through the source still don’t get me an idea what is the flow.

Where to start understanding the cookbook? Should I buy a book? I prefer gooling and expect someone out there share it or google book coulde give a peek. Found book Learning Chef  (2015) by Mischa Taylor, cs. (Is he a family of  Taylor Otwell :‑/ ).

I need a development kit, ChefDK (Development Kit) was mentioned on that book. But now Chef suggest to use Chef Workstation with ChefDK built in. I checked the compatibility of chef version:

  • my company VM setup is Chef 12.19
  • the book using Chef 11, and
  • Chef Workstation is Chef 15

Wow major version changes, but show must go on. Stick with google when found error message during the journey.

Install Vagrant and VirtualBox. Its a plus to be familiar with the command vagrant and VBoxManage, even though you only need kitchen.

From the Learning Chef (2015) Book:

  • Chapter 1-3, I skipped because I don’t need learn Ruby again. Beside the installation steps is different now. I following on the chef site for installation.
  • Chapter 4-10, Just quick read and DO that exercise, it will tell you more concept about chef.
    • Everything you do to execute the run_list is Test Kitchen. Create/Destroy/Login a VM. Execute your recipe with Converge.
    • A node variable on the recipe is predefined with current client info and state.
    • Ohai is always run each converge to populate your current client/node info.
    • Cookbook structure, and Flow of cookbook, and Attribute default values and how to override.
    • You could call Chef Solo and Chef Zero actually the local/offline version of Chef Infra.
    • Supermarket to store community shared cookbook thay may be useful.
  • Chapter 11-12, What will you actually on each cookbook:
    • Doing development always with Chef Zero.And introduce a cli and predefined function to do search a node by specific attribute to not hard code other dependant services on cookbook.

The rest of books is not used by me. Environment is not stored on the cookbook, and the best practice is to use shared then K/V server such us Consul or Etcd. Alternatively chef server have data bag that could be share among clients.

Back to cookbook that is used when deploy my ruby app, let say it rails-cookbook (1). it depends on ruby-cookbook (2). Again depends on ruby-setup-cookbook (3). The recipe in (1) call same recipe in (2), what is this? This is called cookbook wrapper that is seems common on chef. The wrapper usually created to pre or post-configure such as setup node attributes before doing the same thing. The (2) use resources that is defined in (3). The (3) only contains custom resources and the action. From this point my source of knowledge is search from chef docs, read other cookbooks, and googling.

Ruby is a pain with different versions. Chef has it own version too and in its embedded/bin folder. Your system may also have too. Thanks to rbenv we could change version per project basis and using rbenv-chef-workstation plugin is recommended.

Since we develop our apps using rbenv, the cookbook also rbenv, so I choose the VM also using rbenv. The existing rbenv-cookbook (4) seems outdated and overkill, so I need create my own version. But reading other cookbooks give us insight and learn something such us:

  • libraries: store helper function that could be call directly from recipes/resources. I am not interested to do OOP here.
  • resources: define repetitive or specific action. Since multiple recipe could be only called once per-chef-client running.

Then I create new cookbook ultimate-ruby-cookbook (5) for several reasons:

  • To not bothering with existing (1)(2)(3) cookbooks above.
  • Specific OS for only Ubuntu 16.04 to remove too many if-es on (1)(2)(3). And easy to read cookbook.
  • My own version of (4).
  • Better Test Kitchen experience, lock the chef to be same with company VM with require_chef_omnibus: 12.19.36 on kitchen.yml
  • I’ve also create Mock server with Vagrant on different repo for the kitchen converge to run smoothly. The cookbook depends on package installation from company repo server and reading k/v from consul, etc.
  • Mock Server will serve nginx to proxying aptly publish and consul-ui. Publish the package and import the k/v to consul that will be needed during converge.
  • For example, My Test Kitchen VM will have IP 192.168.33.36, and My Mock Server IP 192.168.33.10,  and my Laptop will be known as IP 192.168.33.1.
  • Create a test-cookbook in test/cookbooks as a wrapper to setup these attributes.

What is need to improvement:

  • The first converge will take too much time since compiling ruby by ruby-build. And the role of thumb is to precompile and store it to aptly repo for fast deployment and for security reason.
  • Not understanding Compile-Time vs Run-Time leads improper updated attributes to be used on recipe. On my case I create a recipe with order: install-ruby (A) then execute-ruby (B). The install-ruby set attributes ruby_bin_path to the actual path of installed ruby bin path. The execute-ruby will read attributes ruby_bin_path and expect to get the updated value which is set by the execute-ruby. When converge the attributes gone wrong, empty or not updated. Why?
    1. Compile time:  (A) set attributes is not executed, (B) read the attribute to set a property .
    2. Run Time: (A) running then set attributes, (B) running with already set attributes (old).
  • The solution is to use function lazy evaluated, or move into resource the action todo in (B).

After multiple try and error finally the cookbook will be used on Company Chef Infra. The magic really happen with running ​​bundle exec berks upload  on CI runner. But still got error try to produce locally to get some insight then found:

  • Gem berkshelf without version will install 7.x that is not compatible with ruby version on CI runner or MAYBE the chef version. After search all versions of berkshelf found 6.3.4 is suitable. The thing  I don’t like here is bundler cannot find suitable version since the gem version is not restricted.
  • Different CI runner have different ruby version, some got 2.3. some got 2.4, some don’t have bundle, some times success, some times failed, with retry Luck! Maybe try to tagging with CI runner.
  • After success run: sudo knife cookbook list -c /etc/chef/client.rb to see if cookbook successfully uploaded.
  • The when run ​chef-client the ohai_plugin borrowed from (4) failed to run even the kitchen chef omnibus version already set with same version with real VM. Need to remove it since Not used.
  • Just stick to node.chef_envrionment to get the environment of VM, not from other attributes since it may have different value.

I think that is a lesson I’ve learned for almost two weeks to be able to use MRI ruby on VM.