libsdl-org

Stars and mentions

Last 30 days, Oct 5 – Nov 3

GitHub stars184
mentions13
0481216Oct 5Oct 19Nov 3
UTC days · repositories libsdl-org owns1 repositories

Package downloads

No package download history available yet.

Top repositories

libsdl-org’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside libsdl-org’s account where libsdl-org opened or reviewed pull requests in the past 365 days.

Who noticed libsdl-org

Everyone who starred or forked libsdl-org’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

12posts
Nov 40 posts
  1. Reddit · · r/learnpython · u/ice_cream_pizza901 · libsdl-org/SDL_ttf

    I tried to install pygame throught tonny's package manager, and it gave me this error message. Anything I can do to solve this? install --progress-bar off --user pygame Collecting pygame Using cached pygame-2.6.1.tar.gz (14.8 MB) Installing build dependencies: started Installing build dependencies: finished with status 'done' Getting requirements to build wheel: started Getting requirements to build wheel: finished with status 'error' error: subprocess-exited-with-error × Getting requirements to build wheel did not run successfully. │ exit code: 1 ╰─> [123 lines of output] Skipping Cython compilation WARNING, No "Setup" File Exists, Running "buildconfig/config.py" Using WINDOWS configuration... Making dir :prebuilt_downloads: Downloading... libsdl.org 25ef9d201ce3fd5f976c37dddedac36bd173975c Unzipping :prebuilt_downloads\SDL2-devel-2.28.4-VC.zip: Downloading... libsdl.org 137f86474691f4e12e76e07d58d5920c8d844d5b Unzipping :prebuilt_downloads\SDL2_image-devel-2.0.5-VC.zip: Downloading... github.com/libsdl-org/SDL_ttf/releases/download/r… 371606aceba450384428fd2852f73d2f6290b136 --- For help with compilation see: pygame.org To contribute to pygame development see: pygame.org --- Traceback (most recent call last): File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\urllib\request.py", line 1321, in do_open h.request(req.get_method(), req.selector, req.data, headers, ~~~~~~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ encode_chunked=req.has_header('Transfer-encoding')) \^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\http\client.py", line 1358, in request self._send_request(method, url, body, headers, encode_chunked) ~~~~~~~~~~~~~~~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\http\client.py", line 1404, in _send_request self.endheaders(body, encode_chunked=encode_chunked) ~~~~~~~~~~~~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\http\client.py", line 1353, in endheaders self._send_output(message_body, encode_chunked=encode_chunked) ~~~~~~~~~~~~~~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\http\client.py", line 1113, in _send_output self.send(msg) ~~~~~~~~~\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\http\client.py", line 1057, in send self.connect() ~~~~~~~~~~~~\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\http\client.py", line 1499, in connect self.sock = self._context.wrap_socket(self.sock, ~~~~~~~~~~~~~~~~~~~~~~~~~\^\^\^\^\^\^\^\^\^\^\^ server_hostname=server_hostname) \^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\ssl.py", line 455, in wrap_socket return self.sslsocket_class._create( ~~~~~~~~~~~~~~~~~~~~~~~~~~~~\^ sock=sock, \^\^\^\^\^\^\^\^\^\^ ...<5 lines>... session=session \^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ ) \^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\ssl.py", line 1076, in _create self.do_handshake() ~~~~~~~~~~~~~~~~~\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\ssl.py", line 1372, in do_handshake self._sslobj.do_handshake() ~~~~~~~~~~~~~~~~~~~~~~~~~\^\^ ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081) During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\site-packages\pip\_vendor\pyproject_hooks\_in_process\_in_process.py", line 389, in <module> main() ~~~~\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\site-packages\pip\_vendor\pyproject_hooks\_in_process\_in_process.py", line 373, in main json_out["return_val"] = hook(**hook_input["kwargs"]) ~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\site-packages\pip\_vendor\pyproject_hooks\_in_process\_in_process.py", line 143, in get_requires_for_build_wheel return hook(config_settings) File "C:\Users\pcname\AppData\Local\Temp\pip-build-env-kanljdzv\overlay\Lib\site-packages\setuptools\build_meta.py", line 333, in get_requires_for_build_wheel return self._get_build_requires(config_settings, requirements=[]) ~~~~~~~~~~~~~~~~~~~~~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Temp\pip-build-env-kanljdzv\overlay\Lib\site-packages\setuptools\build_meta.py", line 301, in _get_build_requires self.run_setup() ~~~~~~~~~~~~~~\^\^ File "C:\Users\pcname\AppData\Local\Temp\pip-build-env-kanljdzv\overlay\Lib\site-packages\setuptools\build_meta.py", line 520, in run_setup super().run_setup(setup_script=setup_script) ~~~~~~~~~~~~~~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Temp\pip-build-env-kanljdzv\overlay\Lib\site-packages\setuptools\build_meta.py", line 317, in run_setup exec(code, locals()) # noqa: S102 # exec is intentional here ~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "<string>", line 432, in <module> File "C:\Users\pcname\AppData\Local\Temp\pip-install-taytx0sx\pygame_236d8736d07842b1bbb489950052c45c\buildconfig\config.py", line 234, in main deps = CFG.main(**kwds, auto_config=auto) File "C:\Users\ASUS\AppData\Local\Temp\pip-install-taytx0sx\pygame_236d8736d07842b1bbb489950052c45c\buildconfig\config_win.py", line 479, in main and download_win_prebuilt.ask(**download_kwargs): ~~~~~~~~~~~~~~~~~~~~~~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Temp\pip-install-taytx0sx\pygame_236d8736d07842b1bbb489950052c45c\buildconfig\download_win_prebuilt.py", line 265, in ask update(x86=x86, x64=x64) ~~~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Temp\pip-install-taytx0sx\pygame_236d8736d07842b1bbb489950052c45c\buildconfig\download_win_prebuilt.py", line 248, in update download_prebuilts(download_dir, x86=x86, x64=x64) ~~~~~~~~~~~~~~~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Temp\pip-install-taytx0sx\pygame_236d8736d07842b1bbb489950052c45c\buildconfig\download_win_prebuilt.py", line 116, in download_prebuilts download_sha1_unzip(url, checksum, temp_dir, 1) ~~~~~~~~~~~~~~~~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Temp\pip-install-taytx0sx\pygame_236d8736d07842b1bbb489950052c45c\buildconfig\download_win_prebuilt.py", line 51, in download_sha1_unzip response = urllib.urlopen(request).read() ~~~~~~~~~~~~~~\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\urllib\request.py", line 187, in urlopen return opener.open(url, data, timeout) ~~~~~~~~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\urllib\request.py", line 487, in open response = self._open(req, data) File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\urllib\request.py", line 504, in _open result = self._call_chain(self.handle_open, protocol, protocol + '_open', req) File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\urllib\request.py", line 464, in _call_chain result = func(*args) File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\urllib\request.py", line 1369, in https_open return self.do_open(http.client.HTTPSConnection, req, ~~~~~~~~~~~~\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ context=self._context) \^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^\^ File "C:\Users\pcname\AppData\Local\Programs\Thonny\Lib\urllib\request.py", line 1324, in do_open raise URLError(err) urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081)> [end of output] note: This error originates from a subprocess, and is likely not a problem with pip. ERROR: Failed to build 'pygame' when getting requirements to build wheel Process returned with code 1 (note, any instance of 'pcname' is the name of my computer, I just needed to keep that private).

    7 comments
  2. @icculus Congratulations on getting a cold crimped fibers hooked into the home! In the meantime, I've been meaning to try the DOS port of SDL3 and noticed some important two fixes which I'd like to test with my 386 like box :) SDL#15984 SDL#16573

    6 views
  3. @shihab_7ussain Wait why don't you added SDL2 we controller driver so it can extend controller support to wider range it also open source libsdl-org/SDL

    1 like · 28 views
  4. [quote="Jookia, post:1, topic:143166"] The first is how I should enforce SDLSystem being a singleton. It's unsound to create another SDLSystem as it will drop before the other and invalidate all my references. I plan to just use a global variable and assert, but something with lifetimes sounds better. I was thinking of having SDLSystem be a mutable global object you borrow on success? [/quote] [quote="Jookia, post:1, topic:143166"] Another question to throw in is that I want to make it impossible to use my structures across threads. [/quote] Coming back to original question, and hoping that is doesn't stop the ongoing discussion, ... Often a problem which points towards a singleton pattern in other languages points to other patterns in rust which leverage compile-time guarantees through the type system (including but not limited to Send&Sync) along with fallible initialisers allowing callers to decide how to handle things. A singleton assumes that a caller to init will be happy to get "someone-else's" pre-initialised system, a fallible init lets them decide what to do. Secondly, I find the best safety comments explain not just what & how, but also why - both at the callsite and in the unsafe fn docs. Taking a look at the c code for SDL it stores the SDL MainThreadID in a c global and then relies on that throughout. The docs for SDL_IsMainThread in sdl3_sys::init adds a bit more info than the C comments, highlighting that, except for mac (why?), the SDL main thread is not by definition, the application main thread but whichever thread called the first initialisation. Based on that, if I were designing a nice interface wrapping this, and either not targetting mac or did the research to understand more, I'd probably go for something like the following. Which is a bit different from the usual call_from_main_thread function most often seen in such cases but which, to my mind is a touch more explicit and ergonomic[^1]: Rust Playground #![feature(negative_impls)] use std::sync::{atomic::AtomicBool, mpsc::{Receiver, Sender}}; /// A handle to the main SDL system. /// /// SDL commands need to be called from the same thread that initiated the system. /// SdlSystem is therefore !Send. /// Construction provides a Sender channel to allow for commands /// to be passed from other threads. pub struct SdlSystem { rx: Receiver<SdlCommand>, /// We keep a copy of the Sender ourselves, to avoid the receiver from ever panicking. _tx: Sender<SdlCommand>, } impl !Send for SdlSystem {} // or stable alternative via PhantomData etc... /// At its simplest this could be a type alias for any closure. /// Better would be a more complex custom type wrapper /// or a Box<dyn SdlCommand> with structs/enums for each valid command. pub type SdlCommand = Box<dyn FnOnce()>; pub enum SdlError { DoubleInit, // Various errors mapped to responses expected from `SDL_GetError` // ... } /// Whether the SDL System has already been initialised, *by this library*. /// There appears to be no way to identify whether an SDL System has been initialised /// directly in related C code, or via another library. Sadly the C-static `SDL_MainThreadID` /// is not accessible via the ffi-API, no function such as `Get_MainThreadID` could be found. /// /// Calling [`SdlSystem::init`] will set this flag to `true`. Calling `drop` on an [`SdlSystem`] /// will set this flag to `false` static SDL_INITIALISED: AtomicBool = AtomicBool::new(false); impl SdlSystem { /// Attempts to initialise the main SDL System. The returned SdlSystem /// acts as a guard and the system will be shut down when it is dropped. /// A clone of the relevant Sender for other threads to request command /// execution is provided on initialisation. pub fn init() -> Result<(SdlSystem, Sender<SdlCommand>), SdlError> { todo!("Check & swap SDL_INITIALISED, return SdlError::DoubleInit or init the system") } /// Blocks the current thread and listens for SdlCommands sent via the provided Sender pub fn run(&self) -> Result<!, SdlError> { todo!("run an endlees loop on while let OK rx.recv()"); } } /// Dropping an SdlSystem will call the relevant C destructors and then reset the internal /// flag that guards against double initialisation. impl Drop for SdlSystem { fn drop(&mut self) { todo!("call SDL_Quit etc., swap SDL_INITIALISED") } } [^1]: I'd also be really interested to hear from anyone who has been down this route and run into roadblocks which explain why call_from_main_thread is the prevalent idiom ...

    1 like · 1 comment
  5. bgfx Instancing Tutorial: Draw 625 Cubes with One Draw Call --- title: "bgfx Instancing Tutorial: Draw 625 Cubes with One Draw Call" published: true description: "Implement GPU instancing with C++ and bgfx. Share one cube mesh, pass per-instance positions, and compare the rendered output against individual draw calls." tags: cpp, graphics, gamedev, tutorial series: GPU Implementation with bgfx cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com --- If you draw 625 copies of the same cube, does the CPU need to submit 625 separate drawing commands? It can instead supply a list of placements and request the whole group at once. Instancing draws a shared mesh repeatedly, using different data for each copy. GPU Implementation with bgfx is a series about building GPU effects and processing examples with bgfx. We use the library to make small working programs, connecting code to what appears on screen. This second installment uses a colorful field of moving cubes to explore how drawing commands can be combined. The first demo arranged 25 × 25 cubes and animated their height in a vertex shader. Here we keep the same geometry, wave, and colors while reducing the cube submissions from 625 calls to one. An unchanged picture can be produced through a different data path. You do not need prior experience with instancing. We will recap the relationship between geometry and drawing, then work through the changes using C++ arrays and loops. We will modify the previous cube renderer: keep its window, vertex and index buffers, and camera setup, then replace placement handling and the drawing loop. This is an independent tutorial, and I am not part of the bgfx team. The demo code was written for this article. bgfx uses the BSD 2-Clause license; SDL3, which creates the window, uses the zlib license. 1. Sharing geometry did not combine the draw calls The cube is a mesh: a shape described by vertices and triangles. Each of its six square faces is split into two triangles, giving twelve triangles per cube. The demo's vertex buffer, vb, stores positions and face normals. Its index buffer, ib, stores the vertex numbers that form each triangle. Both buffers are created once and shared by all cubes. In the individual drawing path, each placement still receives a separate submission: Shared cube mesh ├─ set placement A → submit ├─ set placement B → submit └─ …625 submissions bgfx::submit submits a draw using the configured geometry, shader program, and rendering state. It does not mean the GPU has completed the drawing when the call returns. Drawing API Instancing keeps the shared geometry and changes how placements are supplied: Shared mesh ─────────────────────────────┐ ├─ one submit → 625 cubes Placement records [A, B, C, …625 records] ─┘ Each placement record belongs to an instance, one copy of the shared shape. The GPU draws the mesh using each record. This does not divide the triangle count by 625. 2. What changes from the previous implementation We will change how the CPU supplies placements and how the vertex shader receives them. | Location | Previous implementation | This implementation | | --- | --- | --- | | C++ rendering loop | Set a model matrix and submit each cube | Fill a list of centers and submit the group | | Vertex shader | Obtain placement from a model matrix | Read placement from instance input | | Shader input definitions | Vertex positions and normals | Add one center per instance | | Fragment shader | Compute color and shading | Keep the same computation | Put the C++ changes in main.cpp and the additional input definition in varying.def.sc. Create vs_instanced.sc for the new vertex shader and pair it with the existing fs_wave.sc. Keep vs_wave.sc for comparison. The explanation focuses on changes to the previous renderer. The complete files at the end include window creation, rendering, building, and verification, so you can also try the program without having the previous project. The target is Linux, SDL3/X11, the bgfx API 159 SDK, and the OpenGL backend. Use matching headers, libraries, and shaderc. Other backends need their corresponding shader binaries. Use SDL3 for the window and input When adapting the first installment, migrate its SDL initialization too. SDL3 owns the window and input; bgfx handles rendering. No custom library such as CBSS is needed. Include <SDL3/SDL.h> and, in the file defining main, <SDL3/SDL_main.h>. SDL_Init returns true on success, so check failure with if (!SDL_Init(SDL_INIT_VIDEO)). Update the compiler and linker flags to use pkg-config --cflags --libs sdl3. SDL3 migration guide After successfully creating window, obtain its X11 handles through properties: const SDL_PropertiesID properties = SDL_GetWindowProperties(window); void* display = SDL_GetPointerProperty( properties, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr); const Sint64 nativeWindow = SDL_GetNumberProperty( properties, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0); if (!display || nativeWindow == 0) { std::fprintf(stderr, "This demo requires SDL3 X11.\n"); SDL_DestroyWindow(window); SDL_Quit(); return 1; } // init is a bgfx::Init. Set these fields before bgfx::init(init). init.swapChain.ndt = display; init.swapChain.nwh = reinterpret_cast<void*>(uintptr_t(nativeWindow)); display identifies the X11 connection; nativeWindow identifies the native window. Do not pass the SDL window pointer itself as the native handle. This example selects X11/XWayland with SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "x11") before initialization. Window properties API Use SDL_EVENT_QUIT for exit events and SDL_EVENT_KEY_DOWN for key presses. Read the key from event.key.key; the R key constant is SDLK_R. The instance buffers and shaders below belong to bgfx rather than SDL. 3. Each cube only needs a center position The previous version used a 4 × 4 model matrix for each cube. Since these cubes neither rotate nor scale, a center position is sufficient. X represents the horizontal direction, Y represents height, and Z represents depth. Looking toward the origin from negative Z, positive X points right, positive Y points up, and positive Z points away from you. The demo camera looks from above at an angle, so screen-right does not necessarily match positive X. We represent a placement with four floats: // A center 1.12 units along positive X from the origin. const float record[4] = {1.12f, 0.0f, 0.0f, 0.0f}; The first three components are X, Y, and Z; the fourth is unused. A transient instance record in bgfx needs a size that is a multiple of 16 bytes. In this SDK, four floats occupy 16 bytes. The distance from one record to the next is called its stride. Instance buffer API First 16-byte record Second 16-byte record [ centerX, Y, Z, unused ] [ centerX, Y, Z, unused ] … ↓ ↓ Cube 1 Cube 2 Vertex data and instance data have different jobs. A vertex position identifies a corner within the cube; the instance center places that cube in the scene. All vertices belonging to the same instance use the same center. 4. Allocate space for this frame Before the rendering loop, define the count and stride: constexpr uint16_t instanceStride = 16; constexpr uint32_t instanceCount = gridSize * gridSize; // 25 × 25 = 625 Each frame, check capacity before allocating the records. This excerpt comes from the demo's instancing path: // The API returns an available record count, not a boolean. const bool useInstancing = !individual && bgfx::getAvailInstanceDataBuffer(instanceCount, instanceStride) == instanceCount; if (useInstancing) { bgfx::InstanceDataBuffer instances; // The function fills the structure passed through its first argument. bgfx::allocInstanceDataBuffer(&instances, instanceCount, instanceStride); // Fill the records and configure the draw here. } individual is a boolean used to compare the rendering paths. Declare bool individual = false; before the rendering loop. Set it to true for individual draws, or false to use instancing when capacity permits. instances is a description of the allocation, not an inline array containing 625 cubes. instances.data points to the writable bytes, instances.stride gives the record spacing, and instances.num gives the instance count. In C++, &instances passes the address of that structure. This allocation is temporary and belongs to the current frame. Do not retain its pointer across the next bgfx::frame(); allocate again in the next frame. It does not require your own delete or bgfx::destroy. If there is insufficient capacity, use the previous individual drawing loop in the else branch instead of leaving cubes out. Allocation and lifetime contract 5. Write the placement records This loop runs inside the preceding if, immediately after allocation: for (int z = 0; z < gridSize; ++z) { for (int x = 0; x < gridSize; ++x) { // Subtract 12 from indices 0–24 to center the grid on the origin. const float record[4] = {(x-12)*1.12f, 0.0f, (z-12)*1.12f, 0.0f}; const uint32_t index = uint32_t(z * gridSize + x); // Move index records into the buffer, then copy four floats as bytes. std::memcpy(instances.data + index * instances.stride, record, sizeof(record)); } } x and z are grid indices, not distances. At x = 13, z = 12, the center becomes (1.12, 0, 0). The spacing of 1.12 leaves a gap of 0.20 between cubes whose side length is 0.92 when the grid is flat. index flattens the two-dimensional grid into a list. The first row occupies records 0–24, the next 25–49, and so on. At z = 1, x = 2, the index is 27. Its record begins 27 × 16 = 432 bytes from the start. Include <cstring> for std::memcpy. Copying transfers the local array's contents into bgfx's allocation. Creating another record on the next loop iteration does not remove the bytes already written. The CPU still executes a nested loop. What changes is the repeated per-cube state setup and submission. 6. Receive instance positions in the shader Add an instance input to varying.def.sc. This declaration follows the 05-instancing example supplied with the API 159 SDK used here: vec4 i_data0 : TEXCOORD31; i_data0 receives the four floats in a record. TEXCOORD31 identifies the input mapping; it does not cause a texture image to be read. Official sample input definitions The web documentation also contains declarations starting at TEXCOORD7. SDK generations differ, so avoid mixing an input declaration from another generation with your shader compiler. This demo uses the declaration and shaderc from the same SDK. Declare the input at the beginning of the vertex shader too: $input a_position, a_normal, i_data0 $output v_normal, v_world, v_local #include <bgfx_shader.sh> uniform vec4 u_wave; a_position and a_normal vary by vertex. i_data0 varies by instance. u_wave is a uniform shared by the draw: its .x component holds elapsed seconds, and .y holds the wave amplitude. The shader's main() uses these values as follows: void main() { // Every vertex in this cube receives the same center. vec3 center = i_data0.xyz; float radius = length(center.xz); float height = u_wave.y * ( sin(radius * 0.65 - u_wave.x * 1.8) + 0.35 * cos(center.x * 0.5 + center.z * 0.35 + u_wave.x)); // Move the local vertex into the scene, then add the wave height. vec3 world = a_position + center; world.y += height; gl_Position = mul(u_viewProj, vec4(world, 1.0)); // Translation changes neither the direction nor length of the normal. v_normal = a_normal; v_world = world; v_local = a_position; } center.xz selects the horizontal coordinates, and length finds their distance from the origin. The sine term produces a radial wave, with a smaller cosine wave added to it. Calculating height from the center gives every vertex in one cube the same vertical displacement, preserving its shape. For example, a vertex at (0.46, 0.46, 0.46) and a center at (1.12, 0, 0) produce (1.58, 0.46, 0.46) before adding the wave. We then add height to Y and use u_viewProj to apply the camera and projection transformations. Predefined bgfx uniforms This shader does not read a model matrix, so the instanced path does not call setTransform. To add rotation, supply suitable rotation data and implement that transformation too. Instancing does not automatically construct a matrix. 7. Bind the list and submit once During initialization, the demo's loadProgram helper combines the vertex and fragment shaders: // assets identifies the directory containing the executable and shader binaries. auto instancedProgram = loadProgram(assets, "vs_instanced.bin", "fs_wave.bin"); auto uWave = bgfx::createUniform("u_wave", bgfx::UniformType::Vec4); loadProgram is a demo helper, not a bgfx API. It reads the binaries and calls createShader and createProgram. The reference and instanced paths use different vertex shaders but share the fragment shader. Each frame, after filling the allocation, configure the draw inside the same if block. wave is the frame's float array containing {time, amplitude, 0, 0}. bgfx::setVertexBuffer(0, vb); // Shared geometry bgfx::setIndexBuffer(ib); // Triangle connectivity bgfx::setInstanceDataBuffer(&instances); // 625 placements bgfx::setUniform(uWave, wave); // Shared time and amplitude bgfx::setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_WRITE_Z | BGFX_STATE_DEPTH_TEST_LESS); bgfx::submit(0, instancedProgram); // One submission for the cubes The bound instance count determines how many copies are drawn. The 0 in setVertexBuffer identifies a vertex stream; the 0 in submit identifies a view. Depth handling remains the same. WRITE_Z records depth, and DEPTH_TEST_LESS selects nearer surfaces. A separate draw adds the title overlay before bgfx::frame(). One draw refers to the cubes; the complete image uses two submissions including the overlay. 8. Assemble the changes and verify the result Place the pieces in the per-frame loop in this order: Build the wave array from time and amplitude ↓ Check transient capacity for 625 records ├─ Enough capacity and individual is false │ Allocate → write placements → bind buffers → submit once └─ Otherwise Use the previous nested loop: set a model matrix → submit 625 times ↓ Draw the title and any other elements ↓ bgfx::frame() Compile the new vertex shader before loading it. Set BGFX_SDK to the directory containing bgfx, bx, and bimg, then run this from the project root. Adjust the compiler path if your SDK stores shadercRelease elsewhere. # Compile the new vertex shader for OpenGL. mkdir -p build "$BGFX_SDK/bgfx/.build/linux64_gcc/bin/shadercRelease" \ -f shaders/vs_instanced.sc -o build/vs_instanced.bin \ --type vertex --platform linux -p 330 \ --varyingdef shaders/varying.def.sc -i "$BGFX_SDK/bgfx/src" Rebuild the C++ program using the same dependencies as before. Match the filename and location of vs_instanced.bin to what loadProgram reads. Saving shader source alone does not update the binary loaded by the program. First check the individual path with individual = true, then change it to false and rebuild. The count, placement, movement, and colors should remain the same. After rendering several frames, inspect the submission count: // These statistics describe a rendered frame; do not compare immediately after initialization. const bgfx::Stats* stats = bgfx::getStats(); std::printf("Submitted draws: %u\n", stats->numDraw); The statistics include more than cubes. The verification program draws one title overlay and omits the empty submission made by touch, giving 626 for individual rendering and 2 for instancing. Account for any additional UI or other drawing in your application. For a strict visual comparison, fix time and amplitude. With times 0 and 2 seconds and amplitudes 0 and 1.35, all four pairs were pixel-identical in this OpenGL run. Changing time had no effect at zero amplitude, while the enabled wave moved. Capacity exhaustion did not occur in these checks. Fewer submissions do not imply a fixed speedup The cube submissions drop from 625 to one. The program still draws twelve triangles for each of 625 cubes, calculates wave heights, and shades the covered pixels. A 625-fold reduction in submissions is not a 625-fold reduction in frame time. The instance records occupy 625 × 16 = 10,000 bytes. That is the placement data we write, not a measurement of all transferred data. The CPU fills it every frame. Since placement is constant in this demo, a persistent buffer is another possible implementation. Here, transient allocation makes the write and lifetime sequence explicit. Combining draws also requires compatible inputs and state. These cubes share their mesh, shader, and rendering state; only placement differs. Different geometry, materials, or ordering requirements for transparency may require separate groups. Start by identifying what is shared and what varies by instance. A future installment can change the image itself: render to a target, extract bright areas, and blur them into a glow. That shifts the focus from passing a list of shapes to passing images between rendering stages. Complete source by file Below is the complete SDL3 implementation. Save each code block under its filename. These are full files used by the demo, not abbreviated excerpts. The SDK and SDL3 itself are separate dependencies. Create a working directory named demo-instanced-cubes and a shaders subdirectory. Save files whose names begin with shaders/ there, and the remaining files in the working directory. The build creates build/ and generates the title image. You need Linux with X11/XWayland, a C++20-capable g++, pkg-config, SDL3 development headers and libraries, X11 development libraries, Python 3 with Pillow, and DejaVu Sans fonts. Also provide the bgfx API 159 SDK with matching bx, bimg, and shaderc. See the bgfx build guide and SDL3 installation guide. This implementation was run with SDL 3.3.0. The directory identified by BGFX_SDK contains sibling bgfx/, bx/, and bimg/ directories. By default the script reads shadercRelease, libbgfxRelease.a, libbimgRelease.a, and libbxRelease.a from bgfx/.build/linux64_gcc/bin/. Override BGFX_LIB_DIR and SHADERC if your outputs are elsewhere. The complete program accepts --individual to select the reference path without editing the source. Its default is instancing, with individual rendering as a fallback when transient capacity is insufficient. demo_support.h #pragma once #include <bgfx/bgfx.h> #include <bx/math.h> #include <SDL3/SDL.h> #include <algorithm> #include <array> #include <cstdarg> #include <cstdio> #include <cstdlib> #include <filesystem> #include <fstream> #include <stdexcept> #include <string> #include <vector> constexpr uint32_t width = 1280, height = 800; constexpr int gridSize = 25; // bgfx delivers screenshots from its renderer; capture only this application's pixels. struct Capture final : bgfx::CallbackI { void fatal(const char* file, uint16_t line, bgfx::Fatal::Enum, const char* msg) override { std::fprintf(stderr, "bgfx fatal %s:%u: %s\n", file, line, msg); std::abort(); } void traceVargs(const char*, uint16_t, const char* format, va_list args) override { std::vfprintf(stderr, format, args); } void profilerBegin(const char*, uint32_t, const char*, uint16_t) override {} void profilerBeginLiteral(const char*, uint32_t, const char*, uint16_t) override {} void profilerEnd() override {} uint32_t cacheReadSize(uint64_t) override { return 0; } bool cacheRead(uint64_t, void*, uint32_t) override { return false; } void cacheWrite(uint64_t, const void*, uint32_t) override {} void screenShot(const char* path, uint32_t w, uint32_t h, uint32_t pitch, bgfx::TextureFormat::Enum format, const void* data, uint32_t, bool flip) override { if (format != bgfx::TextureFormat::BGRA8 && format != bgfx::TextureFormat::RGBA8) fatal(__FILE__, __LINE__, bgfx::Fatal::DebugCheck, "Unsupported screenshot format"); std::ofstream file(path, std::ios::binary); file << "P6\n" << w << ' ' << h << "\n255\n"; const auto* pixels = static_cast<const uint8_t*>(data); std::vector<char> rgb(size_t(w)*h*3); for (uint32_t y = 0; y < h; ++y) { const auto* row = pixels + (flip ? h - 1 - y : y) * pitch; for (uint32_t x = 0; x < w; ++x) { const auto* p = row + 4*x; auto offset=(size_t(y)*w+x)*3; rgb[offset] = char(p[format == bgfx::TextureFormat::BGRA8 ? 2 : 0]); rgb[offset+1] = char(p[1]); rgb[offset+2] = char(p[format == bgfx::TextureFormat::BGRA8 ? 0 : 2]); } } file.write(rgb.data(), std::streamsize(rgb.size())); if (!file) fatal(__FILE__, __LINE__, bgfx::Fatal::DebugCheck, "Screenshot write failed"); } void captureBegin(uint32_t, uint32_t, uint32_t, bgfx::TextureFormat::Enum, bool) override {} void captureEnd() override {} void captureFrame(const void*, uint32_t) override {} }; std::vector<char> readFile(const std::filesystem::path& path) { std::ifstream input(path, std::ios::binary | std::ios::ate); if (!input) throw std::runtime_error("Cannot open " + path.string()); const auto length = input.tellg(); if (length <= 0) throw std::runtime_error("Empty file " + path.string()); std::vector<char> bytes(static_cast<size_t>(length)); input.seekg(0); input.read(bytes.data(), length); if (!input) throw std::runtime_error("Cannot read " + path.string()); return bytes; } bgfx::ProgramHandle loadProgram(const std::filesystem::path& dir, const char* vs, const char* fs) { auto vb = readFile(dir / vs), fb = readFile(dir / fs); auto v = bgfx::createShader(bgfx::copy(vb.data(), uint32_t(vb.size()))); auto f = bgfx::createShader(bgfx::copy(fb.data(), uint32_t(fb.size()))); if (!bgfx::isValid(v) || !bgfx::isValid(f)) { if (bgfx::isValid(v)) bgfx::destroy(v); if (bgfx::isValid(f)) bgfx::destroy(f); throw std::runtime_error("Shader creation failed"); } auto program = bgfx::createProgram(v, f, true); // Program owns these shader references. if (!bgfx::isValid(program)) throw std::runtime_error("Program creation failed"); return program; } struct Vertex { float x, y, z, nx, ny, nz; }; struct OverlayVertex { float x, y, z, u, v; }; main.cpp #include "demo_support.h" #include <cstring> #include <SDL3/SDL_main.h> int main(int argc, char** argv) { int frameLimit = 0; float startTime = 0.0f, amplitude = 1.35f; bool fixedTime = false, paused = false, individual = false; std::string screenshot, recordDir; // Explicit deterministic controls let the validation script compare real rendered frames. try { for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; auto value = [&]() { if (++i >= argc) throw std::runtime_error("Missing argument"); return argv[i]; }; if (arg == "--individual") individual = true; else if (arg == "--frames") frameLimit = std::stoi(value()); else if (arg == "--time") { startTime = std::stof(value()); fixedTime = true; } else if (arg == "--amplitude") amplitude = std::stof(value()); else if (arg == "--screenshot") screenshot = value(); else if (arg == "--record") recordDir = value(); else throw std::runtime_error("Unknown argument: " + arg); } if ((!screenshot.empty() || !recordDir.empty()) && frameLimit < 4) throw std::runtime_error("Capture requires --frames >= 4"); if (!recordDir.empty()) std::filesystem::create_directories(recordDir); } catch (const std::exception& e) { std::fprintf(stderr, "%s\n", e.what()); return 1; } // This demonstrator deliberately selects SDL's X11 path and bgfx's OpenGL backend. SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "x11"); if (!SDL_Init(SDL_INIT_VIDEO)) { std::fprintf(stderr, "%s\n", SDL_GetError()); return 1; } SDL_Window* window = SDL_CreateWindow("GPU Sketchbook | Instanced cubes", width, height, recordDir.empty() && screenshot.empty() ? 0 : SDL_WINDOW_HIDDEN); if (!window) { std::fprintf(stderr, "%s\n", SDL_GetError()); SDL_Quit(); return 1; } // SDL3 exposes native handles through window properties instead of SDL_SysWMinfo. const SDL_PropertiesID properties = SDL_GetWindowProperties(window); void* display = SDL_GetPointerProperty(properties, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr); const Sint64 nativeWindow = SDL_GetNumberProperty(properties, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0); if (!display || nativeWindow == 0) { std::fprintf(stderr, "This demo requires SDL3 X11 (XWayland is fine).\n"); SDL_DestroyWindow(window); SDL_Quit(); return 1; } Capture capture; bgfx::Init init; init.type = bgfx::RendererType::OpenGL; init.fallback = false; // A backend change would require matching shader binaries. init.swapChain.nwh = reinterpret_cast<void*>(uintptr_t(nativeWindow)); init.swapChain.ndt = display; init.swapChain.width = width; init.swapChain.height = height; init.reset = BGFX_RESET_VSYNC; init.callback = &capture; if (!bgfx::init(init)) { SDL_DestroyWindow(window); SDL_Quit(); return 1; } int result = 0; try { const auto assets = std::filesystem::canonical(argv[0]).parent_path(); std::vector<Vertex> vertices; std::vector<uint16_t> indices; // Four distinct vertices per face preserve a flat face normal at each corner. const float normals[6][3] = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}; for (int face = 0; face < 6; ++face) { const int axis = face / 2, a = (axis + 1) % 3, b = (axis + 2) % 3; const uint16_t base = uint16_t(vertices.size()); for (int corner = 0; corner < 4; ++corner) { float p[3]{}; p[axis] = (face % 2 == 0 ? 0.46f : -0.46f); p[a] = (corner == 1 || corner == 2) ? 0.46f : -0.46f; p[b] = corner >= 2 ? 0.46f : -0.46f; vertices.push_back({p[0],p[1],p[2],normals[face][0],normals[face][1],normals[face][2]}); } for (uint16_t offset : {0,1,2,0,2,3}) indices.push_back(base + offset); } bgfx::VertexLayout layout; layout.begin().add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) .add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Float).end(); auto vb = bgfx::createVertexBuffer(bgfx::copy(vertices.data(), uint32_t(vertices.size()*sizeof(Vertex))), layout); auto ib = bgfx::createIndexBuffer(bgfx::copy(indices.data(), uint32_t(indices.size()*sizeof(uint16_t)))); auto program = loadProgram(assets, "vs_wave.bin", "fs_wave.bin"); auto instancedProgram = loadProgram(assets, "vs_instanced.bin", "fs_wave.bin"); // Each record is one center position plus an unused fourth float. constexpr uint16_t instanceStride = 16; constexpr uint32_t instanceCount = gridSize * gridSize; uint32_t instancedFrames = 0, individualFrames = 0; auto uWave = bgfx::createUniform("u_wave", bgfx::UniformType::Vec4); // A separate screen-space pass adds only typography, not the 3D effect. const OverlayVertex quad[] = {{-1,1,0,0,0},{1,1,0,1,0},{1,-1,0,1,1},{-1,-1,0,0,1}}; const uint16_t quadIndices[] = {0,1,2,0,2,3}; bgfx::VertexLayout overlayLayout; overlayLayout.begin().add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float).end(); auto overlayVb = bgfx::createVertexBuffer(bgfx::copy(quad, sizeof(quad)), overlayLayout); auto overlayIb = bgfx::createIndexBuffer(bgfx::copy(quadIndices, sizeof(quadIndices))); auto overlayProgram = loadProgram(assets, "vs_overlay.bin", "fs_overlay.bin"); auto overlayBytes = readFile(assets / "overlay.rgba"); if (overlayBytes.size() != width * height * 4) throw std::runtime_error("Wrong overlay size"); auto overlayTexture = bgfx::createTexture2D(width, height, false, 1, bgfx::TextureFormat::RGBA8, BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP, bgfx::copy(overlayBytes.data(), uint32_t(overlayBytes.size()))); auto overlaySampler = bgfx::createUniform("s_overlay", bgfx::UniformType::Sampler); if (!bgfx::isValid(vb) || !bgfx::isValid(ib) || !bgfx::isValid(uWave) || !bgfx::isValid(overlayVb) || !bgfx::isValid(overlayIb) || !bgfx::isValid(overlayTexture) || !bgfx::isValid(overlaySampler)) throw std::runtime_error("Resource creation failed"); bgfx::setViewClear(0, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x0b1020ff, 1.0f, 0); bgfx::setViewRect(0, 0, 125, width, 605); bgfx::setViewRect(1, 0, 0, width, height); float view[16], projection[16]; bx::mtxLookAt(view, {27.0f, 25.0f, -32.0f}, {0.0f, 0.0f, 0.0f}); bx::mtxOrtho(projection, -25.0f, 25.0f, -11.82f, 11.82f, 0.1f, 120.0f, 0.0f, bgfx::getCaps()->homogeneousDepth); bgfx::setViewTransform(0, view, projection); bgfx::setViewClear(1, BGFX_CLEAR_NONE); std::printf("Renderer: %s; vendor=0x%04x device=0x%04x; cubes=%d\n", bgfx::getRendererName(bgfx::getRendererType()), bgfx::getCaps()->vendorId, bgfx::getCaps()->deviceId, gridSize*gridSize); uint64_t previous = SDL_GetPerformanceCounter(); float time = startTime; bool running = true; int frame = 0; while (running && (frameLimit == 0 || frame < frameLimit)) { SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_EVENT_QUIT) running = false; if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) { switch (event.key.key) { case SDLK_ESCAPE: running = false; break; case SDLK_SPACE: paused = !paused; break; case SDLK_UP: amplitude = std::min(3.0f, amplitude + 0.15f); break; case SDLK_DOWN: amplitude = std::max(0.0f, amplitude - 0.15f); break; case SDLK_R: time = 0.0f; amplitude = 1.35f; paused = false; break; default: break; } } } const uint64_t now = SDL_GetPerformanceCounter(); if (!paused && !fixedTime) time += float(double(now-previous)/double(SDL_GetPerformanceFrequency())); previous = now; if (!recordDir.empty()) time = startTime + float(frame)/30.0f; const float wave[4] = {time, amplitude, 0.0f, 0.0f}; // Check capacity before allocation. Never silently omit cubes. const bool useInstancing = !individual && bgfx::getAvailInstanceDataBuffer(instanceCount, instanceStride) == instanceCount; if (useInstancing) { bgfx::InstanceDataBuffer instances; bgfx::allocInstanceDataBuffer(&instances, instanceCount, instanceStride); for (int z = 0; z < gridSize; ++z) { for (int x = 0; x < gridSize; ++x) { const float record[4] = {(x-12)*1.12f, 0.0f, (z-12)*1.12f, 0.0f}; const uint32_t index = uint32_t(z * gridSize + x); // Copy bytes without assuming float alignment of the returned pointer. std::memcpy(instances.data + index * instances.stride, record, sizeof(record)); } } bgfx::setVertexBuffer(0, vb); bgfx::setIndexBuffer(ib); bgfx::setInstanceDataBuffer(&instances); bgfx::setUniform(uWave, wave); bgfx::setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_WRITE_Z | BGFX_STATE_DEPTH_TEST_LESS); bgfx::submit(0, instancedProgram); ++instancedFrames; } else { // Reference path also handles insufficient transient capacity. for (int z = 0; z < gridSize; ++z) { for (int x = 0; x < gridSize; ++x) { float model[16]; // CPU places cube centers on a flat grid. GPU calculates their height. bx::mtxTranslate(model, (x-12)*1.12f, 0.0f, (z-12)*1.12f); bgfx::setTransform(model); bgfx::setVertexBuffer(0, vb); bgfx::setIndexBuffer(ib); bgfx::setUniform(uWave, wave); // Culling is deliberately disabled; opposite cube faces have independent vertices. bgfx::setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_WRITE_Z | BGFX_STATE_DEPTH_TEST_LESS); bgfx::submit(0, program); } } ++individualFrames; } bgfx::setVertexBuffer(0, overlayVb); bgfx::setIndexBuffer(overlayIb); bgfx::setTexture(0, overlaySampler, overlayTexture); bgfx::setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_BLEND_ALPHA); bgfx::submit(1, overlayProgram); if (!recordDir.empty()) { char name[64]; std::snprintf(name, sizeof(name), "/frame-%04d.ppm", frame); bgfx::requestScreenShot(BGFX_INVALID_HANDLE, (recordDir + name).c_str()); } else if (!screenshot.empty() && frame == frameLimit-2) { bgfx::requestScreenShot(BGFX_INVALID_HANDLE, screenshot.c_str()); } bgfx::frame(); ++frame; } const auto* stats = bgfx::getStats(); std::printf("Completed %d frames; last frame draws=%u; amplitude=%.2f; time=%.3f\n", frame, stats->numDraw, amplitude, time); std::printf("Instanced frames=%u; individual frames=%u\n", instancedFrames, individualFrames); if (bgfx::isValid(instancedProgram)) bgfx::destroy(instancedProgram); bgfx::destroy(overlaySampler); bgfx::destroy(overlayTexture); bgfx::destroy(overlayProgram); bgfx::destroy(overlayVb); bgfx::destroy(overlayIb); bgfx::destroy(uWave); bgfx::destroy(program); bgfx::destroy(vb); bgfx::destroy(ib); } catch (const std::exception& e) { std::fprintf(stderr, "%s\n", e.what()); result = 1; } bgfx::shutdown(); // Finish renderer work before destroying the native window or callback. SDL_DestroyWindow(window); SDL_Quit(); return result; } shaders/varying.def.sc vec3 a_position : POSITION; vec3 a_normal : NORMAL; vec2 a_texcoord0 : TEXCOORD0; vec3 v_normal : TEXCOORD1; vec3 v_world : TEXCOORD2; vec3 v_local : TEXCOORD3; vec2 v_uv : TEXCOORD0; vec4 i_data0 : TEXCOORD31; shaders/vs_instanced.sc $input a_position, a_normal, i_data0 $output v_normal, v_world, v_local #include <bgfx_shader.sh> // x: time in seconds, y: amplitude, z/w: unused. uniform vec4 u_wave; void main() { // One record per cube: all its vertices receive the same center. vec3 center = i_data0.xyz; float radius = length(center.xz); float height = u_wave.y * ( sin(radius * 0.65 - u_wave.x * 1.8) + 0.35 * cos(center.x * 0.5 + center.z * 0.35 + u_wave.x)); vec3 world = a_position + center; world.y += height; gl_Position = mul(u_viewProj, vec4(world, 1.0)); // Placement only translates; normals need no rotation or rescaling. v_normal = a_normal; v_world = world; v_local = a_position; } shaders/vs_wave.sc $input a_position, a_normal $output v_normal, v_world, v_local #include <bgfx_shader.sh> // x: time in seconds, y: amplitude, z/w: unused. uniform vec4 u_wave; void main() { // Transform the cube's center separately: every corner gets the same height. vec3 center = mul(u_model[0], vec4(0.0, 0.0, 0.0, 1.0)).xyz; float radius = length(center.xz); float height = u_wave.y * ( sin(radius * 0.65 - u_wave.x * 1.8) + 0.35 * cos(center.x * 0.5 + center.z * 0.35 + u_wave.x)); vec3 world = mul(u_model[0], vec4(a_position, 1.0)).xyz; world.y += height; gl_Position = mul(u_viewProj, vec4(world, 1.0)); // Model matrices only translate; normals need no rotation or rescaling here. v_normal = a_normal; v_world = world; v_local = a_position; } shaders/fs_wave.sc $input v_normal, v_world, v_local #include <bgfx_shader.sh> void main() { // A spatial palette: raised cubes also move through the color gradient. float phase = v_world.x * 0.022 + v_world.z * 0.018 + v_world.y * 0.055; vec3 color = 0.55 + 0.45 * cos(6.2831853 * (vec3(phase, phase, phase) + vec3(0.02, 0.35, 0.65))); float light = 0.32 + 0.68 * max(dot(normalize(v_normal), normalize(vec3(-0.45, 0.85, -0.55))), 0.0); // The second-largest local coordinate approaches 0.46 near a face edge. vec3 p = abs(v_local); float edgeCoordinate = max(min(p.x, p.y), max(min(p.y, p.z), min(p.z, p.x))); float edge = smoothstep(0.425, 0.455, edgeCoordinate); color = color * light + edge * vec3(0.12, 0.18, 0.22); gl_FragColor = vec4(color, 1.0); } shaders/vs_overlay.sc $input a_position, a_texcoord0 $output v_uv #include <bgfx_shader.sh> void main() { gl_Position = vec4(a_position, 1.0); v_uv = a_texcoord0; } shaders/fs_overlay.sc $input v_uv #include <bgfx_shader.sh> SAMPLER2D(s_overlay, 0); void main() { gl_FragColor = texture2D(s_overlay, v_uv); } make_overlay.py """Create the demo's transparent title card; no screenshot manipulation.""" from pathlib import Path from PIL import Image, ImageDraw, ImageFont root = Path(__file__).resolve().parent out = root / "build" out.mkdir(exist_ok=True) im = Image.new("RGBA", (1280, 800)) d = ImageDraw.Draw(im) d.rectangle((0, 0, 1279, 124), fill="#0b1020") d.rectangle((0, 730, 1279, 799), fill="#0b1020") fonts = Path("/usr/share/fonts/truetype/dejavu") def font(size, bold=False): return ImageFont.truetype(str(fonts / ("DejaVuSans-Bold.ttf" if bold else "DejaVuSans.ttf")), size) d.rounded_rectangle((46, 38, 53, 61), 3, fill="#6ee7dc") d.text((65, 38), "GPU SKETCHBOOK / 02", font=font(16, True), fill="#94b4c3") d.text((44, 72), "625 cubes. One draw.", font=font(49, True), fill="#f2f5ff") d.text((47, 136), "Same wave. Shared mesh. Per-instance positions.", font=font(18), fill="#97a7bd") d.rounded_rectangle((1040, 42, 1232, 79), 18, fill="#152a35", outline="#30525e") d.ellipse((1057, 56, 1067, 66), fill="#6ee7dc") d.text((1080, 50), "bgfx / OpenGL", font=font(15), fill="#c7e7e6") d.line((48, 730, 1232, 730), fill="#293446", width=1) d.text((48, 751), "SPACE pause UP / DOWN amplitude R reset ESC exit", font=font(15), fill="#a7b7cb") d.text((1030, 751), "bgfx instancing", font=font(15), fill="#6ee7dc") (out / "overlay.rgba").write_bytes(im.tobytes()) build.sh #!/usr/bin/env bash set -euo pipefail cd -- "$(dirname -- "$0")" : "${BGFX_SDK:?Set BGFX_SDK to the directory containing bgfx, bx, and bimg}" bgfx_lib_dir="${BGFX_LIB_DIR:-$BGFX_SDK/bgfx/.build/linux64_gcc/bin}" shaderc_path="${SHADERC:-$bgfx_lib_dir/shadercRelease}" mkdir -p build "${PYTHON:-python3}" make_overlay.py for stage in vs_wave vs_instanced fs_wave vs_overlay fs_overlay; do shader_type=vertex if [[ "$stage" == fs_* ]]; then shader_type=fragment; fi "$shaderc_path" -f "shaders/$stage.sc" -o "build/$stage.bin" \ --type "$shader_type" --platform linux -p 330 \ --varyingdef shaders/varying.def.sc -i "$BGFX_SDK/bgfx/src" done read -r -a sdl_cflags <<< "$(pkg-config --cflags sdl3)" read -r -a sdl_libs <<< "$(pkg-config --libs sdl3)" g++ -std=c++20 -O2 -Wall -Wextra -Werror -fmax-errors=5 -DBX_CONFIG_DEBUG=0 \ "${sdl_cflags[@]}" -I"$BGFX_SDK/bgfx/include" -I"$BGFX_SDK/bx/include" \ main.cpp -o build/instanced-cubes \ "$bgfx_lib_dir/libbgfxRelease.a" "$bgfx_lib_dir/libbimgRelease.a" \ "$bgfx_lib_dir/libbxRelease.a" "${sdl_libs[@]}" -lX11 -ldl -lpthread -lrt verify_instancing.py """Compare actual framebuffer pixels and draw counts, not estimated FPS.""" from pathlib import Path import json import re import subprocess from PIL import Image, ImageChops root = Path(__file__).resolve().parent build = root / 'build' report = {} images = {} for name, time, amplitude in [('flat-0', 0, 0), ('flat-2', 2, 0), ('wave-0', 0, 1.35), ('wave-2', 2, 1.35)]: for mode in ['instanced', 'individual']: key = name + '-' + mode path = build / (key + '.ppm') args = [str(build / 'instanced-cubes'), '--frames', '8', '--time', str(time), '--amplitude', str(amplitude), '--screenshot', str(path)] if mode == 'individual': args.append('--individual') run = subprocess.run(args, check=True, capture_output=True, text=True, timeout=30) draws = int(re.search(r'last frame draws=(\d+)', run.stdout)[1]) assert draws == (2 if mode == 'instanced' else 626), run.stdout assert ('Instanced frames=8' if mode == 'instanced' else 'individual frames=8') in run.stdout images[key] = Image.open(path).convert('RGB') assert images[key].size == (1280, 800) assert len(images[key].getcolors(1280*800) or []) > 100 report[key] = {'draws_including_overlay': draws, 'stdout': run.stdout.strip()} delta = ImageChops.difference(images[name+'-instanced'], images[name+'-individual']) assert delta.getbbox() is None, f'{name}: rendering paths differ' assert ImageChops.difference(images['flat-0-instanced'], images['flat-2-instanced']).getbbox() is None changed = sum(max(pixel) > 8 for pixel in ImageChops.difference(images['wave-0-instanced'], images['wave-2-instanced']).getdata()) assert changed > 10000, changed report['checks'] = {'four_pairs_pixel_identical': True, 'zero_amplitude_time_invariant': True, 'moving_wave_changed_pixels': changed} (build/'instancing-validation.json').write_text(json.dumps(report, indent=2)+'\n') print(json.dumps(report['checks'])) Build and run the saved files Run these commands inside demo-instanced-cubes. Set PYTHON to an interpreter with Pillow installed. export BGFX_SDK=/path/to/bgfx-sdk export PYTHON=/usr/bin/python3 pkg-config --modversion sdl3 bash build.sh ./build/instanced-cubes # Render the same scene with individual draws. ./build/instanced-cubes --individual # Compare framebuffer images and draw counts under four fixed conditions. "$PYTHON" verify_instancing.py Space pauses, Up/Down change amplitude, R resets, and Esc exits. The verification script uses hidden windows but still requires access to X11 and OpenGL. It writes build/instancing-validation.json and fails if images differ or submission counts do not match expectations. Clay Board Style System Clay Board Style System is a foundation I am developing for native GUI toolkits, drawing on CSS concepts. This demo runs independently with C++ and SDL3 and does not use CBSS. If you want to combine GPU rendering with UI layout and input, see its bgfx adapter through bgfxim and the GPU host documentation.

    1 reaction
  6. @raysan5 You can prototype a lot of things with SDL3 and AI 👀 libsdl-org/SDL

    1 like · 103 views
  7. @quimedesu This one? they specifically tell you to not use AI in the claude and agents files 💀 Nice trick tbh github.com/libsdl-org/SDL/blob/main/CLAUDE.md

    12 views
  8. We got text in our game using SDL TTF github.com/libsdl-org/SDL_ttf/tree/main . Using C libraries/code in Zig is simple. But when using the open gl core profile, you must provide a VAO! This stumped us for a couple hours. And we share this fact so you don't lose hours. #gamedev #opengl #indiegame

    7 likes · 2 reposts
  9. Reddit · · r/Onimusha · u/projectxsent · libsdl-org/sdl2-compat

    Onimusha 3 PC Controller Fix Hello guys. I'd like to share my controller fix for this game. It uses SDL2 so XInput/Xbox, PS, 8BitDo, GameSir controllers, etc. will be compatible. ProjectXsent/onimusha3-gamepad-fix Notes: - If you're using OnimushaEnhanced, disable XInput and Rumble before launching the game. - If you're using the Onimusha 3 Widescreen Fix, rename "dinput8.dll" to "d3d9.dll" then extract the controller fix. - If you have modern controllers like Steam Controller 2, use SDL2 Compat.

    2 points
  10. macOS で kext も DriverKit も使わずに USB デバイスのドライバを書く(Switch 2 Pro Controller を例に) はじめに macOS 用のデバイスドライバというと kext や DriverKit を思い浮かべますが、システムのドライバが掴んでいないインターフェースなら、普通のアプリ(ユーザー空間)から直接 USB を叩けます。署名の特別な entitlement も、SIP の無効化も要りません。 この記事では、macOS が標準サポートしていない Nintendo Switch 2 Pro Controller を題材に、 - macOS のドライバ実装の選択肢と、どれを選ぶかの判断基準 - IORegistry でデバイスを調べる方法 - IOUSBHost で Bulk 転送 → IOHIDManager で入力を読む実装 - 入力をキー/マウス操作に変換する、BLE で接続する - 配布時の署名・TCC(プライバシー許可)の注意 をまとめます。 成果物: - 最小サンプル(約200行、MIT): kakignu/switch2pro-macos-sample - 完成版アプリ(メニューバー常駐、キー割り当て GUI 付き)の配布ページ: kakignu/Switch2ProBridge-releases :::note info 確認環境: Apple Silicon Mac / macOS 26 / Swift 5.9+(Swift Package)。最小デプロイ先は macOS 13。 ::: macOS のドライバ実装、3 つの選択肢 | 方式 | 動く場所 | 必要なもの | 向いているケース | |---|---|---|---| | kext | カーネル | 起動セキュリティの緩和・ユーザー承認。Apple は非推奨 | 新規開発ではまず選ばない | | DriverKit(dext) | ユーザー空間(専用プロセス) | com.apple.developer.driverkit.transport.usb などの entitlement を Apple に申請。対象 VID の指定が必要で、他社製品の VID は通常もらえない | 自社ハードを OS 全体に見せたい | | ユーザー空間ライブラリ(IOKit / IOUSBHost / IOHIDManager / CoreBluetooth) | 普通のアプリ | 基本なし | システムのドライバが掴んでいないインターフェースを、自分のアプリ(や常駐アプリ)から使う | 他社製品をとりあえず Mac で使いたい、という個人開発ではほぼ 3 番目一択です。問題は「そのインターフェースは誰かに掴まれていないか」で、これは IORegistry を見れば分かります。 まず IORegistry を見る デバイスを挿して ioreg で構成を確認します。 # USB デバイスのツリー(デバイス → インターフェース → ドライバ) ioreg -p IOService -l -r -c IOUSBHostDevice -n "Pro Controller" | less # インターフェースだけを一覧 ioreg -r -c IOUSBHostInterface -l | grep -E '"(bInterfaceNumber|bInterfaceClass|IOClass)"|\+-o' 見るポイントは 2 つです。 1. bInterfaceClass: 0x03 は HID、0xFF はベンダー定義 2. IOUSBHostInterface の下にドライバ(子ノード)がぶら下がっているか: AppleUserHIDDevice などが付いていれば掴まれている。何も付いていなければアプリから開ける Switch 2 Pro Controller(VID 0x057E / PID 0x2069)の場合はこうなっていました。 IOUSBHostDevice "Pro Controller" ├─ IOUSBHostInterface #0 class 0x03 (HID) ← AppleUserHIDDevice が掴む └─ IOUSBHostInterface #1 class 0xFF (Vendor) ← 誰も掴んでいない EP 0x02 Bulk OUT / EP 0x82 Bulk IN (64B) さらに、このコントローラーは挿しただけでは HID 入力を送ってきません。IF#1 に初期化コマンドを送ると、IF#0 から入力レポートが流れ始めます。つまり、 1. IF#1(空いている)を IOUSBHost で開いて初期化コマンドを送る 2. IF#0(macOS の HID ドライバが掴んでいる)は IOHIDManager で読む という役割分担になります。IF#0 は掴まれているので IOUSBHost では開けませんが、HID として既に OS に見えているので、HID の API で読めば十分です。 :::note info プロトコル(コマンド形式・初期化手順・レポート形式)は ndeadly/switch2_controller_research と SDL3 の SDL_hidapi_switch2.c を参考にしました。 ::: 実装 Swift Package の構成 IOUSBHost は Objective-C の API なので、薄い Objective-C ラッパーを 1 ターゲット作り、Swift から呼びます。 // swift-tools-version:5.9 import PackageDescription let package = Package( name: "Switch2ProMacOSSample", platforms: [.macOS(.v13)], targets: [ .target( name: "USBBulk", cSettings: [.unsafeFlags(["-fobjc-arc"])], // ← 重要 linkerSettings: [ .linkedFramework("IOKit"), .linkedFramework("IOUSBHost"), ] ), .executableTarget( name: "switch2-sample", dependencies: ["USBBulk"], linkerSettings: [.linkedFramework("IOKit")] ), ] ) :::note warn SwiftPM の Objective-C ターゲットは ARC 無効がデフォルトです。-fobjc-arc を付けないと、ARC 前提で書いたコードがリークしたりクラッシュしたりします。 ::: 1. デバイスとインターフェースを探す IOUSBHostDevice を列挙し、VID/PID が一致したデバイスの子から bInterfaceNumber == 1 の IOUSBHostInterface を探します。 func intProperty(_ entry: io_registry_entry_t, _ key: String) -> Int? { IORegistryEntryCreateCFProperty(entry, key as CFString, kCFAllocatorDefault, 0)? .takeRetainedValue() as? Int } func next(_ iterator: io_iterator_t) -> io_object_t? { let obj = IOIteratorNext(iterator) return obj == 0 ? nil : obj } func findCommandInterface() -> io_service_t? { var devices: io_iterator_t = 0 guard IOServiceGetMatchingServices(kIOMainPortDefault, IOServiceMatching("IOUSBHostDevice"), &devices) == KERN_SUCCESS else { return nil } defer { IOObjectRelease(devices) } while let device = next(devices) { defer { IOObjectRelease(device) } guard intProperty(device, "idVendor") == 0x057E, intProperty(device, "idProduct") == 0x2069 else { continue } var children: io_iterator_t = 0 guard IORegistryEntryGetChildIterator(device, kIOServicePlane, &children) == KERN_SUCCESS else { continue } defer { IOObjectRelease(children) } while let child = next(children) { if IOObjectConformsTo(child, "IOUSBHostInterface") != 0, intProperty(child, "bInterfaceNumber") == 1 { return child // 呼び出し側で IOObjectRelease } IOObjectRelease(child) } } return nil } ハマりどころが 2 つありました。 :::note warn マッチングDictionaryに数値プロパティを入れるとマッチしなかった IOServiceMatching のDictionaryに idVendor / idProduct を数値で入れると、実機では 1 件もマッチしませんでした(レジストリ側の OSNumber とビット幅が合わないためと推測)。クラス名だけでマッチして、VID/PID はコードで比較するのが確実です。 ::: :::note warn インターフェースはデバイスより少し遅れて登録される デバイス検出直後に子を探すと、まだインターフェースが無いことがあります。0.25 秒間隔で数秒リトライしています。常駐アプリなら IOServiceAddMatchingNotification で抜き差しを監視し、検出時に同じ探索をします。 ::: 2. IOUSBHost で Bulk 送受信する IOUSBHostInterface を開き、エンドポイントアドレスから IOUSBHostPipe を取り出して sendIORequestWithData: で同期送受信します。 - (nullable instancetype)initWithService:(io_service_t)service outEndpoint:(uint8_t)outEP inEndpoint:(uint8_t)inEP error:(NSError **)error { self = [super init]; if (!self) { return nil; } // システムのドライバが掴んでいるインターフェースだとここで失敗する _interface = [[IOUSBHostInterface alloc] initWithIOService:service options:IOUSBHostObjectInitOptionsNone queue:nil error:error interestHandler:nil]; if (!_interface) { return nil; } _outPipe = [_interface copyPipeWithAddress:outEP error:error]; if (!_outPipe) { [self close]; return nil; } _inPipe = [_interface copyPipeWithAddress:inEP error:error]; if (!_inPipe) { [self close]; return nil; } return self; } - (nullable NSData *)sendCommand:(NSData *)command maxResponseLength:(NSUInteger)maxResponseLength timeout:(NSTimeInterval)timeout error:(NSError **)error { NSMutableData *outData = [command mutableCopy]; NSUInteger sent = 0; if (![_outPipe sendIORequestWithData:outData bytesTransferred:&sent completionTimeout:1.0 error:error]) { return nil; } NSMutableData *response = [NSMutableData data]; while (response.length < maxResponseLength) { // Bulk IN は wMaxPacketSize の倍数で要求する(オーバーラン防止) NSMutableData *chunk = [NSMutableData dataWithLength:64]; NSUInteger got = 0; if (![_inPipe sendIORequestWithData:chunk bytesTransferred:&got completionTimeout:timeout error:nil]) { break; // タイムアウト = 応答なし } [response appendBytes:chunk.bytes length:got]; if (got < 64) { break; } // ショートパケット = 転送終了 } return response; } Swift 側からは try USBBulkInterface(service:outEndpoint:inEndpoint:)、try iface.sendCommand(_:maxResponseLength:timeout:) として呼べます(NSError ** を取るメソッドは Swift では throws になります)。 3. 初期化コマンドを送る コマンドは 8 バイトのヘッダ + データです。 [0] cmd [1] 0x91 (host→device) [2] 0x00 (USB) / 0x01 (BLE) [3] subcmd [4] 0x00 [5] データ長 [6..7] 0x00 [8..] データ 応答は [cmd] 0x01 0x00 [subcmd] 0x00 0xF8 … のような形で返ってきます(0xF8 が ACK)。 func command(_ cmd: UInt8, _ sub: UInt8, _ data: [UInt8] = []) -> [UInt8] { [cmd, 0x91, 0x00, sub, 0x00, UInt8(data.count), 0x00, 0x00] + data } let initSequence: [(String, [UInt8])] = [ ("07/01", command(0x07, 0x01)), ("feature mask", command(0x0C, 0x02, [0x27, 0, 0, 0])), ("11/01", command(0x11, 0x01)), ("vibration data", command(0x0A, 0x08, [0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x35, 0x00, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])), ("enable features", command(0x0C, 0x04, [0x27, 0, 0, 0])), ("01/0C", command(0x01, 0x0C)), ("01/01", command(0x01, 0x01)), ("select report 0x09", command(0x03, 0x0A, [0x09, 0, 0, 0])), ("initialise USB", command(0x03, 0x0D, [0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])), ("player LED 1", command(0x09, 0x07, [0x01, 0, 0, 0, 0, 0, 0, 0])), ] 順番と内容は SDL3 の実装に準拠しています(意味が分かっていないコマンドもあります)。1 点だけ SDL と変えていて、入力レポートに 0x09 を選んでいます(SDL は 0x05)。 :::note info なぜ report 0x09 か USB の HID レポートディスクリプタ上、0x09 は「21 ボタン + X/Y/Rx/Rz(12bit)」の標準 Game Pad として記述されています。macOS 標準の HID ドライバがそのまま解釈できるので、自前のアプリだけでなく他のアプリからも普通のゲームパッドとして見えます。0x05 はベンダー定義で、自分で解析する以外に使い道がありません。 ::: 同じインターフェースでフラッシュ読み出し(02/01)もでき、スティックの工場較正値などが取れます(詳細はプロトコルメモ)。 4. IOHIDManager で入力を読む 初期化が終わると IF#0 から report 0x09 が流れてきます。これは IOHIDManager で読みます。 let reportBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: 128) let manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone)) func startHID() { IOHIDManagerSetDeviceMatching(manager, [ kIOHIDVendorIDKey: 0x057E, kIOHIDProductIDKey: 0x2069, kIOHIDPrimaryUsagePageKey: kHIDPage_GenericDesktop, kIOHIDPrimaryUsageKey: kHIDUsage_GD_GamePad, ] as CFDictionary) IOHIDManagerRegisterDeviceMatchingCallback(manager, { _, _, _, device in IOHIDDeviceRegisterInputReportCallback(device, reportBuffer, 128, { _, _, _, _, _, report, length in handleReport(Array(UnsafeBufferPointer(start: report, count: length))) }, nil) }, nil) IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue) // kIOHIDOptionsTypeNone = 非排他。ゲームや Steam も同時に読める IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone)) } ポイント: - kIOHIDOptionsTypeSeizeDevice で開くと排他になり、他のアプリから見えなくなります。ブリッジ用途なら非排他で開く - IOHIDManager はプロセスが生きている間ずっと参照を持っておく(ローカル変数にすると解放されてコールバックが来なくなる) - C の関数ポインタとして渡すクロージャはキャプチャできません。グローバル関数を呼ぶか、context に Unmanaged.passUnretained(self).toOpaque() を渡してインスタンスを取り出します - USB HID のコールバックでは先頭に report ID(0x09)が付いた 64 バイトが来ました レポートの中身(report ID 込みのオフセット): | offset | 内容 | |---|---| | 0 | report ID 0x09 | | 1 | カウンタ | | 2 | 電源(bit0 外部電源 / bit1 充電中 / bit2-5 残量 0-9) | | 3-5 | ボタン 24bit | | 6-8 | 左スティック(12bit × 2) | | 9-11 | 右スティック(12bit × 2) | let buttons = UInt32(r[3]) | UInt32(r[4]) << 8 | UInt32(r[5]) << 16 // 3 バイトに 12bit 値が 2 つ詰まっている func stick(_ i: Int) -> (Int, Int) { (Int(r[i]) | Int(r[i + 1] & 0x0F) << 8, Int(r[i + 1] >> 4) | Int(r[i + 2]) << 4) } ボタンのビット割り当ては byte3 が B A Y X R ZR + RS、byte4 が ↓ → ← ↑ L ZL − LS、byte5 が HOME Capture GR GL C(LSB から)です。 ここまでで最小サンプルは完成です。 git clone https://github.com/kakignu/switch2pro-macos-sample.git cd switch2pro-macos-sample swift run -c release switch2-sample アプリにするときの追加要素 最小サンプルから、実用アプリ(Switch2ProBridge)にするために足したものです。 抜き差しの監視 IOServiceAddMatchingNotification で kIOFirstMatchNotification / kIOTerminatedNotification を登録し、IONotificationPortSetDispatchQueue で専用キューに流します。挿された時に初期化を送り直さないと、HID 入力は止まったままです。 入力をキー/マウスに変換する(CGEvent) macOS ネイティブのゲームでパッドを使えるようにする手段がないので(後述)、現実解としてキー/マウス操作に変換します。 let e = CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: true) e?.post(tap: .cghidEventTap) CGEvent.post にはアクセシビリティ許可が必要です(AXIsProcessTrustedWithOptions でダイアログを出せます)。 :::note warn アクセシビリティ許可はコード署名とパスに紐づく ad-hoc 署名(codesign -s -)だとビルドのたびに署名が変わり、「ON にしているのに効かない」状態になります。Apple Development 証明書など安定した ID で署名し、/Applications の 1 か所に置いて起動するのが確実です。ad-hoc で開発するなら tccutil reset Accessibility <bundle id> で毎回リセットします。 ::: Bluetooth LE Switch 2 Pro Controller は BLE でも接続できますが、HOGP(HID over GATT)ではなく任天堂独自の GATT サービスです。そのため macOS の Bluetooth 設定からペアリングしても使えず、CoreBluetooth で自前実装します。 - SYNC ボタン長押しで、Manufacturer Data(会社 ID 0x0553、VID/PID 入り)付きの広告が出る → scanForPeripherals で拾う - 入力レポートは Notify キャラクタリスティックで届く(USB と同じ 0x09 形式、report ID なし) - コマンドは Write Without Response で送る(ヘッダの transport を 0x01 に) - 標準の SMP ペアリングを始めると切断されるので、ペアリングはしない :::note warn CoreBluetooth を使うには Info.plist に NSBluetoothAlwaysUsageDescription が必要です。swift run で直接起動するとクラッシュするので、.app バンドルを組み立てて起動します。 ::: .app バンドルを組み立てる Xcode プロジェクトを作らなくても、SwiftPM でビルドしたバイナリを .app の形に置けば動きます。 swift build -c release --product Switch2ProBridge BIN="$(swift build -c release --show-bin-path)/Switch2ProBridge" APP=build/Switch2ProBridge.app mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" cp "$BIN" "$APP/Contents/MacOS/" cp Resources/Info.plist "$APP/Contents/Info.plist" # LSUIElement, NSBluetoothAlwaysUsageDescription など codesign --force --deep --sign "Apple Development: ..." "$APP" メニューバー常駐にするなら Info.plist に LSUIElement = true、UI は SwiftUI の MenuBarExtra で書けます。 できなかったこと: Apple 純正のゲームコントローラーとして見せる 一番やりたかったのは「GameController.framework 対応のゲームでそのまま使う」ことですが、ユーザー空間からは無理でした。 - GameController.framework は既知のコントローラー(MFi / Xbox / DualSense / Switch Pro 初代 など)しか認識しない。Switch 2 Pro は 2026年10月時点で未対応 - 仮想 HID デバイス(CoreHID の HIDVirtualDevice / IOHIDUserDevice)で既知コントローラーになりすます案は、com.apple.developer.hid.virtual.device という Apple 承認制の entitlement が必要。加えて Apple のエンジニアがフォーラムで「OS は仮想 HID デバイスを意図的に無視する」と回答している(Virtual Controllers on Mac) - DriverKit で USB デバイスとして見せる案は、任天堂 VID を指定した entitlement が個人では得られない(開発時は SIP 無効化が必要) そのため、前述のキー/マウス割り当てを実用上の落としどころにしています。 配布: ソース非公開・Release だけ公開 今回はアプリ本体のソースを非公開にして、Release 専用の公開リポジトリを作り、そこにビルド済みの zip と README・技術資料(docs)だけを置いています。 - ソース: private リポジトリ - 配布: public リポジトリ(README / docs / Releases) - 手法の解説: この記事 + 最小サンプル(MIT) 証明書なし(ad-hoc 署名)で配るときにユーザーがやること Developer ID 証明書(Apple Developer Program、年額有料)が無い場合、.app は ad-hoc 署名で配ることになります。この場合、利用者側で次の対応が必要になるので、配布ページに手順を書いておきます(使い方ガイド)。 | タイミング | 起きること | 対応 | |---|---|---| | 初回起動 | Gatekeeper が「“〜”は開いていません」と表示してブロック | システム設定 → プライバシーとセキュリティ →「このまま開く」 | | アクセシビリティ等の許可 | 署名(cdhash)に紐づいて記録される | 通常どおり許可 | | アップデート | 署名が変わるので以前の許可が無効化(設定画面では ON のまま) | 一覧から削除して許可し直す / tccutil reset | :::note warn macOS 15 Sequoia 以降、Finder の右クリック →「開く」で Gatekeeper を回避する方法は廃止されました。 古い記事の手順は通用しないので、「プライバシーとセキュリティ」の「このまま開く」か、xattr -dr com.apple.quarantine <app> で隔離属性を外す方法を案内します。 ::: また、Apple Silicon では署名が一切ないバイナリは起動できないので、最低でも codesign --sign -(ad-hoc)は必要です。Apple Development 証明書(無料の Apple ID でも Xcode で作れる)は開発機での実行用で、配布物を Gatekeeper に通すことはできません。ちゃんと配るなら Developer ID 署名 + 公証(notarytool)が必要です。 まとめ - 他社 USB デバイスを Mac で動かすなら、まず ioreg でインターフェースが空いているかを見る - 空いていれば IOUSBHost でユーザー空間から直接 Bulk/Interrupt 転送できる(entitlement 不要) - すでに HID として見えている部分は IOHIDManager で非排他に読む - BLE は独自 GATT なら CoreBluetooth で自前実装 - OS 全体に「別の純正デバイス」として見せるのは entitlement の壁があり、個人ではほぼ不可能 kext や DriverKit に手を出す前に、ユーザー空間でどこまでできるかを確認してみてください。 参考 - ndeadly/switch2_controller_research — Switch 2 コントローラーのプロトコル調査 - SDL3 SDL_hidapi_switch2.c — 初期化シーケンス - IOUSBHost | Apple Developer Documentation - Apple Developer Forums: Virtual Controllers on Mac - 最小サンプル: kakignu/switch2pro-macos-sample - 配布ページ / 技術資料: kakignu/Switch2ProBridge-releases ※ Nintendo Switch は任天堂の商標です。本記事および各リポジトリは任天堂とは無関係の非公式なものです。

Trending awards, every completed day and week
Gold
01st place finishes
Silver
02nd place finishes
Bronze
03rd place finishes
Awarded repositories
0Owned by libsdl-org now

No trending awards yet

None of the repositories libsdl-org owns has finished in the top three of a completed trending day or week, by stars, forks or PR authors. See who is trending now.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.